feat: 模板导出导入 + 模板应用自动刷新主窗口 + 窗口放大 + 状态栏可拖拽
This commit is contained in:
+55
-2
@@ -111,7 +111,7 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
|
||||
ipcMain.handle('window:resizeMain', (_event, width: number) => {
|
||||
const [wx, wy] = win.getPosition()
|
||||
win.setBounds({ x: wx, y: wy, width: Math.max(200, Math.min(width, 1200)), height: 64 })
|
||||
win.setBounds({ x: wx, y: wy, width: Math.max(200, Math.min(width, 1200)), height: 76 })
|
||||
})
|
||||
|
||||
ipcMain.handle('screenshot:start', () => {
|
||||
@@ -163,7 +163,8 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
return result
|
||||
} catch (err: any) {
|
||||
log.error('AI grade failed:', err)
|
||||
const apiMsg = err?.response?.data?.error?.message || ''
|
||||
const res = err?.response?.data
|
||||
const apiMsg = res?.error?.message || res?.error || res?.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 等)')
|
||||
@@ -277,6 +278,56 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('template:exportJson', async () => {
|
||||
try {
|
||||
const d = getDb()
|
||||
const rows: any[] = d.prepare('SELECT * FROM exam ORDER BY created_at DESC').all()
|
||||
if (!rows.length) throw new Error('暂无模板可导出')
|
||||
const templates = rows.map(r => ({
|
||||
name: r.name,
|
||||
course: r.course,
|
||||
questions: r.questions_json ? JSON.parse(r.questions_json) : [],
|
||||
created_at: r.created_at
|
||||
}))
|
||||
const result = await dialogBox.showSaveDialog({
|
||||
defaultPath: '评分模板.json',
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
})
|
||||
if (result.canceled || !result.filePath) return false
|
||||
fs.writeFileSync(result.filePath, JSON.stringify(templates, null, 2), 'utf-8')
|
||||
return true
|
||||
} catch (err) {
|
||||
log.error('template:exportJson error:', err)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('template:importJson', async () => {
|
||||
try {
|
||||
const result = await dialogBox.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
})
|
||||
if (result.canceled || result.filePaths.length === 0) return null
|
||||
const content = fs.readFileSync(result.filePaths[0], 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
if (!Array.isArray(data)) throw new Error('JSON 格式错误:应为数组')
|
||||
const db = getDb()
|
||||
const insert = db.prepare('INSERT INTO exam (name, course, questions_json) VALUES (?, ?, ?)')
|
||||
let count = 0
|
||||
for (const tpl of data) {
|
||||
if (tpl.name && tpl.course) {
|
||||
insert.run(tpl.name, tpl.course, JSON.stringify(tpl.questions || []))
|
||||
count++
|
||||
}
|
||||
}
|
||||
return { count }
|
||||
} catch (err) {
|
||||
log.error('template:importJson error:', err)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('template:save', (_event, data: { name: string; course: string; questions?: any[] }) => {
|
||||
try {
|
||||
const db = getDb()
|
||||
@@ -315,6 +366,8 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
insert.run(q.questionTitle || '', q.maxScore ?? 100, q.referenceAnswer || '', q.rubric || '', i, q.referenceAnswerImage || '')
|
||||
}
|
||||
rubricNextIndex = 0
|
||||
const mainWin = getMainWindow()
|
||||
if (mainWin) mainWin.webContents.send('rubric:updated')
|
||||
return true
|
||||
} catch (err) {
|
||||
log.error('template:apply error:', err)
|
||||
|
||||
@@ -62,11 +62,11 @@ export function openDialogWindow(type: string): BrowserWindow | null {
|
||||
}
|
||||
|
||||
const sizes: Record<string, { width: number; height: number }> = {
|
||||
settings: { width: 520, height: 560 },
|
||||
history: { width: 800, height: 600 },
|
||||
templates: { width: 640, height: 520 },
|
||||
rubric: { width: 640, height: 700 },
|
||||
result: { width: 520, height: 700 }
|
||||
settings: { width: 640, height: 680 },
|
||||
history: { width: 1000, height: 750 },
|
||||
templates: { width: 720, height: 600 },
|
||||
rubric: { width: 750, height: 800 },
|
||||
result: { width: 600, height: 780 }
|
||||
}
|
||||
const size = sizes[type] || { width: 500, height: 500 }
|
||||
|
||||
|
||||
@@ -57,14 +57,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
importJson: () => ipcRenderer.invoke('rubric:importJson'),
|
||||
getNextIndex: () => ipcRenderer.invoke('rubric:getNextIndex'),
|
||||
setNextIndex: (idx: number) => ipcRenderer.invoke('rubric:setNextIndex', idx),
|
||||
resetNextIndex: () => ipcRenderer.invoke('rubric:resetNextIndex')
|
||||
resetNextIndex: () => ipcRenderer.invoke('rubric:resetNextIndex'),
|
||||
onUpdated: (callback: () => void) => {
|
||||
const handler = () => callback()
|
||||
ipcRenderer.on('rubric:updated', handler)
|
||||
return () => ipcRenderer.removeListener('rubric:updated', handler)
|
||||
}
|
||||
},
|
||||
|
||||
template: {
|
||||
list: () => ipcRenderer.invoke('template:list'),
|
||||
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)
|
||||
apply: (id: number) => ipcRenderer.invoke('template:apply', id),
|
||||
exportJson: () => ipcRenderer.invoke('template:exportJson'),
|
||||
importJson: () => ipcRenderer.invoke('template:importJson')
|
||||
},
|
||||
|
||||
dialog: {
|
||||
|
||||
@@ -41,11 +41,12 @@
|
||||
<span class="btn-label">设置</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="status-bar">{{ statusText }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
@@ -59,6 +60,12 @@ const settingsStore = useSettingsStore()
|
||||
const showMenu = ref(false)
|
||||
const toolbarRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const statusText = computed(() => {
|
||||
const questions = gradingStore.questions
|
||||
const idx = gradingStore.currentQuestionIndex
|
||||
return questions?.length ? `第 ${idx + 1}/${questions.length} 题` : ''
|
||||
})
|
||||
|
||||
async function resizeWindow() {
|
||||
await nextTick()
|
||||
if (toolbarRef.value && window.electronAPI) {
|
||||
@@ -70,6 +77,7 @@ async function resizeWindow() {
|
||||
watch(showMenu, resizeWindow)
|
||||
|
||||
let cleanupScreenshotListener: (() => void) | null = null
|
||||
let cleanupRubricListener: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
resizeWindow()
|
||||
@@ -84,11 +92,19 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
})
|
||||
cleanupRubricListener = window.electronAPI.rubric.onUpdated(async () => {
|
||||
const data = await window.electronAPI!.rubric.getData()
|
||||
if (data?.questions?.length) {
|
||||
gradingStore.setQuestions(JSON.parse(JSON.stringify(data.questions)))
|
||||
ElMessage.success(`已应用模板(${data.questions.length} 题)`)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupScreenshotListener?.()
|
||||
cleanupRubricListener?.()
|
||||
})
|
||||
|
||||
function startScreenshot() {
|
||||
@@ -197,7 +213,6 @@ function openSettings() { window.electronAPI?.dialog.open('settings') }
|
||||
left: 0;
|
||||
width: fit-content;
|
||||
min-width: 200px;
|
||||
height: 64px;
|
||||
z-index: 99999;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
@@ -211,10 +226,18 @@ function openSettings() { window.electronAPI?.dialog.open('settings') }
|
||||
.toolbar-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
padding: 8px 14px 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
padding: 0 14px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sub-btn {
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
<template>
|
||||
<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="考试名称">
|
||||
<el-input v-model="form.name" placeholder="如:期末考试" />
|
||||
</el-form-item>
|
||||
<el-form-item label="课程">
|
||||
<el-input v-model="form.course" placeholder="如:Java" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="small" @click="saveTemplate">保存模板</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-dialog v-model="visible" title="评分模板管理" width="680px" :modal="false" @close="handleClose">
|
||||
<el-table :data="templates" size="small" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="50" />
|
||||
<el-table-column prop="name" label="考试名称" min-width="130" />
|
||||
@@ -32,6 +16,11 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top: 12px; display: flex; gap: 8px;">
|
||||
<el-button size="small" @click="exportTemplates">导出</el-button>
|
||||
<el-button size="small" @click="importTemplates">导入</el-button>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button size="small" @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
@@ -42,14 +31,11 @@
|
||||
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'
|
||||
import type { TemplateItem } from '@/types'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gradingStore = useGradingStore()
|
||||
const visible = ref(false)
|
||||
const templates = ref<TemplateItem[]>([])
|
||||
const form = ref({ name: '', course: '' })
|
||||
|
||||
watch(() => appStore.isTemplateDialogOpen, (val) => {
|
||||
visible.value = val
|
||||
@@ -62,29 +48,6 @@ async function loadTemplates() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!form.value.name || !form.value.course) {
|
||||
ElMessage.warning('请填写考试名称和课程')
|
||||
return
|
||||
}
|
||||
if (window.electronAPI) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTemplate(tpl: TemplateItem) {
|
||||
if (!tpl.questions?.length) {
|
||||
ElMessage.warning('该模板没有题目数据')
|
||||
@@ -92,14 +55,29 @@ async function selectTemplate(tpl: TemplateItem) {
|
||||
}
|
||||
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 exportTemplates() {
|
||||
const ok = await window.electronAPI?.template.exportJson()
|
||||
if (ok) {
|
||||
ElMessage.success('模板已导出')
|
||||
} else if (ok === false) {
|
||||
ElMessage.warning('导出失败或已取消')
|
||||
}
|
||||
}
|
||||
|
||||
async function importTemplates() {
|
||||
const result = await window.electronAPI?.template.importJson()
|
||||
if (result) {
|
||||
ElMessage.success(`已导入 ${result.count} 个模板`)
|
||||
loadTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(tpl: TemplateItem) {
|
||||
if (window.electronAPI) {
|
||||
const ok = await window.electronAPI.template.delete(tpl.id)
|
||||
@@ -116,7 +94,4 @@ function handleClose() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-section {
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -115,12 +115,15 @@ export interface ElectronAPI {
|
||||
getNextIndex: () => Promise<number>
|
||||
setNextIndex: (idx: number) => Promise<void>
|
||||
resetNextIndex: () => Promise<void>
|
||||
onUpdated: (callback: () => void) => () => void
|
||||
}
|
||||
template: {
|
||||
list: () => Promise<TemplateItem[]>
|
||||
save: (data: { name: string; course: string; questions?: QuestionItem[] }) => Promise<number | null>
|
||||
delete: (id: number) => Promise<boolean>
|
||||
apply: (id: number) => Promise<boolean>
|
||||
exportJson: () => Promise<boolean>
|
||||
importJson: () => Promise<{ count: number } | null>
|
||||
}
|
||||
dialog: {
|
||||
open: (type: string, data?: any) => Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user