@@ -1,8 +1,11 @@
import React , { useState , useEffect } from 'react' ;
import { createPortal } from 'react-dom' ;
import { useParams , useNavigate } from 'react-router-dom' ;
import {
ChevronLeft , Plus , Sparkles , Send , Check , X ,
Trash2 , Edit2 , FileText
import { motion , AnimatePresence } from 'framer-motion' ;
import {
ChevronLeft , Plus , Sparkles , Send , Check , X ,
Trash2 , Edit2 , FileText , Loader2 , BookOpen , Brain ,
AlertCircle , Hash , Layers
} from 'lucide-react' ;
import { questionBankService , QuestionBank , QuestionBankItem , CreateQuestionBankItemDto } from '../../services/questionBankService' ;
import { templateService } from '../../services/templateService' ;
@@ -28,10 +31,16 @@ const DIMENSIONS = [
{ value : 'WORK_CAPABILITY' , label : '工作能力' } ,
] ;
const typeIcons : Record < string , React.ReactNode > = {
SHORT_ANSWER : < FileText size = { 12 } / > ,
MULTIPLE_CHOICE : < Layers size = { 12 } / > ,
TRUE_FALSE : < Check size = { 12 } / > ,
} ;
export default function QuestionBankDetailView() {
const { id : bankId } = useParams < { id : string } > ( ) ;
const navigate = useNavigate ( ) ;
if ( ! bankId ) {
return (
< div className = "p-6" >
@@ -49,11 +58,11 @@ export default function QuestionBankDetailView() {
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 < QuestionBankItem | null > ( null ) ;
const [ itemForm , setItemForm ] = useState < CreateQuestionBankItemDto > ( {
questionText : '' ,
questionType : 'SHORT_ANSWER' ,
@@ -62,105 +71,59 @@ export default function QuestionBankDetailView() {
dimension : 'WORK_CAPABILITY' ,
} ) ;
const [ keyPointsInput , setKeyPointsInput ] = useState ( '' ) ;
const [ generateForm , setGenerateForm ] = useState ( {
count : 5 ,
knowledgeBaseContent : '' ,
} ) ;
const [ generating , setGenerating ] = useState ( false ) ;
useEffect ( ( ) = > {
fetchData ( ) ;
fetchTemplates ( ) ;
} , [ bankId ] ) ;
useEffect ( ( ) = > { fetchData ( ) ; fetchTemplates ( ) ; } , [ bankId ] ) ;
const fetchData = async ( ) = > {
try {
setLoading ( true ) ;
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 ) ;
}
} 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 ) ;
}
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 ( '' ) ;
await questionBankService . createItem ( bankId , { . . . itemForm , keyPoints : keyPointsInput.split ( '\n' ) . filter ( k = > k . trim ( ) ) } ) ;
closeItemForm ( ) ;
fetchData ( ) ;
} catch ( err : any ) {
alert ( '创建失败: ' + ( err . message || '未知错误' ) ) ;
} finally {
setSaving ( false ) ;
}
} 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 ( '' ) ;
await questionBankService . updateItem ( bankId , editingItem . id , { . . . itemForm , keyPoints : keyPointsInput.split ( '\n' ) . filter ( k = > k . trim ( ) ) } ) ;
closeItemForm ( ) ;
fetchData ( ) ;
} catch ( err : any ) {
alert ( '更新失败: ' + ( err . message || '未知错误' ) ) ;
} finally {
setSaving ( false ) ;
}
} 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 || '未知错误' ) ) ;
}
try { await questionBankService . deleteItem ( bankId , itemId ) ; fetchData ( ) ;
} catch ( err : any ) { alert ( '删除失败: ' + ( err . message || '未知错误' ) ) ; }
} ;
const handleGenerate = async ( ) = > {
@@ -170,70 +133,48 @@ export default function QuestionBankDetailView() {
setShowGenerate ( false ) ;
setGenerateForm ( { count : 5 , knowledgeBaseContent : '' } ) ;
fetchData ( ) ;
} catch ( err : any ) {
alert ( '生成失败: ' + ( err . message || '未知错误' ) ) ;
} finally {
setGenerating ( false ) ;
}
} 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 || '未知错误' ) ) ;
}
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 || '未知错误' ) ) ;
}
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 || '未知错误' ) ) ;
}
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 ,
} ) ;
setItemForm ( { questionText : item.questionText , questionType : item.questionType , options : item.options || [ ] , keyPoints : item.keyPoints , difficulty : item.difficulty , dimension : item.dimension } ) ;
setKeyPointsInput ( item . keyPoints . join ( '\n' ) ) ;
setShowAddItem ( true ) ;
} ;
const closeItemForm = ( ) = > { setShowAddItem ( false ) ; setEditingItem ( null ) ; } ;
const getStatusBadge = ( status : string ) = > {
switch ( status ) {
case 'PUBLISHED' :
return < span className = "px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-70 0" > 已 发 布 < / span > ;
case 'PENDING_REVIEW' :
return < span className = "px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-700" > 待 审 核 < / span > ;
default :
return < span className = "px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600" > 草 稿 < / span > ;
case 'PUBLISHED' : return < span className = "px-2.5 py-1 text-[10px] font-black uppercase tracking-widest rounded-full bg-emerald-50 text-emerald-600 border border-emerald-200/50" > 已 发 布 < / span > ;
case 'PENDING_REVIEW' : return < span className = "px-2.5 py-1 text-[10px] font-black uppercase tracking-widest rounded-full bg-amber-50 text-amber-600 border border-amber-200/5 0" > 待 审 核 < / span > ;
default : return < span className = "px-2.5 py-1 text-[10px] font-black uppercase tracking-widest rounded-full bg-slate-50 text-slate-500 border border-slate-200/50" > 草 稿 < / span > ;
}
} ;
if ( loading ) {
return (
< div className = "flex items-center justify-center h-64" >
< div className = "w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" > < / div >
< Loader2 className = "w-8 h-8 animate-spin text-blue-600 opacity-30" / >
< / div >
) ;
}
@@ -241,10 +182,8 @@ export default function QuestionBankDetailView() {
if ( error ) {
return (
< div className = "p-6" >
< button onClick = { ( ) = > navigate ( '/question-banks' ) } className = "flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4" >
< ChevronLeft size = { 20 } / > 返 回
< / button >
< div className = "text-red-500" > 加 载 失 败 : { error } < / div >
< button onClick = { ( ) = > navigate ( '/question-banks' ) } className = "flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4" > < ChevronLeft size = { 20 } / > 返 回 < / button >
< div className = "flex items-center gap-2 text-red-500 bg-red-50 rounded-2xl p-4 border border-red-100" > < AlertCircle size = { 18 } / > < span className = "text-sm font-bold" > { error } < / span > < / div >
< / div >
) ;
}
@@ -253,294 +192,204 @@ export default function QuestionBankDetailView() {
const publishedItems = items . filter ( i = > i . status === 'PUBLISHED' ) ;
return (
< div className = "p-6 bg-white min-h-screen " >
< button onClick = { ( ) = > navigate ( '/question-banks' ) } className = "flex items-center gap-2 text-gray-6 00 hover:text-gray-900 mb-6 " >
< ChevronLeft size = { 20 } / > 返 回 题 库 列 表
< div className = "space-y-6 " >
< button onClick = { ( ) = > navigate ( '/question-banks' ) } className = "flex items-center gap-2 text-slate-4 00 hover:text-slate-600 transition-colors mb-2 " >
< ChevronLeft size = { 18 } / > < span className = "text-xs font-black uppercase tracking-widest" > 返 回 题 库 列 表 < / span >
< / button >
< div className = "flex items-start justify-between mb-6 " >
< div >
< h1 className = "text-2xl font-bold" > { bank ? . name } < / h1 >
< p className = "text-gray-500 mt-1" > { bank ? . description || '暂无描述' } < / p >
< div className = "flex items-center gap-4 mt-2 " >
< span className = "text-sm text-gray-500 " >
模 板 : { templates . find ( t = > t . id === bank ? . templateId ) ? . name || '未关联' }
< / span >
{ getStatusBadge ( bank ? . status || 'DRAFT' ) }
< div className = "flex items-start justify-between" >
< div className = "flex items-center gap-4 " >
< div className = "w-14 h-14 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center shadow-sm" > < BookOpen size = { 28 } / > < / div >
< div >
< h1 className = "text-2xl font-black text-slate-900" > { bank ? . name } < / h1 >
< p className = "text-sm text-slate-500 mt-1 " > { bank ? . description || '暂无描述' } < / p >
< div className = "flex items-center gap-3 mt-2 " >
< span className = "text-[10px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-1.5" > < Brain size = { 12 } className = "text-blue-500" / > { templates . find ( t = > t . id === bank ? . templateId ) ? . name || '未关联模板 ' } < / span >
{ getStatusBadge ( bank ? . status || 'DRAFT' ) }
< / div >
< / div >
< / div >
< div className = "flex gap-2" >
{ bank ? . status === 'DRAFT' && (
< button
onClick = { handleSubmitForReview }
className = "flex items-center gap-2 px-4 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600"
>
< Send size = { 18 } / > 提 交 审 核
< button onClick = { handleSubmitForReview } className = "px-5 py-3 bg-amber-500 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-amber-100 hover:bg-amber-600 transition-all active:scale-95" >
< Send size = { 16 } / > 提 交 审 核
< / button >
) }
{ bank ? . status === 'PENDING_REVIEW' && (
< button
onClick = { handlePublish }
className = "flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
< Check size = { 18 } / > 发 布
< button onClick = { handlePublish } className = "px-5 py-3 bg-emerald-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-emerald-100 hover:bg-emerald-700 transition-all active:scale-95" >
< Check size = { 16 } / > 发 布
< / button >
) }
< button
onClick = { ( ) = > setShowGenerate ( true ) }
className = "flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
< Sparkles size = { 18 } / > AI生成
< button onClick = { ( ) = > setShowGenerate ( true ) } className = "px-5 py-3 bg-purple-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95" >
< Sparkles size = { 16 } / > AI生成
< / button >
< / div >
< / div >
< div className = "grid grid-cols-3 gap-4 mb-6" >
< div className = "bg-blue-50 rounded-lg p-4" >
< div className = "text-2xl font-bold text-blue-600" > { items . length } < / div >
< div className = "text-sm text-gray-600" > 总 题 目 数 < / div >
< / div >
< div className = "bg-yellow-50 rounded-lg p-4" >
< div className = "text-2xl font-bold text-yellow-600" > { pendingItems . length } < / div >
< div className = "text-sm text-gray-600" > 待 审 核 < / div >
< / div >
< div className = "bg-green-50 rounded-lg p-4" >
< div className = "text-2xl font-bold text-green-600" > { publishedItems . length } < / div >
< div className = "text-sm text-gray-600" > 已 发 布 < / div >
< / div >
< div className = "grid grid-cols-3 gap-4" >
{ [
{ label : '总题目数' , value : items.length , color : 'blue' , icon : < FileText size = { 16 } / > } ,
{ label : '待审核' , value : pendingItems.length , color : 'amber' , icon : < Send size = { 16 } / > } ,
{ label : '已发布' , value : publishedItems.length , color : 'emerald' , icon : < Check size = { 16 } / > } ,
] . map ( ( stat , i ) = > (
< motion.div key = { stat . label } initial = { { opacity : 0 , y : 10 } } animate = { { opacity : 1 , y : 0 } } transition = { { delay : i * 0.1 } }
className = { ` bg- ${ stat . color } -50/50 border border- ${ stat . color } -100/50 rounded-2xl p-4 ` } >
< div className = "flex items-center justify-between mb-2" >
< span className = { ` text-[10px] font-black uppercase tracking-widest text- ${ stat . color } -500 ` } > { stat . label } < / span >
< span className = { ` text- ${ stat . color } -500 ` } > { stat . icon } < / span >
< / div >
< div className = { ` text-3xl font-black text- ${ stat . color } -700 ` } > { stat . value } < / div >
< / motion.div >
) ) }
< / div >
< div className = "flex items-center justify-between mb-4" >
< h2 className = "text-lg font-semibold" > 题 目 列 表 < / h2 >
< button
onClick = { ( ) = > { setShowAddItem ( true ) ; setEditingItem ( null ) ; setKeyPointsInput ( '' ) ; } }
className = "flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
< Plus size = { 18 } / > 添 加 题 目
< div className = "flex items-center justify-between" >
< h2 className = "text-lg font-black text-slate-900" > 题 目 列 表 < / h2 >
< button onClick = { ( ) = > { setShowAddItem ( true ) ; setEditingItem ( null ) ; setKeyPointsInput ( '' ) ; setItemForm ( { questionText : '' , questionType : 'SHORT_ANSWER' , keyPoints : [ ] , difficulty : 'STANDARD' , dimension : 'WORK_CAPABILITY' } ) ; } }
className = "px-5 py-3 bg-blue-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95" >
< Plus size = { 16 } / > 添 加 题 目
< / button >
< / div >
{ items . length === 0 ? (
< div className = "text-center py-12 text-gray-500 border-2 border-dashed rounded-lg " >
< FileText size = { 48 } className = "mx-auto mb-4 text-gray-300 " / >
< p > 暂 无 题 目 , 点 击 上 方 按 钮 添 加 或 使 用 AI生成 < / p >
< div className = "bg-slate-50 rounded-[2rem] border-2 border-dashed border-slate-200 p-16 text-center " >
< FileText className = "w-14 h-14 text-slate-200 mx-auto mb-4 " / >
< p className = "text-slate-400 font-bold uppercase tracking-widest text-xs" > 暂 无 题 目 < / p >
< p className = "text-slate-300 text-xs mt-2" > 点 击 上 方 按 钮 添 加 或 使 用 AI生成 < / p >
< / div >
) : (
< div className = "space-y -4" >
{ items . map ( ( item ) = > (
< div key = { item . id } className = "border rounded-lg p-4" >
< div className = "flex items-start justify-between" >
< div className = "flex-1 " >
< div className = "flex items-center gap-2 mb-2" >
< span className = "text-xs px-2 py-0.5 rounded bg-gray-100 text-gray-60 0" >
{ QUESTION_TYPES . find ( t = > t . value === item . questionType ) ? . label }
< / span >
< span className = "text-xs px-2 py-0.5 rounded bg-blu e-1 00 text-blue-600" >
{ DIFFICULTIES . find ( d = > d . value === item . difficulty ) ? . label }
< / span >
< span className = "text-xs px-2 py-0.5 rounded bg-purple-100 text-purple-600" >
{ DIMENSIONS . find ( d = > d . value === item . dimension ) ? . label }
< / span >
{ getStatusBadge ( item . status ) }
< div className = "grid grid-cols-1 gap -4" >
< AnimatePresence mode = "popLayout" >
{ items . map ( ( item , idx ) = > (
< motion.div key = { item . id } layout initial = { { opacity : 0 , y : 10 } } animate = { { opacity : 1 , y : 0 } } exit = { { opacity : 0 , scale : 0.95 } } transition = { { delay : idx * 0.03 } }
className = "bg-white border border-slate-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition-all group relative overflow-hidden " >
< div className = "absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -mr-16 -mt-16" / >
< div className = "flex items-start justify-between relative z-1 0" >
< div className = "flex-1 min-w-0" >
< div className = "flex items-center gap-2 mb-2.5 flex-wrap" >
< span className = "inline-flex items-center gap-1 px-2.5 py-1 bg-slate-50 text-slat e-6 00 text-[10px] font-bold rounded-lg border border-slate-100" > { typeIcons [ item . questionType ] } { QUESTION_TYPES . find ( t = > t . value === item . questionType ) ? . label } < / span >
< span className = "inline-flex items-center gap-1 px-2.5 py-1 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100" > < Hash size = { 10 } / > { DIFFICULTIES . find ( d = > d . value === item . difficulty ) ? . label } < / span >
< span className = "inline-flex items-center gap-1 px-2.5 py-1 bg-purple-50 text-purple-600 text-[10px] font-bold rounded-lg border border-purple-100" > < Brain size = { 10 } / > { DIMENSIONS . find ( d = > d . value === item . dimension ) ? . label } < / span >
{ getStatusBadge ( item . status ) }
< / div >
< p className = "font-bold text-slate-900 leading-relaxed" > { item . questionText } < / p >
{ item . keyPoints . length > 0 && (
< div className = "mt-3 flex flex-wrap gap-1.5" >
< span className = "text-[10px] font-black text-slate-400 uppercase tracking-widest mr-1" > 评 分 要 点 : < / span >
{ item . keyPoints . map ( ( kp , i ) = > < span key = { i } className = "px-2.5 py-1 bg-amber-50 text-amber-700 text-[10px] font-bold rounded-lg border border-amber-100/50" > { kp } < / span > ) }
< / div >
) }
{ item . basis && < div className = "mt-2 flex items-center gap-1.5 text-[10px] text-slate-400" > < FileText size = { 10 } / > < span className = "font-medium" > 依 据 : < / span > < span > { item . basis } < / span > < / div > }
< / div >
< div className = "flex gap-1 ml-4 shrink-0" >
{ item . status === 'PENDING_REVIEW' && < button onClick = { ( ) = > handleApproveItem ( item . id ) } className = "p-2 text-emerald-600 hover:bg-emerald-50 rounded-xl transition-all" title = "通过" > < Check size = { 15 } / > < / button > }
< button onClick = { ( ) = > openEditItem ( item ) } className = "p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-all" title = "编辑" > < Edit2 size = { 15 } / > < / button >
< button onClick = { ( ) = > handleDeleteItem ( item . id ) } className = "p-2 text-red-600 hover:bg-red-50 rounded-xl transition-all" title = "删除" > < Trash2 size = { 15 } / > < / button >
< / div >
< p className = "font-medium" > { item . questionText } < / p >
{ item . keyPoints . length > 0 && (
< div className = "mt-2 text-sm text-gray-600" >
< span className = "font-medium" > 评 分 要 点 : < / span >
{ item . keyPoints . map ( ( kp , i ) = > (
< span key = { i } className = "mr-2" > • { kp } < / span >
) ) }
< / div >
) }
{ item . basis && (
< div className = "mt-2 text-xs text-gray-500" >
< span className = "font-medium" > 依 据 : < / span > { item . basis }
< / div >
) }
< / div >
< div className = "flex gap-1 ml-4" >
{ item . status === 'PENDING_REVIEW' && (
< button
onClick = { ( ) = > handleApproveItem ( item . id ) }
className = "p-1.5 text-green-600 hover:bg-green-50 rounded"
title = "通过"
>
< Check size = { 16 } / >
< / button >
) }
< button
onClick = { ( ) = > openEditItem ( item ) }
className = "p-1.5 text-blue-600 hover:bg-blue-50 rounded"
title = "编辑"
>
< Edit2 size = { 16 } / >
< / button >
< button
onClick = { ( ) = > handleDeleteItem ( item . id ) }
className = "p-1.5 text-red-600 hover:bg-red-50 rounded"
title = "删除"
>
< Trash2 size = { 16 } / >
< / button >
< / div >
< / div >
< / div >
) ) }
< / motion.div >
) ) }
< / AnimatePresence >
< / div >
) }
{ showAddItem && (
< div className = "fixed inset-0 bg-black/20 backdrop-blur-sm z-40" onClick = { ( ) = > { setShowAddItem ( false ) ; setEditingItem ( null ) ; } } / >
) }
< div className = { ` fixed right-0 top-0 h-full w-full max-w-lg bg-white shadow-2xl z-50 transform transition-transform duration-300 ${ showAddItem || editingItem ? 'translate-x-0' : 'translate-x-full' } ` } >
< div className = "flex flex-col h-full" >
< div className = "flex items-center justify-between px-6 py-4 border-b bg-slate-50" >
< h2 className = "text-xl font-semibold text-slate-800 " >
{ editingItem ? '编辑题目' : '添加题目' }
< / h2 >
< button onClick = { ( ) = > { setShowAddItem ( false ) ; setEditingItem ( null ) ; } } className = "p-2 text-slate-400 hover: text-slat e-600 rounded-full" >
< X size = { 24 } / >
< / button >
< / div >
< div className = "flex-1 overflow-y-auto p-6" >
< form id = "item-form" onSubmit = { editingItem ? handleUpdateItem : handleCreateItem } className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 题 目 内 容 * < / label >
< textarea
value = { itemForm . questionText }
onChange = { ( e ) = > setItemForm ( { . . . itemForm , questionText : e.target.value } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
placeholder = "输入题目内容"
rows = { 3 }
required
/ >
< / div >
< div className = "grid grid-cols-2 gap-4" >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 题 型 < / label >
< select
value = { itemForm . questionType }
onChange = { ( e ) = > setItemForm ( { . . . itemForm , questionType : e.target.value as any } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
>
{ QUESTION_TYPES . map ( t = > ( < option key = { t . value } value = { t . value } > { t . label } < / option > ) ) }
< / select >
{ createPortal (
< AnimatePresence >
{ ( showAddItem || editingItem ) && (
< div key = "question-item-modal" className = "fixed inset-0 z-[1000] flex items-center justify-center p-4" >
< motion.div initial = { { opacity : 0 } } animate = { { opacity : 1 } } exit = { { opacity : 0 } } onClick = { closeItemForm } className = "absolute inset-0 bg-slate-900/40 backdrop-blur-sm" / >
< motion.div initial = { { opacity : 0 , scale : 0.9 , y : 20 } } animate = { { opacity : 1 , scale : 1 , y : 0 } } exit = { { opacity : 0 , scale : 0.9 , y : 20 } }
className = "w-full max-w-xl bg-white rounded-[2.5rem] shadow-2xl relative z-10 overflow-hidden " >
< div className = "p-8 pb-4 flex items-center justify-between border-b border-slate-100" >
< div className = "flex items-center gap-3" >
< div className = "w-12 h-12 bg-blue-50 text-blu e-600 rounded-2xl flex items-center justify-center" > { editingItem ? < Edit2 size = { 24 } / > : < Plus size = { 24 } / > } < / div >
< h3 className = "text-xl font-black text-slate-900" > { editingItem ? '编辑题目' : '添加题目' } < / h3 >
< / div >
< button onClick = { closeItemForm } className = "p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-xl transition-all" > < X size = { 20 } / > < / button >
< / div >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 难 度 < / label >
< select
value = { itemForm . difficulty }
onChange = { ( e ) = > setItemForm ( { . . . itemForm , difficulty : e.target.value as any } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
>
{ DIFFICULTIES . map ( d = > ( < option key = { d . value } value = { d . value } > { d . label } < / option > ) ) }
< / select >
< / div >
< / div >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 维 度 < / label >
< select
value = { itemForm . dimension }
onChange = { ( e ) = > setItemForm ( { . . . itemForm , dimension : e.target.value as any } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
>
{ DIMENSIONS . map ( d = > ( < option key = { d . value } value = { d . value } > { d . label } < / option > ) ) }
< / select >
< / div >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 评 分 要 点 ( 每 行 一 个 ) < / label >
< textarea
value = { keyPointsInput }
onChange = { ( e ) = > setKeyPointsInput ( e . target . value ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
placeholder = "要点1 要点2 要点3"
rows = { 4 }
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-slate-7 00 mb-1" > 出 题 依 据 < / label >
< input
type = "text"
value = { itemForm . basis || '' }
onChange = { ( e ) = > setItemForm ( { . . . itemForm , basis : e.target.value } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
placeholder = "可选"
/ >
< / div >
< / form >
< / div >
< div className = "p-6 border-t bg-slate-50" >
< button
type = "submit"
form = "item-form"
disabled = { saving }
className = "w-full flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 text-white font-medium rounded-xl hover:bg-blue-700 disabled:opacity-50"
>
{ saving ? '保存中...' : ( editingItem ? '更新' : '添加' ) }
< / button >
< / div >
< / div >
< / div >
{ showGenerate && (
< >
< div className = "fixed inset-0 bg-black/20 backdrop-blur-sm z-40" onClick = { ( ) = > setShowGenerate ( false ) } / >
< div className = "fixed inset-0 flex items-center justify-center z-50" >
< div className = "bg-white rounded-2xl p-6 w-full max-w-md shadow-2xl" >
< h3 className = "text-lg font-semibold mb-4 flex items-center gap-2" >
< Sparkles className = "text-purple-600" size = { 20 } / >
AI生成题目
< / h3 >
< div className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 生 成 数 量 < / label >
< input
type = "number"
value = { generateForm . count }
onChange = { ( e ) = > 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 }
/ >
< / div >
< div >
< label className = "block text-sm font-medium text-slate-700 mb-1" > 知 识 库 内 容 ( 可 选 ) < / label >
< textarea
value = { generateForm . knowledgeBaseContent }
onChange = { ( e ) = > setGenerateForm ( { . . . generateForm , knowledgeBaseContent : e.target.value } ) }
className = "w-full px-4 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50"
placeholder = "输入知识库内容作为生成依据..."
rows = { 4 }
/ >
< / div >
< / div >
< div className = "flex gap-3 mt-6" >
< button
onClick = { ( ) = > setShowGenerate ( false ) }
className = "flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
>
取 消
< / button >
< button
onClick = { handleGenerate }
disabled = { generating }
className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{ generating ? (
< >
< div className = "w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" > < / div >
生 成 中 . . .
< / >
) : (
< >
< Sparkles size = { 18 } / > 生 成
< / >
) }
< / button >
< / div >
< form id = "item-form" onSubmit = { editingItem ? handleUpdateItem : handleCreateItem } className = "p-8 space-y-5" >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < FileText size = { 12 } className = "text-blue-500" / > 题 目 内 容 * < / label >
< textarea value = { itemForm . questionText } onChange = { ( e ) = > setItemForm ( { . . . itemForm , questionText : e.target.value } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" placeholder = "输入题目内容" rows = { 3 } required / >
< / div >
< div className = "grid grid-cols-2 gap-5" >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < Layers size = { 12 } className = "text-blue-500" / > 题 型 < / label >
< select value = { itemForm . questionType } onChange = { ( e ) = > setItemForm ( { . . . itemForm , questionType : e.target.value as any } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all appearance-none cursor-pointer" >
{ QUESTION_TYPES . map ( t = > < option key = { t . value } value = { t . value } > { t . label } < / option > ) }
< / select >
< / div >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < Hash size = { 12 } className = "text-blue-500" / > 难 度 < / label >
< select value = { itemForm . difficulty } onChange = { ( e ) = > setItemForm ( { . . . itemForm , difficulty : e.target.value as any } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all appearance-none cursor-pointer" >
{ DIFFICULTIES . map ( d = > < option key = { d . value } value = { d . value } > { d . label } < / option > ) }
< / select >
< / div >
< / div >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < Brain size = { 12 } className = "text-blue-500" / > 维 度 < / label >
< select value = { itemForm . dimension } onChange = { ( e ) = > setItemForm ( { . . . itemForm , dimension : e.target.value as any } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all appearance-none cursor-pointer" >
{ DIMENSIONS . map ( d = > < option key = { d . value } value = { d . value } > { d . label } < / option > ) }
< / select >
< / div >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < AlertCircle size = { 12 } className = "text-blue-500" / > 评 分 要 点 ( 每 行 一 个 ) < / label >
< textarea value = { keyPointsInput } onChange = { ( e ) = > setKeyPointsInput ( e . target . value ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder: text-slate-3 00" placeholder = "要点1
要点2
要点3" rows = { 4 } / >
< / div >
< div className = "flex justify-end gap-3 pt-4" >
< button type = "button" onClick = { closeItemForm } className = "px-6 py-4 text-sm font-black text-slate-500 hover:text-slate-700 transition-colors" > 取 消 < / button >
< button type = "submit" form = "item-form" disabled = { saving }
className = "px-10 py-4 bg-blue-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95 flex items-center gap-2" >
{ saving && < Loader2 size = { 16 } className = "animate-spin" / > } { saving ? '保存中...' : ( editingItem ? '更新' : '添加' ) } < / button >
< / div >
< / form >
< / motion.div >
< / div >
< / div >
< / >
) }
< / AnimatePresence > ,
document . body
) }
{ createPortal (
< AnimatePresence >
{ showGenerate && (
< div key = "generate-modal" className = "fixed inset-0 z-[1000] flex items-center justify-center p-4" >
< motion.div initial = { { opacity : 0 } } animate = { { opacity : 1 } } exit = { { opacity : 0 } } onClick = { ( ) = > setShowGenerate ( false ) } className = "absolute inset-0 bg-slate-900/40 backdrop-blur-sm" / >
< motion.div initial = { { opacity : 0 , scale : 0.9 , y : 20 } } animate = { { opacity : 1 , scale : 1 , y : 0 } } exit = { { opacity : 0 , scale : 0.9 , y : 20 } }
className = "w-full max-w-md bg-white rounded-[2.5rem] shadow-2xl relative z-10 overflow-hidden" >
< div className = "p-8 pb-4 flex items-center justify-between border-b border-slate-100" >
< div className = "flex items-center gap-3" >
< div className = "w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center" > < Sparkles size = { 24 } / > < / div >
< h3 className = "text-xl font-black text-slate-900" > AI生成题目 < / h3 >
< / div >
< button onClick = { ( ) = > setShowGenerate ( false ) } className = "p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-xl transition-all" > < X size = { 20 } / > < / button >
< / div >
< div className = "p-8 space-y-5" >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < Hash size = { 12 } className = "text-purple-500" / > 生 成 数 量 < / label >
< input type = "number" value = { generateForm . count } onChange = { ( e ) = > setGenerateForm ( { . . . generateForm , count : parseInt ( e . target . value ) || 5 } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500/50 outline-none transition-all" min = { 1 } max = { 20 } / >
< / div >
< div className = "space-y-1.5" >
< label className = "text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2" > < FileText size = { 12 } className = "text-purple-500" / > 知 识 库 内 容 ( 可 选 ) < / label >
< textarea value = { generateForm . knowledgeBaseContent } onChange = { ( e ) = > setGenerateForm ( { . . . generateForm , knowledgeBaseContent : e.target.value } ) }
className = "w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500/50 outline-none transition-all placeholder:text-slate-300" placeholder = "输入知识库内容作为生成依据..." rows = { 4 } / >
< / div >
< div className = "flex gap-3 pt-4" >
< button onClick = { ( ) = > setShowGenerate ( false ) } className = "flex-1 px-6 py-4 text-sm font-black text-slate-500 hover:text-slate-700 transition-colors" > 取 消 < / button >
< button onClick = { handleGenerate } disabled = { generating }
className = "flex-1 px-6 py-4 bg-purple-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95 flex items-center justify-center gap-2" >
{ generating ? < > < Loader2 size = { 16 } className = "animate-spin" / > 生 成 中 . . . < / > : < > < Sparkles size = { 16 } / > 生 成 < / > } < / button >
< / div >
< / div >
< / motion.div >
< / div >
) }
< / AnimatePresence > ,
document . body
) }
< / div >
) ;