From e84b0705e5dcec84e587f8a1f7c20e7fa2c196b5 Mon Sep 17 00:00:00 2001 From: brianling Date: Thu, 16 Jul 2026 13:21:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=84=E5=88=86=E6=A0=87=E5=87=86?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E4=B8=BA=E6=A8=A1=E6=9D=BF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ipc.ts | 56 ++++++++++++++++++++++++++--- electron/preload/preload.ts | 4 ++- src/components/RubricEditor.vue | 46 ++++++++++++++++++++++++ src/pages/TemplateDialog.vue | 64 +++++++++++++++++++++++++-------- src/types/index.ts | 14 ++++++-- 5 files changed, 161 insertions(+), 23 deletions(-) diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index 44d9d68..0983939 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -72,8 +72,10 @@ function getDb() { id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, course TEXT, + questions_json TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`).run() + try { db.prepare(`ALTER TABLE exam ADD COLUMN questions_json TEXT DEFAULT ''`).run() } catch (_) {} db.prepare(`CREATE TABLE IF NOT EXISTS question ( id INTEGER PRIMARY KEY AUTOINCREMENT, exam_id INTEGER, @@ -161,7 +163,12 @@ export function setupIpc(win: BrowserWindow): void { return result } catch (err: any) { log.error('AI grade failed:', err) - throw new Error(err.message || 'AI grading failed') + const apiMsg = err?.response?.data?.error?.message || '' + const allMsg = (apiMsg + ' ' + (err.message || '')).toLowerCase() + if (allMsg.includes('does not support image') || allMsg.includes('image input') || allMsg.includes('not support image')) { + throw new Error('当前模型不支持图片输入,请在设置中更换为支持多模态的模型(如 gpt-4o、qwen-vl 等)') + } + throw new Error(apiMsg || err.message || 'AI grading failed') } }) @@ -258,17 +265,24 @@ export function setupIpc(win: BrowserWindow): void { ipcMain.handle('template:list', () => { try { const db = getDb() - const exams = db.prepare('SELECT * FROM exam ORDER BY created_at DESC').all() - return exams + const exams = db.prepare('SELECT * FROM exam ORDER BY created_at DESC').all() as any[] + const result = exams.map((e: any) => { + const questions = e.questions_json ? JSON.parse(e.questions_json) : [] + log.info('template:list item', { id: e.id, name: e.name, jsonLen: (e.questions_json || '').length, questionCount: questions.length }) + return { ...e, questions } + }) + return result } catch { return [] } }) - ipcMain.handle('template:save', (_event, data: { name: string; course: string }) => { + ipcMain.handle('template:save', (_event, data: { name: string; course: string; questions?: any[] }) => { try { const db = getDb() - const result = db.prepare('INSERT INTO exam (name, course) VALUES (?, ?)').run(data.name, data.course) + const questionsJson = JSON.stringify(data.questions || []) + log.info('template:save', { name: data.name, course: data.course, questionCount: data.questions?.length ?? 0, jsonLen: questionsJson.length }) + const result = db.prepare('INSERT INTO exam (name, course, questions_json) VALUES (?, ?, ?)').run(data.name, data.course, questionsJson) return result.lastInsertRowid } catch (err) { log.error('template:save error:', err) @@ -276,6 +290,38 @@ export function setupIpc(win: BrowserWindow): void { } }) + ipcMain.handle('template:delete', (_event, id: number) => { + try { + getDb().prepare('DELETE FROM exam WHERE id = ?').run(id) + return true + } catch { + return false + } + }) + + ipcMain.handle('template:apply', (_event, id: number) => { + try { + const db = getDb() + const row = db.prepare('SELECT * FROM exam WHERE id = ?').get(id) as any + if (!row) throw new Error('模板不存在') + const questions = row.questions_json ? JSON.parse(row.questions_json) : [] + if (!questions.length) throw new Error('模板中没有题目') + db.prepare('DELETE FROM rubric').run() + const insert = db.prepare( + 'INSERT INTO rubric (question_title, max_score, reference_answer, rubric, sort_order, reference_answer_image) VALUES (?, ?, ?, ?, ?, ?)' + ) + for (let i = 0; i < questions.length; i++) { + const q = questions[i] + insert.run(q.questionTitle || '', q.maxScore ?? 100, q.referenceAnswer || '', q.rubric || '', i, q.referenceAnswerImage || '') + } + rubricNextIndex = 0 + return true + } catch (err) { + log.error('template:apply error:', err) + return false + } + }) + // --- Dialog child windows --- ipcMain.handle('dialog:open', (_event, { type, data }: { type: string; data?: any }) => { log.info('IPC: dialog:open', type) diff --git a/electron/preload/preload.ts b/electron/preload/preload.ts index 1265a7f..8c814dc 100644 --- a/electron/preload/preload.ts +++ b/electron/preload/preload.ts @@ -62,7 +62,9 @@ contextBridge.exposeInMainWorld('electronAPI', { template: { list: () => ipcRenderer.invoke('template:list'), - save: (data: { name: string; course: string }) => ipcRenderer.invoke('template:save', data) + save: (data: { name: string; course: string; questions?: any[] }) => ipcRenderer.invoke('template:save', data), + delete: (id: number) => ipcRenderer.invoke('template:delete', id), + apply: (id: number) => ipcRenderer.invoke('template:apply', id) }, dialog: { diff --git a/src/components/RubricEditor.vue b/src/components/RubricEditor.vue index 173da9a..2a07e81 100644 --- a/src/components/RubricEditor.vue +++ b/src/components/RubricEditor.vue @@ -46,12 +46,28 @@ +
+ + + + + + + + + 保存 + + +
@@ -68,6 +84,9 @@ const gradingStore = useGradingStore() const visible = ref(false) const activeIndex = ref(0) const questions = ref([]) +const showTemplateForm = ref(false) +const templateName = ref('') +const templateCourse = ref('') function emptyQuestion(): QuestionItem { return { questionTitle: '', maxScore: 100, referenceAnswer: '', rubric: '' } @@ -135,6 +154,28 @@ async function handleImport() { } } +async function saveAsTemplate() { + if (!templateName.value || !templateCourse.value) { + ElMessage.warning('请填写模板名称和课程') + return + } + try { + const ok = await window.electronAPI?.template.save({ + name: templateName.value, + course: templateCourse.value, + questions: JSON.parse(JSON.stringify(questions.value)) + }) + if (ok) { + ElMessage.success('模板已保存') + templateName.value = '' + templateCourse.value = '' + showTemplateForm.value = false + } + } catch (err) { + ElMessage.error('保存模板失败:' + (err as Error).message) + } +} + function handleClose() { window.electronAPI?.dialog.close() } @@ -217,4 +258,9 @@ function removeRefImage(idx: number) { border-radius: 4px; border: 1px solid #e4e7ed; } +.template-form { + padding: 8px 16px; + border-top: 1px solid #eee; + background: #fafafa; +} \ No newline at end of file diff --git a/src/pages/TemplateDialog.vue b/src/pages/TemplateDialog.vue index e6353cf..e6810f0 100644 --- a/src/pages/TemplateDialog.vue +++ b/src/pages/TemplateDialog.vue @@ -1,5 +1,5 @@