feat: 题库管理功能完善

- QuestionBankView: 添加删除按钮、卡片点击跳转详情页
- QuestionBankDetailView: 新建题库详情页(题目CRUD/AI生成/审核)
- questionBankService: 添加generateQuestions方法
- index.tsx: 添加详情页路由
This commit is contained in:
Developer
2026-05-13 21:51:33 +08:00
parent 8686d101cd
commit b2c17e3eca
4 changed files with 632 additions and 6 deletions
+73 -6
View File
@@ -1,9 +1,15 @@
import React, { useState, useEffect } from 'react';
import { Plus, BookOpen, ChevronRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Plus, BookOpen, ChevronRight, Trash2, Edit2 } from 'lucide-react';
import { apiClient } from '../../services/apiClient';
import { templateService } from '../../services/templateService';
import { questionBankService } from '../../services/questionBankService';
import { AssessmentTemplate } from '../../types';
interface QuestionBankViewProps {
isAdmin?: boolean;
}
interface QuestionBank {
id: string;
name: string;
@@ -13,7 +19,8 @@ interface QuestionBank {
createdAt: string;
}
export default function QuestionBankView() {
export default function QuestionBankView({ isAdmin }: QuestionBankViewProps) {
const navigate = useNavigate();
const [banks, setBanks] = useState<QuestionBank[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
@@ -26,6 +33,7 @@ export default function QuestionBankView() {
const [saving, setSaving] = useState(false);
const [templates, setTemplates] = useState<AssessmentTemplate[]>([]);
const [loadingTemplates, setLoadingTemplates] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
useEffect(() => {
fetchData();
@@ -86,6 +94,26 @@ export default function QuestionBankView() {
}
};
const handleDelete = async (e: React.MouseEvent, bankId: string, bankName: string) => {
e.stopPropagation();
if (!confirm(`确定要删除题库"${bankName}"吗?此操作不可恢复。`)) return;
setDeletingId(bankId);
try {
await questionBankService.deleteBank(bankId);
fetchData();
} catch (err: any) {
console.error('删除失败:', err);
alert('删除失败: ' + (err.message || '未知错误'));
} finally {
setDeletingId(null);
}
};
const handleCardClick = (bank: QuestionBank) => {
navigate(`/question-banks/${bank.id}`);
};
return (
<div className="p-6 bg-white min-h-screen">
<div className="flex items-center justify-between mb-6">
@@ -111,10 +139,49 @@ export default function QuestionBankView() {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{banks.map((bank) => (
<div key={bank.id} className="border rounded-lg p-4">
<h3 className="font-semibold">{bank.name}</h3>
<p className="text-sm text-gray-500">{bank.description}</p>
<p className="text-xs text-gray-400 mt-2">: {bank.status}</p>
<div
key={bank.id}
className="border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer group relative"
onClick={() => handleCardClick(bank)}
>
<div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => { e.stopPropagation(); handleCardClick(bank); }}
className="p-1.5 text-gray-400 hover:text-blue-600 rounded-md bg-white border shadow-sm"
title="编辑"
>
<Edit2 size={14} />
</button>
<button
onClick={(e) => handleDelete(e, bank.id, bank.name)}
disabled={deletingId === bank.id}
className="p-1.5 text-gray-400 hover:text-red-600 rounded-md bg-white border shadow-sm disabled:opacity-50"
title="删除"
>
{deletingId === bank.id ? (
<span className="w-3.5 h-3.5 border-2 border-red-500 border-t-transparent rounded-full animate-spin block"></span>
) : (
<Trash2 size={14} />
)}
</button>
</div>
<h3 className="font-semibold pr-16">{bank.name}</h3>
<p className="text-sm text-gray-500 mt-1">{bank.description || '暂无描述'}</p>
<div className="flex items-center justify-between mt-3 pt-3 border-t">
<span className={`text-xs px-2 py-0.5 rounded-full ${
bank.status === 'PUBLISHED' ? 'bg-green-100 text-green-700' :
bank.status === 'PENDING_REVIEW' ? 'bg-yellow-100 text-yellow-700' :
bank.status === 'REJECTED' ? 'bg-red-100 text-red-700' :
'bg-gray-100 text-gray-600'
}`}>
{bank.status === 'PUBLISHED' ? '已发布' :
bank.status === 'PENDING_REVIEW' ? '待审核' :
bank.status === 'REJECTED' ? '已否决' : '草稿'}
</span>
<span className="text-xs text-gray-400">
{new Date(bank.createdAt).toLocaleDateString()}
</span>
</div>
</div>
))}
</div>