feat: 评分标准保存为模板功能
This commit is contained in:
+51
-5
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -46,12 +46,28 @@
|
||||
<div class="footer-left">
|
||||
<el-button size="small" @click="handleImport">导入</el-button>
|
||||
<el-button size="small" @click="handleExport">导出</el-button>
|
||||
<el-button size="small" @click="showTemplateForm = !showTemplateForm">
|
||||
{{ showTemplateForm ? '取消' : '保存为模板' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<el-button size="small" @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="handleSave">应用</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="showTemplateForm" class="template-form">
|
||||
<el-form inline size="small">
|
||||
<el-form-item label="模板名称">
|
||||
<el-input v-model="templateName" placeholder="如:期末考试" />
|
||||
</el-form-item>
|
||||
<el-form-item label="课程">
|
||||
<el-input v-model="templateCourse" placeholder="如:Java" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" size="small" @click="saveAsTemplate">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -68,6 +84,9 @@ const gradingStore = useGradingStore()
|
||||
const visible = ref(false)
|
||||
const activeIndex = ref(0)
|
||||
const questions = ref<QuestionItem[]>([])
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="评分模板管理" width="600px" :modal="false" @close="handleClose">
|
||||
<el-dialog v-model="visible" title="评分模板管理" width="620px" :modal="false" @close="handleClose">
|
||||
<div class="template-section">
|
||||
<el-form :model="form" size="small" inline>
|
||||
<el-form-item label="考试名称">
|
||||
@@ -17,13 +17,17 @@
|
||||
<el-divider />
|
||||
|
||||
<el-table :data="templates" size="small" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="name" label="考试名称" min-width="150" />
|
||||
<el-table-column prop="course" label="课程" min-width="120" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="100">
|
||||
<el-table-column prop="id" label="ID" width="50" />
|
||||
<el-table-column prop="name" label="考试名称" min-width="130" />
|
||||
<el-table-column prop="course" label="课程" min-width="100" />
|
||||
<el-table-column label="题目数" width="70">
|
||||
<template #default="{ row }">{{ row.questions?.length || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button text size="small" @click="selectTemplate(row)">使用</el-button>
|
||||
<el-button text size="small" :disabled="!row.questions?.length" @click="selectTemplate(row)">使用</el-button>
|
||||
<el-button text size="small" type="danger" @click="deleteTemplate(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -35,15 +39,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
import type { TemplateItem, QuestionItem } from '@/types'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gradingStore = useGradingStore()
|
||||
const visible = ref(false)
|
||||
const templates = ref<any[]>([])
|
||||
const templates = ref<TemplateItem[]>([])
|
||||
const form = ref({ name: '', course: '' })
|
||||
|
||||
watch(() => appStore.isTemplateDialogOpen, (val) => {
|
||||
@@ -63,17 +68,46 @@ async function saveTemplate() {
|
||||
return
|
||||
}
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.template.save(form.value)
|
||||
ElMessage.success('模板已保存')
|
||||
const currentRubric = await window.electronAPI.rubric.getData()
|
||||
const questions = currentRubric?.questions?.length ? JSON.parse(JSON.stringify(currentRubric.questions)) : []
|
||||
if (!questions.length) {
|
||||
ElMessage.warning('当前没有评分标准数据,模板将保存为空。请先在评分标准编辑器中添加题目并点击「应用」')
|
||||
return
|
||||
}
|
||||
await window.electronAPI.template.save({
|
||||
name: form.value.name,
|
||||
course: form.value.course,
|
||||
questions
|
||||
})
|
||||
ElMessage.success(`模板已保存(${questions.length} 题)`)
|
||||
form.value = { name: '', course: '' }
|
||||
loadTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
function selectTemplate(tpl: any) {
|
||||
gradingStore.setRubric(`考试: ${tpl.name}\n课程: ${tpl.course}\n请根据标准答案评分。`)
|
||||
ElMessage.success('已选择模板:' + tpl.name)
|
||||
handleClose()
|
||||
async function selectTemplate(tpl: TemplateItem) {
|
||||
if (!tpl.questions?.length) {
|
||||
ElMessage.warning('该模板没有题目数据')
|
||||
return
|
||||
}
|
||||
const ok = await window.electronAPI?.template.apply(tpl.id)
|
||||
if (ok) {
|
||||
gradingStore.setQuestions(JSON.parse(JSON.stringify(tpl.questions)))
|
||||
ElMessage.success(`已应用模板「${tpl.name}」(${tpl.questions.length} 题)`)
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error('应用模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(tpl: TemplateItem) {
|
||||
if (window.electronAPI) {
|
||||
const ok = await window.electronAPI.template.delete(tpl.id)
|
||||
if (ok) {
|
||||
ElMessage.success('已删除')
|
||||
loadTemplates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
|
||||
+12
-2
@@ -52,6 +52,14 @@ export interface HistoryRecord {
|
||||
reference_answer_image?: string
|
||||
}
|
||||
|
||||
export interface TemplateItem {
|
||||
id: number
|
||||
name: string
|
||||
course: string
|
||||
questions: QuestionItem[]
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
window: {
|
||||
dragMove: (deltaX: number, deltaY: number) => Promise<void>
|
||||
@@ -109,8 +117,10 @@ export interface ElectronAPI {
|
||||
resetNextIndex: () => Promise<void>
|
||||
}
|
||||
template: {
|
||||
list: () => Promise<unknown[]>
|
||||
save: (data: { name: string; course: string }) => Promise<number | null>
|
||||
list: () => Promise<TemplateItem[]>
|
||||
save: (data: { name: string; course: string; questions?: QuestionItem[] }) => Promise<number | null>
|
||||
delete: (id: number) => Promise<boolean>
|
||||
apply: (id: number) => Promise<boolean>
|
||||
}
|
||||
dialog: {
|
||||
open: (type: string, data?: any) => Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user