fix: 代码整合修复 - Entity类型、题库生成、评估流程等14项修复
This commit is contained in:
@@ -50,6 +50,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
const [showBasis, setShowBasis] = useState(false);
|
||||
const [templates, setTemplates] = useState<AssessmentTemplate[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
|
||||
const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -91,6 +92,27 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
fetchHistory();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session || session.status !== 'IN_PROGRESS') {
|
||||
setTimeCheck(null);
|
||||
return;
|
||||
}
|
||||
const checkTime = async () => {
|
||||
try {
|
||||
const data = await assessmentService.checkTimeLimits(session.id);
|
||||
setTimeCheck(data);
|
||||
if (data.isTotalTimeout || data.isQuestionTimeout) {
|
||||
setError(t('timeLimitExceeded'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check time:', err);
|
||||
}
|
||||
};
|
||||
checkTime();
|
||||
const interval = setInterval(checkTime, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [session]);
|
||||
|
||||
const isZh = language === 'zh';
|
||||
const isJa = language === 'ja';
|
||||
|
||||
@@ -502,6 +524,12 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
{processStep || t('aiIsProcessing')}
|
||||
</span>
|
||||
)}
|
||||
{timeCheck && (
|
||||
<div className={`flex items-center gap-1 text-[10px] font-bold px-2 py-0.5 rounded-full ${timeCheck.totalTimeRemaining < 60 || timeCheck.questionTimeRemaining < 30 ? 'bg-red-50 text-red-600' : 'bg-slate-100 text-slate-600'}`}>
|
||||
<span>⏱</span>
|
||||
<span>{Math.floor(timeCheck.totalTimeRemaining / 60)}:{String(timeCheck.totalTimeRemaining % 60).padStart(2, '0')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -745,10 +773,64 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
{t('newAssessmentSession')}
|
||||
</button>
|
||||
<button
|
||||
className="px-8 py-4 bg-white border-2 border-slate-100 text-slate-700 rounded-2xl font-bold hover:bg-slate-50 transition-all active:scale-[0.98]"
|
||||
onClick={async () => {
|
||||
if (!session) return;
|
||||
try {
|
||||
const result = await assessmentService.exportPdf(session.id);
|
||||
const blob = new Blob([result.content], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to export PDF:', err);
|
||||
}
|
||||
}}
|
||||
className="px-6 py-4 bg-white border-2 border-slate-100 text-slate-700 rounded-2xl font-bold hover:bg-slate-50 transition-all active:scale-[0.98]"
|
||||
>
|
||||
{t('downloadPdfReport')}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!session) return;
|
||||
try {
|
||||
const result = await assessmentService.exportExcel(session.id);
|
||||
const binary = atob(result.buffer);
|
||||
const array = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
array[i] = binary.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([array], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Failed to export Excel:', err);
|
||||
}
|
||||
}}
|
||||
className="px-6 py-4 bg-white border-2 border-slate-100 text-slate-700 rounded-2xl font-bold hover:bg-slate-50 transition-all active:scale-[0.98]"
|
||||
>
|
||||
{t('exportExcel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
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')}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to get certificate:', err);
|
||||
}
|
||||
}}
|
||||
className="px-6 py-4 bg-amber-50 border-2 border-amber-200 text-amber-700 rounded-2xl font-bold hover:bg-amber-100 transition-all active:scale-[0.98]"
|
||||
>
|
||||
{t('viewCertificate')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,40 @@ export interface AssessmentState {
|
||||
finalScore?: number;
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: string;
|
||||
level: string;
|
||||
totalScore: number;
|
||||
passed: boolean;
|
||||
issuedAt: string;
|
||||
qrCode?: string;
|
||||
dimensionScores?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface TimeCheck {
|
||||
totalTimeRemaining: number;
|
||||
questionTimeRemaining: number;
|
||||
isTotalTimeout: boolean;
|
||||
isQuestionTimeout: boolean;
|
||||
}
|
||||
|
||||
export interface StatsData {
|
||||
totalAssessments: number;
|
||||
averageScore: number;
|
||||
completionRate: number;
|
||||
passRate: number;
|
||||
}
|
||||
|
||||
export interface RadarData {
|
||||
dimensions: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface TrendData {
|
||||
date: string;
|
||||
score: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class AssessmentService {
|
||||
async startSession(knowledgeBaseId: string, language: string, templateId?: string): Promise<AssessmentSession> {
|
||||
const { data } = await apiClient.post<AssessmentSession>('/assessment/start', { knowledgeBaseId, language, templateId });
|
||||
@@ -43,7 +77,12 @@ export class AssessmentService {
|
||||
}
|
||||
|
||||
async getHistory(): Promise<AssessmentSession[]> {
|
||||
const { data } = await apiClient.get<AssessmentSession[]>('/assessment');
|
||||
const { data } = await apiClient.get<AssessmentSession[]>('/assessment/history');
|
||||
return data;
|
||||
}
|
||||
|
||||
async getUserHistory(): Promise<AssessmentSession[]> {
|
||||
const { data } = await apiClient.get<AssessmentSession[]>('/assessment/history');
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -51,6 +90,75 @@ export class AssessmentService {
|
||||
await apiClient.delete(`/assessment/${sessionId}`);
|
||||
}
|
||||
|
||||
async getCertificate(sessionId: string): Promise<Certificate> {
|
||||
const { data } = await apiClient.get<Certificate>(`/assessment/${sessionId}/certificate`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async reviewAssessment(sessionId: string, newScore: number, comment?: string): Promise<AssessmentSession> {
|
||||
const { data } = await apiClient.put<AssessmentSession>(`/assessment/${sessionId}/review`, { newScore, comment });
|
||||
return data;
|
||||
}
|
||||
|
||||
async getStats(startDate?: string, endDate?: string, templateId?: string, knowledgeGroupId?: string): Promise<StatsData> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('startDate', startDate);
|
||||
if (endDate) params.append('endDate', endDate);
|
||||
if (templateId) params.append('templateId', templateId);
|
||||
if (knowledgeGroupId) params.append('knowledgeGroupId', knowledgeGroupId);
|
||||
const { data } = await apiClient.get<StatsData>(`/assessment/stats?${params.toString()}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getRadarStats(templateId?: string): Promise<RadarData> {
|
||||
const params = templateId ? `?templateId=${templateId}` : '';
|
||||
const { data } = await apiClient.get<RadarData>(`/assessment/stats/radar${params}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getTrendStats(startDate?: string, endDate?: string): Promise<TrendData[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('startDate', startDate);
|
||||
if (endDate) params.append('endDate', endDate);
|
||||
const { data } = await apiClient.get<TrendData[]>(`/assessment/stats/trend?${params.toString()}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async checkTimeLimits(sessionId: string): Promise<TimeCheck> {
|
||||
const { data } = await apiClient.get<TimeCheck>(`/assessment/${sessionId}/time-check`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async startNextQuestion(sessionId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await apiClient.post<{ success: boolean }>(`/assessment/${sessionId}/next-question`, {});
|
||||
return data;
|
||||
}
|
||||
|
||||
async exportExcel(sessionId: string): Promise<{ filename: string; buffer: string }> {
|
||||
const { data } = await apiClient.get<{ filename: string; buffer: string }>(`/assessment/${sessionId}/export/excel`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async exportPdf(sessionId: string): Promise<{ filename: string; content: string }> {
|
||||
const { data } = await apiClient.get<{ filename: string; content: string }>(`/assessment/${sessionId}/export/pdf`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async forceEnd(sessionId: string): Promise<AssessmentSession> {
|
||||
const { data } = await apiClient.post<AssessmentSession>(`/assessment/${sessionId}/force-end`, {});
|
||||
return data;
|
||||
}
|
||||
|
||||
async verifyCertificate(certificateId: string): Promise<{ valid: boolean; certificate?: Certificate; message?: string }> {
|
||||
const { data } = await apiClient.get(`/assessment/certificate/verify/${certificateId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPublicCertificate(sessionId: string): Promise<{ exists: boolean; certificate?: Certificate; message?: string }> {
|
||||
const { data } = await apiClient.get(`/assessment/certificate/public/${sessionId}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async *startSessionStream(sessionId: string, templateId?: string): AsyncIterableIterator<any> {
|
||||
const query = templateId ? `?templateId=${templateId}` : '';
|
||||
const response = await apiClient.request(`/assessment/${sessionId}/start-stream${query}`, {
|
||||
|
||||
Reference in New Issue
Block a user