Files
AI-Grading-Assistant/src/components/FloatingToolbar.vue
T

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>