diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index c9a793e..44d9d68 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -43,8 +43,31 @@ function getDb() { ai_score REAL, teacher_score REAL, confidence REAL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + question_title TEXT DEFAULT '', + max_score REAL DEFAULT 100, + reference_answer TEXT DEFAULT '', + rubric TEXT DEFAULT '', + deductions TEXT DEFAULT '', + comment TEXT DEFAULT '', + result_json TEXT DEFAULT '', + image_data TEXT DEFAULT '' )`).run() + // migrate: add columns for existing DBs + const migrateCols: { name: string; type: string }[] = [ + { name: 'question_title', type: 'TEXT DEFAULT \'\'' }, + { name: 'max_score', type: 'REAL DEFAULT 100' }, + { name: 'reference_answer', type: 'TEXT DEFAULT \'\'' }, + { name: 'rubric', type: 'TEXT DEFAULT \'\'' }, + { name: 'deductions', type: 'TEXT DEFAULT \'\'' }, + { name: 'comment', type: 'TEXT DEFAULT \'\'' }, + { name: 'result_json', type: 'TEXT DEFAULT \'\'' }, + { name: 'image_data', type: 'TEXT DEFAULT \'\'' }, + { name: 'reference_answer_image', type: 'TEXT DEFAULT \'\'' } + ] + for (const col of migrateCols) { + try { db.prepare(`ALTER TABLE history ADD COLUMN ${col.name} ${col.type}`).run() } catch (_) {} + } db.prepare(`CREATE TABLE IF NOT EXISTS exam ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, @@ -180,6 +203,15 @@ export function setupIpc(win: BrowserWindow): void { } }) + ipcMain.handle('history:getById', (_event, id: number) => { + try { + const record = getDb().prepare('SELECT * FROM history WHERE id = ?').get(id) as any + return record || null + } catch { + return null + } + }) + ipcMain.handle('grading:submit', (_event, data: { questionId: number imagePath: string @@ -187,22 +219,40 @@ export function setupIpc(win: BrowserWindow): void { teacherScore: number confidence: number studentAnswer?: string + questionTitle?: string + maxScore?: number + referenceAnswer?: string + rubric?: string + deductions?: string[] + comment?: string + resultJson?: string + imageData?: string + referenceAnswerImage?: string }) => { - try { - getDb().prepare(`INSERT INTO history (question_id, image_path, student_answer, ai_score, teacher_score, confidence) - VALUES (?, ?, ?, ?, ?, ?)`).run( - data.questionId || 0, - data.imagePath || '', - data.studentAnswer || '', - data.aiScore, - data.teacherScore, - data.confidence - ) - return true - } catch (err) { - log.error('grading:submit error:', err) - return false - } + log.info('grading:submit called', JSON.stringify({ ...data, imageData: data.imageData ? `len=${data.imageData.length}` : 'empty', referenceAnswerImage: data.referenceAnswerImage ? `len=${data.referenceAnswerImage.length}` : 'empty' })) + const deductionsStr = JSON.stringify(data.deductions || []) + getDb().prepare(`INSERT INTO history + (question_id, image_path, student_answer, ai_score, teacher_score, confidence, + question_title, max_score, reference_answer, rubric, deductions, comment, result_json, image_data, + reference_answer_image) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run( + data.questionId || 0, + data.imagePath || '', + data.studentAnswer || '', + data.aiScore, + data.teacherScore, + data.confidence, + data.questionTitle || '', + data.maxScore ?? 100, + data.referenceAnswer || '', + data.rubric || '', + deductionsStr, + data.comment || '', + data.resultJson || '', + data.imageData || '', + data.referenceAnswerImage || '' + ) + return true }) ipcMain.handle('template:list', () => { diff --git a/electron/preload/preload.ts b/electron/preload/preload.ts index 1911387..1265a7f 100644 --- a/electron/preload/preload.ts +++ b/electron/preload/preload.ts @@ -33,7 +33,8 @@ contextBridge.exposeInMainWorld('electronAPI', { history: { list: () => ipcRenderer.invoke('history:list'), - delete: (id: number) => ipcRenderer.invoke('history:delete', id) + delete: (id: number) => ipcRenderer.invoke('history:delete', id), + getById: (id: number) => ipcRenderer.invoke('history:getById', id) }, grading: { diff --git a/src/components/FloatingToolbar.vue b/src/components/FloatingToolbar.vue index 3245411..fef017f 100644 --- a/src/components/FloatingToolbar.vue +++ b/src/components/FloatingToolbar.vue @@ -76,8 +76,12 @@ onMounted(() => { if (window.electronAPI) { cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted(async (base64: string) => { if (!appStore.isGrading) { - gradingStore.setImage(await addWatermark(base64, '学生答案')) - startGrading() + try { + gradingStore.setImage(await addWatermark(base64, '学生答案')) + startGrading() + } catch (err) { + ElMessage.error('水印处理失败:' + (err as Error).message) + } } }) } @@ -95,7 +99,12 @@ async function selectImage() { if (!window.electronAPI) return const data = await window.electronAPI.dialog.openImage() if (data) { - gradingStore.setImage(await addWatermark(data.base64, '学生答案')) + try { + gradingStore.setImage(await addWatermark(data.base64, '学生答案')) + } catch (err) { + ElMessage.error('水印处理失败:' + (err as Error).message) + return + } startGrading() } } diff --git a/src/components/ResultDrawer.vue b/src/components/ResultDrawer.vue index fc2ae8c..694942f 100644 --- a/src/components/ResultDrawer.vue +++ b/src/components/ResultDrawer.vue @@ -6,8 +6,8 @@ +
{{ errorMsg }}
-
{{ errorMsg }}
上一题 {{ currentIdx + 1 }} / {{ questions.length }} @@ -187,14 +187,25 @@ async function prevQuestion() { async function saveAndAdvance(targetIdx: number) { try { - await window.electronAPI.grading.submit({ + const result = gradingStore.currentResult + const raw = { questionId: 0, imagePath: '', - aiScore: gradingStore.currentResult?.score ?? 0, + aiScore: result?.score ?? 0, teacherScore: teacherScore.value, - confidence: gradingStore.currentResult?.confidence ?? 0, - studentAnswer: '' - }) + confidence: result?.confidence ?? 0, + studentAnswer: '', + questionTitle: currentQuestion.value.questionTitle, + maxScore: currentQuestion.value.maxScore, + referenceAnswer: currentQuestion.value.referenceAnswer, + rubric: currentQuestion.value.rubric, + deductions: result?.deductions, + comment: result?.comment, + resultJson: JSON.stringify(result), + imageData: gradingStore.currentImage || '', + referenceAnswerImage: currentQuestion.value.referenceAnswerImage || '' + } + await window.electronAPI.grading.submit(JSON.parse(JSON.stringify(raw))) await window.electronAPI?.rubric.setNextIndex(targetIdx) gradingStore.reset() closeDrawer() @@ -208,23 +219,40 @@ async function reGrade() { } async function confirmScore() { + errorMsg.value = '' const result = gradingStore.currentResult - if (!result) return + if (!result) { + errorMsg.value = '暂无评分结果可保存(currentResult 为空)' + return + } + if (!window.electronAPI?.grading?.submit) { + errorMsg.value = 'electronAPI.grading.submit 不可用' + return + } try { - await window.electronAPI.grading.submit({ + const raw = { questionId: 0, imagePath: '', aiScore: result.score, teacherScore: teacherScore.value, confidence: result.confidence, - studentAnswer: '' - }) - ElMessage.success('评分已保存') + studentAnswer: '', + questionTitle: currentQuestion.value.questionTitle, + maxScore: currentQuestion.value.maxScore, + referenceAnswer: currentQuestion.value.referenceAnswer, + rubric: currentQuestion.value.rubric, + deductions: result.deductions, + comment: result.comment, + resultJson: JSON.stringify(result), + imageData: gradingStore.currentImage || '', + referenceAnswerImage: currentQuestion.value.referenceAnswerImage || '' + } + await window.electronAPI.grading.submit(JSON.parse(JSON.stringify(raw))) gradingStore.reset() closeDrawer() } catch (err) { - ElMessage.error('保存失败:' + (err as Error).message) + errorMsg.value = '保存失败:' + (err as Error).message } } diff --git a/src/pages/HistoryDialog.vue b/src/pages/HistoryDialog.vue index 25f5e86..c115002 100644 --- a/src/pages/HistoryDialog.vue +++ b/src/pages/HistoryDialog.vue @@ -2,17 +2,18 @@ - - - + + + - - - + + + @@ -23,10 +24,75 @@ 刷新 + +
+
+
+ 评分详情 + +
+
+
+ {{ detail.question_title }} +
+
+ 满分:{{ detail.max_score }} +
+ +
+
当前评分标准
+
{{ detail.rubric }}
+
+ +
+
学生答案图片
+ +
+ +
+
参考答案
+
{{ detail.reference_answer }}
+ +
+ +
+
{{ detail.ai_score }}
+
建议分数
+
+ +
+
+
+
+
可信度:{{ Math.round(detail.confidence * 100) }}%
+
+ +
+
扣分项
+
+ {{ idx + 1 }}. {{ item }} +
+
+ +
+
评语
+
{{ detail.comment }}
+
+ +
+
教师评分
+
{{ detail.teacher_score }} / {{ detail.max_score || 100 }}
+
+
+ +
+
+ + diff --git a/src/types/index.ts b/src/types/index.ts index 51e7b8a..8d3831b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,6 +41,15 @@ export interface HistoryRecord { teacher_score: number confidence: number created_at: string + question_title?: string + max_score?: number + reference_answer?: string + rubric?: string + deductions?: string + comment?: string + result_json?: string + image_data?: string + reference_answer_image?: string } export interface ElectronAPI { @@ -67,6 +76,7 @@ export interface ElectronAPI { history: { list: () => Promise delete: (id: number) => Promise + getById: (id: number) => Promise } grading: { submit: (data: { @@ -76,6 +86,15 @@ export interface ElectronAPI { teacherScore: number confidence: number studentAnswer?: string + questionTitle?: string + maxScore?: number + referenceAnswer?: string + rubric?: string + deductions?: string[] + comment?: string + resultJson?: string + imageData?: string + referenceAnswerImage?: string }) => Promise, storeData: (data: any) => Promise, getData: () => Promise diff --git a/src/utils/image.ts b/src/utils/image.ts index 1f489d4..51e7ea2 100644 --- a/src/utils/image.ts +++ b/src/utils/image.ts @@ -2,16 +2,25 @@ export function addWatermark(base64: string, text: string): Promise { return new Promise((resolve, reject) => { const img = new Image() img.onload = () => { + let w = img.width + let h = img.height + const MAX = 2048 + let scale = 1 + if (w > MAX || h > MAX) { + scale = Math.min(MAX / w, MAX / h, 1) + w = Math.round(w * scale) + h = Math.round(h * scale) + } const canvas = document.createElement('canvas') - canvas.width = img.width - canvas.height = img.height + canvas.width = w + canvas.height = h const ctx = canvas.getContext('2d') if (!ctx) return reject(new Error('Canvas 2D context not available')) - ctx.drawImage(img, 0, 0) - ctx.font = `bold ${Math.max(28, Math.round(img.width / 20))}px sans-serif` + ctx.drawImage(img, 0, 0, w, h) + ctx.font = `bold ${Math.max(28, Math.round(w / 20))}px sans-serif` ctx.fillStyle = 'rgba(255, 0, 0, 0.85)' ctx.textBaseline = 'top' - const padding = Math.max(8, Math.round(img.width / 60)) + const padding = Math.max(8, Math.round(w / 60)) ctx.fillText(text, padding, padding) resolve(canvas.toDataURL('image/png').replace(/^data:image\/png;base64,/, '')) }