diff --git a/web/components/views/QuestionBankDetailView.tsx b/web/components/views/QuestionBankDetailView.tsx
new file mode 100644
index 0000000..8d5f2bb
--- /dev/null
+++ b/web/components/views/QuestionBankDetailView.tsx
@@ -0,0 +1,547 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ ChevronLeft, Plus, Sparkles, Send, Check, X,
+ Trash2, Edit2, FileText
+} from 'lucide-react';
+import { questionBankService, QuestionBank, QuestionBankItem, CreateQuestionBankItemDto } from '../../services/questionBankService';
+import { templateService } from '../../services/templateService';
+import { AssessmentTemplate } from '../../types';
+
+const QUESTION_TYPES = [
+ { value: 'SHORT_ANSWER', label: '简答题' },
+ { value: 'MULTIPLE_CHOICE', label: '选择题' },
+ { value: 'TRUE_FALSE', label: '判断题' },
+];
+
+const DIFFICULTIES = [
+ { value: 'STANDARD', label: '标准' },
+ { value: 'ADVANCED', label: '高级' },
+ { value: 'SPECIALIST', label: '专家' },
+];
+
+const DIMENSIONS = [
+ { value: 'PROMPT', label: 'Prompt' },
+ { value: 'LLM', label: 'LLM' },
+ { value: 'IDE', label: 'IDE' },
+ { value: 'DEV_PATTERN', label: '开发模式' },
+ { value: 'WORK_CAPABILITY', label: '工作能力' },
+];
+
+export default function QuestionBankDetailView() {
+ const { id: bankId } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+
+ if (!bankId) {
+ return (
+
+
+
无效的题库ID
+
+ );
+ }
+
+ const [bank, setBank] = useState(null);
+ const [items, setItems] = useState([]);
+ const [templates, setTemplates] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState('');
+
+ const [showAddItem, setShowAddItem] = useState(false);
+ const [showGenerate, setShowGenerate] = useState(false);
+ const [editingItem, setEditingItem] = useState(null);
+
+ const [itemForm, setItemForm] = useState({
+ questionText: '',
+ questionType: 'SHORT_ANSWER',
+ keyPoints: [],
+ difficulty: 'STANDARD',
+ dimension: 'WORK_CAPABILITY',
+ });
+ const [keyPointsInput, setKeyPointsInput] = useState('');
+
+ const [generateForm, setGenerateForm] = useState({
+ count: 5,
+ knowledgeBaseContent: '',
+ });
+ const [generating, setGenerating] = useState(false);
+
+ useEffect(() => {
+ fetchData();
+ fetchTemplates();
+ }, [bankId]);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const bankData = await questionBankService.getBank(bankId);
+ setBank(bankData);
+ const itemsData = await questionBankService.getBankItems(bankId);
+ setItems(itemsData);
+ } catch (err: any) {
+ setError(err.message || '加载失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchTemplates = async () => {
+ try {
+ const data = await templateService.getAll();
+ setTemplates(data);
+ } catch (err) {
+ console.error('加载模板失败:', err);
+ }
+ };
+
+ const handleCreateItem = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!itemForm.questionText.trim()) return;
+
+ setSaving(true);
+ try {
+ const payload = {
+ ...itemForm,
+ keyPoints: keyPointsInput.split('\n').filter(k => k.trim()),
+ };
+ await questionBankService.createItem(bankId, payload);
+ setShowAddItem(false);
+ setItemForm({
+ questionText: '',
+ questionType: 'SHORT_ANSWER',
+ keyPoints: [],
+ difficulty: 'STANDARD',
+ dimension: 'WORK_CAPABILITY',
+ });
+ setKeyPointsInput('');
+ fetchData();
+ } catch (err: any) {
+ alert('创建失败: ' + (err.message || '未知错误'));
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleUpdateItem = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!editingItem || !itemForm.questionText.trim()) return;
+
+ setSaving(true);
+ try {
+ const payload = {
+ ...itemForm,
+ keyPoints: keyPointsInput.split('\n').filter(k => k.trim()),
+ };
+ await questionBankService.updateItem(bankId, editingItem.id, payload);
+ setEditingItem(null);
+ setItemForm({
+ questionText: '',
+ questionType: 'SHORT_ANSWER',
+ keyPoints: [],
+ difficulty: 'STANDARD',
+ dimension: 'WORK_CAPABILITY',
+ });
+ setKeyPointsInput('');
+ fetchData();
+ } catch (err: any) {
+ alert('更新失败: ' + (err.message || '未知错误'));
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleDeleteItem = async (itemId: string) => {
+ if (!confirm('确定要删除这道题目吗?')) return;
+ try {
+ await questionBankService.deleteItem(bankId, itemId);
+ fetchData();
+ } catch (err: any) {
+ alert('删除失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ const handleGenerate = async () => {
+ setGenerating(true);
+ try {
+ await questionBankService.generateQuestions(bankId, generateForm.count, generateForm.knowledgeBaseContent);
+ setShowGenerate(false);
+ setGenerateForm({ count: 5, knowledgeBaseContent: '' });
+ fetchData();
+ } catch (err: any) {
+ alert('生成失败: ' + (err.message || '未知错误'));
+ } finally {
+ setGenerating(false);
+ }
+ };
+
+ const handleSubmitForReview = async () => {
+ if (!confirm('确定要提交审核吗?')) return;
+ try {
+ await questionBankService.submitForReview(bankId);
+ fetchData();
+ } catch (err: any) {
+ alert('提交失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ const handlePublish = async () => {
+ if (!confirm('确定要发布题库吗?')) return;
+ try {
+ await questionBankService.publishBank(bankId);
+ fetchData();
+ } catch (err: any) {
+ alert('发布失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ const handleApproveItem = async (itemId: string) => {
+ try {
+ await questionBankService.updateItem(bankId, itemId, { status: 'PUBLISHED' as any });
+ fetchData();
+ } catch (err: any) {
+ alert('操作失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ const openEditItem = (item: QuestionBankItem) => {
+ setEditingItem(item);
+ setItemForm({
+ questionText: item.questionText,
+ questionType: item.questionType,
+ options: item.options || [],
+ keyPoints: item.keyPoints,
+ difficulty: item.difficulty,
+ dimension: item.dimension,
+ });
+ setKeyPointsInput(item.keyPoints.join('\n'));
+ };
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case 'PUBLISHED':
+ return 已发布;
+ case 'PENDING_REVIEW':
+ return 待审核;
+ default:
+ return 草稿;
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
加载失败: {error}
+
+ );
+ }
+
+ const pendingItems = items.filter(i => i.status === 'PENDING_REVIEW');
+ const publishedItems = items.filter(i => i.status === 'PUBLISHED');
+
+ return (
+
+
+
+
+
+
{bank?.name}
+
{bank?.description || '暂无描述'}
+
+
+ 模板: {templates.find(t => t.id === bank?.templateId)?.name || '未关联'}
+
+ {getStatusBadge(bank?.status || 'DRAFT')}
+
+
+
+ {bank?.status === 'DRAFT' && (
+
+ )}
+ {bank?.status === 'PENDING_REVIEW' && (
+
+ )}
+
+
+
+
+
+
+
{items.length}
+
总题目数
+
+
+
{pendingItems.length}
+
待审核
+
+
+
{publishedItems.length}
+
已发布
+
+
+
+
+
题目列表
+
+
+
+ {items.length === 0 ? (
+
+
+
暂无题目,点击上方按钮添加或使用AI生成
+
+ ) : (
+
+ {items.map((item) => (
+
+
+
+
+
+ {QUESTION_TYPES.find(t => t.value === item.questionType)?.label}
+
+
+ {DIFFICULTIES.find(d => d.value === item.difficulty)?.label}
+
+
+ {DIMENSIONS.find(d => d.value === item.dimension)?.label}
+
+ {getStatusBadge(item.status)}
+
+
{item.questionText}
+ {item.keyPoints.length > 0 && (
+
+ 评分要点:
+ {item.keyPoints.map((kp, i) => (
+ • {kp}
+ ))}
+
+ )}
+ {item.basis && (
+
+ 依据:{item.basis}
+
+ )}
+
+
+ {item.status === 'PENDING_REVIEW' && (
+
+ )}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {showAddItem && (
+
{ setShowAddItem(false); setEditingItem(null); }} />
+ )}
+
+
+
+
+ {editingItem ? '编辑题目' : '添加题目'}
+
+
+
+
+
+
+
+
+
+
+ {showGenerate && (
+ <>
+
setShowGenerate(false)} />
+
+
+
+
+ AI生成题目
+
+
+
+
+ setGenerateForm({...generateForm, count: parseInt(e.target.value) || 5})}
+ className="w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
+ min={1}
+ max={20}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/web/components/views/QuestionBankView.tsx b/web/components/views/QuestionBankView.tsx
index 9876c20..9f2c5a7 100644
--- a/web/components/views/QuestionBankView.tsx
+++ b/web/components/views/QuestionBankView.tsx
@@ -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
([]);
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([]);
const [loadingTemplates, setLoadingTemplates] = useState(false);
+ const [deletingId, setDeletingId] = useState(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 (
@@ -111,10 +139,49 @@ export default function QuestionBankView() {
) : (
{banks.map((bank) => (
-
-
{bank.name}
-
{bank.description}
-
状态: {bank.status}
+
handleCardClick(bank)}
+ >
+
+
+
+
+
{bank.name}
+
{bank.description || '暂无描述'}
+
+
+ {bank.status === 'PUBLISHED' ? '已发布' :
+ bank.status === 'PENDING_REVIEW' ? '待审核' :
+ bank.status === 'REJECTED' ? '已否决' : '草稿'}
+
+
+ {new Date(bank.createdAt).toLocaleDateString()}
+
+
))}
diff --git a/web/index.tsx b/web/index.tsx
index 3b72101..e3bf7ea 100644
--- a/web/index.tsx
+++ b/web/index.tsx
@@ -19,6 +19,7 @@ const NotebooksPage = lazy(() => import('./src/pages/workspace/NotebooksPage'));
const MemosPage = lazy(() => import('./src/pages/workspace/MemosPage'));
const SettingsPage = lazy(() => import('./src/pages/workspace/SettingsPage'));
const QuestionBankView = lazy(() => import('./components/views/QuestionBankView'));
+const QuestionBankDetailView = lazy(() => import('./components/views/QuestionBankDetailView'));
const AssessmentStatsView = lazy(() => import('./components/views/AssessmentStatsView'));
const PageLoader = () => (
@@ -90,6 +91,7 @@ function App() {
} />
} />
} />
+
} />
} />
} />
} />
diff --git a/web/services/questionBankService.ts b/web/services/questionBankService.ts
index 457d71a..be2882d 100644
--- a/web/services/questionBankService.ts
+++ b/web/services/questionBankService.ts
@@ -139,4 +139,14 @@ export const questionBankService = {
const response = await apiClient.request(`/question-banks/${bankId}/items/${itemId}`, { method: 'DELETE' });
if (!response.ok) throw new Error('Failed to delete item');
},
+
+ async generateQuestions(bankId: string, count: number, knowledgeBaseContent: string): Promise
{
+ const response = await apiClient.request(`/question-banks/${bankId}/generate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ count, knowledgeBaseContent }),
+ });
+ if (!response.ok) throw new Error('Failed to generate questions');
+ return await response.json();
+ },
};
\ No newline at end of file