216 lines
6.0 KiB
Vue
216 lines
6.0 KiB
Vue
<template>
|
|
<div class="floating-toolbar" :class="{ 'is-hidden': !appStore.isToolbarVisible }">
|
|
<div class="toolbar-buttons">
|
|
<el-button class="toolbar-btn" @click="startScreenshot" :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-dropdown trigger="click" @command="handleMenu">
|
|
<el-button class="toolbar-btn">
|
|
<span class="btn-icon">📎</span>
|
|
<span class="btn-label">其它</span>
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="image">📁 图片阅卷</el-dropdown-item>
|
|
<el-dropdown-item command="templates">📚 模板</el-dropdown-item>
|
|
<el-dropdown-item command="history">📜 历史</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
|
|
<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)
|
|
startGrading()
|
|
}
|
|
}
|
|
|
|
async function handleMenu(cmd: string) {
|
|
if (cmd === 'image') await selectImage()
|
|
else if (cmd === 'templates') window.electronAPI?.dialog.open('templates')
|
|
else if (cmd === 'history') window.electronAPI?.dialog.open('history')
|
|
}
|
|
|
|
async function gradeQuestion(questionIdx: number) {
|
|
const questions = gradingStore.questions
|
|
if (!questions.length) {
|
|
ElMessage.warning('请先在评分标准中添加题目')
|
|
return null
|
|
}
|
|
const q = questions[Math.min(questionIdx, questions.length - 1)]
|
|
const result = await window.electronAPI.ai.grade({
|
|
imageBase64: gradingStore.currentImage,
|
|
rubric: q.rubric || `请根据答案内容评分,满分${q.maxScore}分`,
|
|
maxScore: q.maxScore,
|
|
referenceAnswer: q.referenceAnswer,
|
|
questionTitle: q.questionTitle,
|
|
apiConfig: {
|
|
apiKey: settingsStore.settings.apiKey,
|
|
baseUrl: settingsStore.settings.baseUrl,
|
|
model: settingsStore.settings.model
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// load rubric data
|
|
const rubricData = await window.electronAPI?.rubric.getData()
|
|
const questions = rubricData?.questions?.length
|
|
? JSON.parse(JSON.stringify(rubricData.questions))
|
|
: [{ questionTitle: '', maxScore: 100, referenceAnswer: '', rubric: '' }]
|
|
const nextIdx = Math.min(await window.electronAPI?.rubric.getNextIndex() ?? 0, questions.length - 1)
|
|
gradingStore.setQuestions(questions)
|
|
gradingStore.setCurrentQuestionIndex(nextIdx)
|
|
|
|
appStore.isGrading = true
|
|
try {
|
|
const result = await gradeQuestion(nextIdx)
|
|
if (!result) return
|
|
|
|
gradingStore.setResult(result)
|
|
if (window.electronAPI) {
|
|
// store grading data for result window
|
|
await window.electronAPI.grading.storeData({
|
|
result,
|
|
image: gradingStore.currentImage,
|
|
questions,
|
|
currentQuestionIndex: nextIdx
|
|
})
|
|
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>
|