diff --git a/server/src/assessment/assessment.module.ts b/server/src/assessment/assessment.module.ts index ab66844..833846b 100644 --- a/server/src/assessment/assessment.module.ts +++ b/server/src/assessment/assessment.module.ts @@ -9,6 +9,7 @@ import { AssessmentTemplate } from './entities/assessment-template.entity'; import { AssessmentCertificate } from './entities/assessment-certificate.entity'; import { QuestionBank } from './entities/question-bank.entity'; import { QuestionBankItem } from './entities/question-bank-item.entity'; +import { QuestionBankTemplate } from './entities/question-bank-template.entity'; import { KnowledgeBaseModule } from '../knowledge-base/knowledge-base.module'; import { KnowledgeGroupModule } from '../knowledge-group/knowledge-group.module'; import { ModelConfigModule } from '../model-config/model-config.module'; @@ -36,6 +37,7 @@ import { AuditLogService } from './services/audit-log.service'; AssessmentCertificate, QuestionBank, QuestionBankItem, + QuestionBankTemplate, AuditLog, ]), forwardRef(() => KnowledgeBaseModule), diff --git a/server/src/assessment/entities/assessment-template.entity.ts b/server/src/assessment/entities/assessment-template.entity.ts index f38439e..1dab44c 100644 --- a/server/src/assessment/entities/assessment-template.entity.ts +++ b/server/src/assessment/entities/assessment-template.entity.ts @@ -106,6 +106,26 @@ export class AssessmentTemplate { @Column({ type: 'int', name: 'per_question_time_limit', default: 300 }) perQuestionTimeLimit: number; + /** P2: Max attempts (0=unlimited) */ + @Column({ type: 'int', name: 'attempt_limit', default: 1 }) + attemptLimit: number; + + /** P2: Scheduled window start (null=anytime) */ + @Column({ type: 'text', name: 'scheduled_start', nullable: true }) + scheduledStart: string | null; + + /** P2: Scheduled window end (null=anytime) */ + @Column({ type: 'text', name: 'scheduled_end', nullable: true }) + scheduledEnd: string | null; + + /** P2: Review mode: 'none' | 'after_completion' | 'per_question' */ + @Column({ type: 'varchar', name: 'review_mode', default: 'none', length: 20 }) + reviewMode: string; + + /** P2: Shuffle questions per candidate */ + @Column({ type: 'boolean', name: 'shuffle_questions', default: true }) + shuffleQuestions: boolean; + @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/server/src/assessment/entities/question-bank-item.entity.ts b/server/src/assessment/entities/question-bank-item.entity.ts index fe52df9..e474ec7 100644 --- a/server/src/assessment/entities/question-bank-item.entity.ts +++ b/server/src/assessment/entities/question-bank-item.entity.ts @@ -92,6 +92,10 @@ export class QuestionBankItem { @Column({ type: 'simple-json', nullable: true, name: 'followup_hints' }) followupHints: string[] | null; + /** P1: Tags for cross-category filtering */ + @Column({ type: 'simple-json', nullable: true }) + tags: string[] | null; + @Column({ name: 'created_by', nullable: true, type: 'text' }) createdBy: string | null; diff --git a/server/src/assessment/entities/question-bank-template.entity.ts b/server/src/assessment/entities/question-bank-template.entity.ts new file mode 100644 index 0000000..32f48b4 --- /dev/null +++ b/server/src/assessment/entities/question-bank-template.entity.ts @@ -0,0 +1,37 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { QuestionBank } from './question-bank.entity'; +import { AssessmentTemplate } from './assessment-template.entity'; + +/** + * P1: Join table for QuestionBank <-> AssessmentTemplate many-to-many + * Allows one question bank to be used across multiple templates. + */ +@Entity('question_bank_templates') +export class QuestionBankTemplate { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'bank_id' }) + bankId: string; + + @ManyToOne(() => QuestionBank, (bank) => bank.id, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'bank_id' }) + bank: QuestionBank; + + @Column({ name: 'template_id' }) + templateId: string; + + @ManyToOne(() => AssessmentTemplate, (tpl) => tpl.id, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'template_id' }) + template: AssessmentTemplate; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/web/components/views/AssessmentView.tsx b/web/components/views/AssessmentView.tsx index c0ca4ae..57950b1 100644 --- a/web/components/views/AssessmentView.tsx +++ b/web/components/views/AssessmentView.tsx @@ -57,6 +57,10 @@ export const AssessmentView: React.FC = ({ const [autoSubmitted, setAutoSubmitted] = useState(false); const [showCertModal, setShowCertModal] = useState(false); const [certData, setCertData] = useState(null); + // P0: Flagged questions for review + const [flaggedQuestions, setFlaggedQuestions] = useState>(new Set()); + // P0: Submit confirmation modal + const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout; const messagesEndRef = useRef(null); @@ -241,6 +245,28 @@ export const AssessmentView: React.FC = ({ } }; + // P0: Toggle flag for current question + const toggleFlag = () => { + const idx = state?.currentQuestionIndex ?? 0; + setFlaggedQuestions(prev => { + const next = new Set(prev); + if (next.has(idx)) next.delete(idx); + else next.add(idx); + return next; + }); + }; + + // P0: Confirm & submit + const confirmAndSubmit = async () => { + const totalQs = state?.questions?.length || 0; + const answered = state?.scores ? Object.keys(state.scores).length : 0; + if (answered < totalQs && totalQs > 0) { + setShowSubmitConfirm(true); + return; + } + await handleSubmitAnswer(); + }; + const handleSubmitAnswer = async (forced = false) => { const currentQuestion = state?.questions?.[state.currentQuestionIndex || 0] as any; const isChoice = currentQuestion?.questionType === 'MULTIPLE_CHOICE' && currentQuestion?.options?.length > 0; @@ -546,9 +572,17 @@ export const AssessmentView: React.FC = ({
- + {progressLabel} + {/* P0: Question nav dots */} + {state?.questions && state.questions.length > 1 && ( +
+ {state.questions.map((_: any, qi: number) => ( +
+ ))} +
+ )} {isLoading && (
@@ -641,7 +675,7 @@ export const AssessmentView: React.FC = ({ })}
+ {/* P0: Flag button */} + {state?.questions && state.questions.length > 0 && ( + + )}
@@ -983,7 +1031,23 @@ export const AssessmentView: React.FC = ({
{renderHeader()} - {showCertModal && certData && createPortal( + {showSubmitConfirm && createPortal( +
+
+
+ +
+

提交答案确认

+

你已完成部分题目,确定要提交全部答案吗?已答题目将无法修改。

+
+ + +
+
+
, + document.body + )} + {showCertModal && certData && createPortal(
setShowCertModal(false)} />