feat: enriched certificate with template name, dimension scores, question details + Modal UI
- generateCertificate: return templateName, questionDetails, dimensionScores - Frontend: replace alert() with certificate Modal showing level, scores, dimensions, questions - Status label: change from '已验证' to '合格'
This commit is contained in:
@@ -1500,6 +1500,13 @@ const initialState: Partial<EvaluationState> = {
|
||||
const level = this.determineLevel(session.finalScore || 0);
|
||||
const qrCode = `cert://${sessionId}-${Date.now()}`;
|
||||
|
||||
const questionDetails = (session.questions_json || []).map((q: any, i: number) => ({
|
||||
index: i + 1,
|
||||
questionText: q.questionText?.substring(0, 100) || '',
|
||||
questionType: q.questionType || 'SHORT_ANSWER',
|
||||
dimension: q.dimension || '',
|
||||
}));
|
||||
|
||||
const certificate = this.certificateRepository.create({
|
||||
userId,
|
||||
sessionId,
|
||||
@@ -1512,7 +1519,13 @@ const initialState: Partial<EvaluationState> = {
|
||||
passed: (session as any).passed || false,
|
||||
});
|
||||
|
||||
return this.certificateRepository.save(certificate);
|
||||
const saved = await this.certificateRepository.save(certificate);
|
||||
return {
|
||||
...saved,
|
||||
templateName: session.template?.name || session.templateJson?.name || '-',
|
||||
userName: session.user?.displayName || session.user?.username || '',
|
||||
questionDetails,
|
||||
} as any;
|
||||
}
|
||||
|
||||
private determineLevel(score: number): string {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
Brain,
|
||||
Send,
|
||||
@@ -54,6 +55,8 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null);
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const [autoSubmitted, setAutoSubmitted] = useState(false);
|
||||
const [showCertModal, setShowCertModal] = useState(false);
|
||||
const [certData, setCertData] = useState<any>(null);
|
||||
const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout;
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -640,7 +643,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmitAnswer}
|
||||
onClick={() => handleSubmitAnswer()}
|
||||
disabled={!selectedChoice || isLoading || isTimedOut}
|
||||
className={cn(
|
||||
"w-full mt-3 h-14 flex items-center justify-center gap-2 rounded-2xl transition-all shadow-lg text-white font-bold",
|
||||
@@ -965,7 +968,8 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
if (!session) return;
|
||||
try {
|
||||
const cert = await assessmentService.getCertificate(session.id);
|
||||
alert(`${t('certificate')}: ${cert.level}\n${t('totalScore')}: ${cert.totalScore}\n${t('passed')}: ${cert.passed ? t('yes') : t('no')}`);
|
||||
setCertData(cert);
|
||||
setShowCertModal(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to get certificate:', err);
|
||||
}
|
||||
@@ -986,6 +990,58 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
<div className="flex flex-col h-full bg-white animate-in flex-1">
|
||||
{renderHeader()}
|
||||
|
||||
{showCertModal && certData && createPortal(
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" onClick={() => setShowCertModal(false)} />
|
||||
<div className="relative bg-white rounded-3xl shadow-2xl max-w-lg w-full p-8 max-h-[80vh] overflow-y-auto">
|
||||
<button onClick={() => setShowCertModal(false)} className="absolute top-4 right-4 p-2 text-slate-400 hover:text-slate-600 rounded-xl hover:bg-slate-100">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 5L15 15M15 5L5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
|
||||
</button>
|
||||
<div className="flex flex-col items-center text-center mb-6">
|
||||
<Award size={40} className="text-indigo-600 mb-3" />
|
||||
<h3 className="text-2xl font-black text-slate-900">{certData.level}</h3>
|
||||
<p className="text-sm text-slate-500 font-medium mt-1">{certData.templateName || '-'}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-slate-50 rounded-2xl p-4 text-center">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">总分</span>
|
||||
<p className="text-xl font-black text-slate-900 mt-1">{certData.totalScore}/10</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-2xl p-4 text-center">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">结果</span>
|
||||
<p className={`text-xl font-black mt-1 ${certData.passed ? 'text-emerald-600' : 'text-rose-600'}`}>{certData.passed ? '合格' : '不合格'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{certData.dimensionScores && (
|
||||
<div className="mb-6">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">维度得分</span>
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{Object.entries(certData.dimensionScores).map(([dim, score]: [string, any]) => (
|
||||
<div key={dim} className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-slate-600">{dim}</span>
|
||||
<span className="font-black text-slate-900">{score}/10</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{certData.questionDetails && (
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">题目列表</span>
|
||||
<div className="mt-2 space-y-1">
|
||||
{certData.questionDetails.map((qd: any) => (
|
||||
<div key={qd.index} className="text-xs text-slate-600 truncate">
|
||||
<span className="font-bold text-slate-400">#{qd.index}</span> {qd.questionText}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{error && (
|
||||
<motion.div
|
||||
|
||||
@@ -819,7 +819,7 @@ export const translations = {
|
||||
questionBasis: "出题依据",
|
||||
viewBasis: "查看依据",
|
||||
hideBasis: "隐藏依据",
|
||||
verified: "已验证",
|
||||
verified: "合格",
|
||||
fail: "失败",
|
||||
comprehensiveMasteryReport: "综合能力报告",
|
||||
newAssessmentSession: "新评测会话",
|
||||
@@ -1832,7 +1832,7 @@ export const translations = {
|
||||
questionBasis: "Question Basis",
|
||||
viewBasis: "View Basis",
|
||||
hideBasis: "Hide Basis",
|
||||
verified: "Verified",
|
||||
verified: "Qualified",
|
||||
fail: "Fail",
|
||||
comprehensiveMasteryReport: "Comprehensive Mastery Report",
|
||||
newAssessmentSession: "New Assessment Session",
|
||||
@@ -2847,7 +2847,7 @@ export const translations = {
|
||||
questionBasis: "出題の根拠",
|
||||
viewBasis: "根拠を表示",
|
||||
hideBasis: "根拠を非表示",
|
||||
verified: "検証済み",
|
||||
verified: "合格",
|
||||
fail: "失敗",
|
||||
comprehensiveMasteryReport: "包括的習熟度レポート",
|
||||
newAssessmentSession: "新しいアセスメントセッション",
|
||||
|
||||
Reference in New Issue
Block a user