init: AI Grading Assistant MVP

This commit is contained in:
2026-07-15 21:54:21 +08:00
commit 3b1dc16489
34 changed files with 10266 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
<template>
<div class="floating-toolbar" :class="{ 'is-hidden': !appStore.isToolbarVisible }">
<div class="toolbar-buttons">
<el-button class="toolbar-btn" @click="startScreenshot">
<span class="btn-icon">📷</span>
<span class="btn-label">截图</span>
</el-button>
<el-button class="toolbar-btn" @click="selectImage">
<span class="btn-icon">📁</span>
<span class="btn-label">图片</span>
</el-button>
<el-button class="toolbar-btn" @click="startGrading" :loading="appStore.isGrading">
<span class="btn-icon"></span>
<span class="btn-label">{{ appStore.isGrading ? '阅卷中...' : '阅卷' }}</span>
</el-button>
<el-button class="toolbar-btn" @click="openRubric">
<span class="btn-icon">📋</span>
<span class="btn-label">标准</span>
</el-button>
<el-button class="toolbar-btn" @click="openTemplates">
<span class="btn-icon">📚</span>
<span class="btn-label">模板</span>
</el-button>
<el-button class="toolbar-btn" @click="openHistory">
<span class="btn-icon">📜</span>
<span class="btn-label">历史</span>
</el-button>
<el-button class="toolbar-btn" @click="openSettings">
<span class="btn-icon"></span>
<span class="btn-label">设置</span>
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useAppStore } from '@/stores/app'
import { useGradingStore } from '@/stores/grading'
import { useSettingsStore } from '@/stores/settings'
const appStore = useAppStore()
const gradingStore = useGradingStore()
const settingsStore = useSettingsStore()
let cleanupScreenshotListener: (() => void) | null = null
onMounted(() => {
if (window.electronAPI) {
cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted((base64: string) => {
if (!appStore.isGrading) {
gradingStore.setImage(base64)
startGrading()
}
})
}
})
onUnmounted(() => {
cleanupScreenshotListener?.()
})
function startScreenshot() {
if (window.electronAPI) window.electronAPI.screenshot.start()
}
async function selectImage() {
if (!window.electronAPI) return
const data = await window.electronAPI.dialog.openImage()
if (data) {
gradingStore.setImage(data.base64)
ElMessage.success('已选择图片')
}
}
async function startGrading() {
if (!gradingStore.currentImage) {
ElMessage.info('请先截图或选择图片')
return
}
if (!settingsStore.settings.apiKey) {
ElMessage.warning('请先在设置中配置 API Key')
if (window.electronAPI) window.electronAPI.dialog.open('settings')
return
}
appStore.isGrading = true
try {
// load latest rubric data from main process (may have been updated by rubric dialog)
const rubricData = await window.electronAPI?.rubric.getData()
if (rubricData) {
if (rubricData.rubric) gradingStore.setRubric(rubricData.rubric)
if (rubricData.questionTitle) gradingStore.setQuestionTitle(rubricData.questionTitle)
if (rubricData.maxScore) gradingStore.setMaxScore(rubricData.maxScore)
if (rubricData.referenceAnswer) gradingStore.setReferenceAnswer(rubricData.referenceAnswer)
}
const result = await window.electronAPI.ai.grade({
imageBase64: gradingStore.currentImage,
rubric: gradingStore.rubric || `请根据答案内容评分,满分${gradingStore.maxScore}`,
maxScore: gradingStore.maxScore,
referenceAnswer: gradingStore.referenceAnswer,
questionTitle: gradingStore.questionTitle,
apiConfig: {
apiKey: settingsStore.settings.apiKey,
baseUrl: settingsStore.settings.baseUrl,
model: settingsStore.settings.model
}
})
gradingStore.setResult(result)
if (window.electronAPI) {
await window.electronAPI.grading.storeData({
result,
image: gradingStore.currentImage,
rubric: gradingStore.rubric,
questionTitle: gradingStore.questionTitle,
maxScore: gradingStore.maxScore,
referenceAnswer: gradingStore.referenceAnswer
})
// also update rubric bridge
await window.electronAPI.rubric.storeData({
questionTitle: gradingStore.questionTitle,
maxScore: gradingStore.maxScore,
referenceAnswer: gradingStore.referenceAnswer,
rubric: gradingStore.rubric
})
await window.electronAPI.dialog.open('result')
}
} catch (err) {
ElMessage.error('AI 阅卷失败:' + (err as Error).message)
} finally {
appStore.isGrading = false
}
}
function openRubric() {
window.electronAPI?.dialog.open('rubric')
}
async function openTemplates() { window.electronAPI?.dialog.open('templates') }
function openHistory() { window.electronAPI?.dialog.open('history') }
function openSettings() { window.electronAPI?.dialog.open('settings') }
</script>
<style scoped>
.floating-toolbar {
position: fixed;
inset: 0;
z-index: 99999;
user-select: none;
-webkit-app-region: drag;
background: rgba(30, 30, 30, 0.85);
backdrop-filter: blur(12px);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.toolbar-buttons {
display: flex;
gap: 6px;
padding: 8px 14px;
justify-content: center;
align-items: center;
height: 100%;
}
.toolbar-btn {
-webkit-app-region: no-drag;
border: none !important;
background: transparent !important;
color: rgba(255, 255, 255, 0.85) !important;
height: 46px !important;
padding: 4px 14px !important;
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
transition: all 0.2s;
cursor: pointer;
}
.toolbar-btn:hover {
background: rgba(255, 255, 255, 0.12) !important;
color: #fff !important;
}
.btn-icon {
font-size: 16px;
line-height: 1;
}
.btn-label {
font-size: 10px;
line-height: 1;
white-space: nowrap;
}
.is-hidden {
opacity: 0;
pointer-events: none;
}
</style>