init: AI Grading Assistant MVP
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<template v-if="dialogType">
|
||||
<SettingDialog v-if="dialogType === 'settings'" />
|
||||
<HistoryDialog v-if="dialogType === 'history'" />
|
||||
<TemplateDialog v-if="dialogType === 'templates'" />
|
||||
<RubricEditor v-if="dialogType === 'rubric'" />
|
||||
<ResultDrawer v-if="dialogType === 'result'" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-view />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
import ResultDrawer from '@/components/ResultDrawer.vue'
|
||||
import SettingDialog from '@/components/SettingDialog.vue'
|
||||
import HistoryDialog from '@/pages/HistoryDialog.vue'
|
||||
import TemplateDialog from '@/pages/TemplateDialog.vue'
|
||||
import RubricEditor from '@/components/RubricEditor.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const gradingStore = useGradingStore()
|
||||
|
||||
const dialogType = new URLSearchParams(window.location.search).get('dialog')
|
||||
|
||||
onMounted(async () => {
|
||||
if (dialogType === 'result') {
|
||||
const data = await window.electronAPI?.grading.getData()
|
||||
if (data) {
|
||||
if (data.image) gradingStore.setImage(data.image)
|
||||
if (data.rubric) gradingStore.setRubric(data.rubric)
|
||||
if (data.result) gradingStore.setResult(data.result)
|
||||
if (data.questionTitle) gradingStore.setQuestionTitle(data.questionTitle)
|
||||
if (data.maxScore) gradingStore.setMaxScore(data.maxScore)
|
||||
if (data.referenceAnswer) gradingStore.setReferenceAnswer(data.referenceAnswer)
|
||||
}
|
||||
appStore.openResultDrawer()
|
||||
} else if (dialogType) {
|
||||
if (dialogType === 'settings') await settingsStore.loadSettings()
|
||||
const actions: Record<string, () => void> = {
|
||||
settings: () => appStore.openSettingDialog(),
|
||||
history: () => appStore.openHistoryDialog(),
|
||||
templates: () => appStore.openTemplateDialog(),
|
||||
rubric: () => appStore.openRubricEditor()
|
||||
}
|
||||
actions[dialogType]?.()
|
||||
} else {
|
||||
settingsStore.loadSettings()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 483 B |
@@ -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>
|
||||
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<div class="result-drawer" v-show="visible" @click.self="closeDrawer">
|
||||
<div class="drawer-panel">
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">AI 评分结果</span>
|
||||
<el-button text size="small" @click="closeDrawer">✕</el-button>
|
||||
</div>
|
||||
|
||||
<div class="drawer-body" v-if="gradingStore.currentResult">
|
||||
<div class="result-score">
|
||||
<div class="score-value">{{ gradingStore.currentResult.score }}</div>
|
||||
<div class="score-label">建议分数</div>
|
||||
</div>
|
||||
|
||||
<div class="result-confidence">
|
||||
<div class="confidence-bar">
|
||||
<div class="confidence-fill" :style="{ width: (gradingStore.currentResult.confidence * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="confidence-text">可信度:{{ Math.round(gradingStore.currentResult.confidence * 100) }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section" v-if="gradingStore.currentResult.deductions.length">
|
||||
<div class="section-title">扣分项</div>
|
||||
<div class="deduction-item" v-for="(item, idx) in gradingStore.currentResult.deductions" :key="idx">
|
||||
{{ idx + 1 }}. {{ item }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">评语</div>
|
||||
<div class="comment-text">{{ gradingStore.currentResult.comment }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">教师评分(可修改)</div>
|
||||
<div class="teacher-score-input">
|
||||
<el-input-number
|
||||
v-model="teacherScore"
|
||||
:min="0"
|
||||
:max="100"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-title">教师评语</div>
|
||||
<el-input
|
||||
v-model="teacherComment"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
size="small"
|
||||
placeholder="输入评语(可选)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button size="small" @click="reGrade">重新评分</el-button>
|
||||
<el-button type="primary" size="small" @click="confirmScore">确认评分</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gradingStore = useGradingStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const teacherScore = ref(0)
|
||||
const teacherComment = ref('')
|
||||
|
||||
watch(() => appStore.isResultDrawerOpen, (val) => {
|
||||
visible.value = val
|
||||
})
|
||||
|
||||
watch(() => gradingStore.currentResult, (val) => {
|
||||
if (val) {
|
||||
teacherScore.value = val.score
|
||||
teacherComment.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
function closeDrawer() {
|
||||
window.electronAPI?.dialog.close()
|
||||
}
|
||||
|
||||
async function reGrade() {
|
||||
if (!gradingStore.currentImage) return
|
||||
appStore.isGrading = true
|
||||
try {
|
||||
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)
|
||||
ElMessage.success('重新评分完成')
|
||||
} catch (err) {
|
||||
ElMessage.error('重新评分失败:' + (err as Error).message)
|
||||
} finally {
|
||||
appStore.isGrading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmScore() {
|
||||
const result = gradingStore.currentResult
|
||||
if (!result) return
|
||||
|
||||
try {
|
||||
await window.electronAPI.grading.submit({
|
||||
questionId: 0,
|
||||
imagePath: '',
|
||||
aiScore: result.score,
|
||||
teacherScore: teacherScore.value,
|
||||
confidence: result.confidence,
|
||||
studentAnswer: ''
|
||||
})
|
||||
ElMessage.success('评分已保存')
|
||||
gradingStore.reset()
|
||||
closeDrawer()
|
||||
} catch (err) {
|
||||
ElMessage.error('保存失败:' + (err as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-drawer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99998;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drawer-panel {
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.result-score {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #409EFF;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result-confidence {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.confidence-bar {
|
||||
height: 6px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confidence-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #67C23A, #409EFF);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.confidence-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.deduction-item {
|
||||
font-size: 13px;
|
||||
color: #F56C6C;
|
||||
padding: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
background: #f5f7fa;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.teacher-score-input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="评分标准" width="520px" :modal="false" @close="handleClose">
|
||||
<el-form :model="form" size="small">
|
||||
<el-form-item label="题目">
|
||||
<el-input
|
||||
v-model="form.questionTitle"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="输入题目内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="满分">
|
||||
<el-input-number v-model="form.maxScore" :min="1" :max="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参考答案">
|
||||
<el-input
|
||||
v-model="form.referenceAnswer"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入参考答案(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="评分标准">
|
||||
<el-input
|
||||
v-model="form.rubric"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="输入评分标准,如: 实体识别完整(3分) 联系类型正确(4分) 图形规范(2分)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="handleSave">应用</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gradingStore = useGradingStore()
|
||||
const visible = ref(false)
|
||||
const form = ref({ questionTitle: '', maxScore: 100, referenceAnswer: '', rubric: '' })
|
||||
|
||||
watch(() => appStore.isRubricEditorOpen, async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
// load rubric data from main process (set by toolbar window before opening)
|
||||
const data = await window.electronAPI?.rubric.getData()
|
||||
if (data) {
|
||||
form.value = {
|
||||
questionTitle: data.questionTitle || '',
|
||||
maxScore: data.maxScore ?? 100,
|
||||
referenceAnswer: data.referenceAnswer || '',
|
||||
rubric: data.rubric || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
// update local store
|
||||
gradingStore.setQuestionTitle(form.value.questionTitle)
|
||||
gradingStore.setRubric(form.value.rubric)
|
||||
gradingStore.setMaxScore(form.value.maxScore)
|
||||
gradingStore.setReferenceAnswer(form.value.referenceAnswer)
|
||||
// persist to main process so toolbar window can read it
|
||||
await window.electronAPI?.rubric.storeData({
|
||||
questionTitle: form.value.questionTitle,
|
||||
maxScore: form.value.maxScore,
|
||||
referenceAnswer: form.value.referenceAnswer,
|
||||
rubric: form.value.rubric
|
||||
})
|
||||
ElMessage.success('评分标准已应用')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
window.electronAPI?.dialog.close()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div
|
||||
class="screenshot-overlay"
|
||||
@mousedown="onMouseDown"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseup="onMouseUp"
|
||||
:class="{ 'has-selection': hasSelection }"
|
||||
>
|
||||
<canvas ref="canvasRef" class="screenshot-canvas"></canvas>
|
||||
<div v-if="!hasSelection" class="screenshot-tip">拖动选择截图区域 · Esc取消</div>
|
||||
<div v-else class="screenshot-actions" @mousedown.stop @mouseup.stop>
|
||||
<button class="action-btn confirm" @click="confirmCapture">✓ 确认</button>
|
||||
<button class="action-btn cancel" @click="cancel">✕ 取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const state = {
|
||||
isSelecting: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
endX: 0,
|
||||
endY: 0,
|
||||
}
|
||||
const hasSelection = ref(false)
|
||||
const isCapturing = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
})
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
hasSelection.value = false
|
||||
state.isSelecting = true
|
||||
state.startX = e.clientX
|
||||
state.startY = e.clientY
|
||||
state.endX = e.clientX
|
||||
state.endY = e.clientY
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!state.isSelecting) return
|
||||
state.endX = e.clientX
|
||||
state.endY = e.clientY
|
||||
drawOverlay()
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (!state.isSelecting) return
|
||||
state.isSelecting = false
|
||||
|
||||
const w = Math.abs(state.endX - state.startX)
|
||||
const h = Math.abs(state.endY - state.startY)
|
||||
if (w < 10 || h < 10) return
|
||||
|
||||
hasSelection.value = true
|
||||
drawOverlay()
|
||||
}
|
||||
|
||||
function confirmCapture() {
|
||||
const x = Math.min(state.startX, state.endX)
|
||||
const y = Math.min(state.startY, state.endY)
|
||||
const width = Math.abs(state.endX - state.startX)
|
||||
const height = Math.abs(state.endY - state.startY)
|
||||
|
||||
if (width < 10 || height < 10) return
|
||||
if (isCapturing.value) return
|
||||
isCapturing.value = true
|
||||
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.screenshot.capture({ x, y, width, height })
|
||||
.catch(() => {})
|
||||
}
|
||||
try { window.close() } catch {}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.screenshot.cancel()
|
||||
.catch(() => {})
|
||||
}
|
||||
isCapturing.value = false
|
||||
try { window.close() } catch {}
|
||||
}
|
||||
|
||||
function drawOverlay() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const x = Math.min(state.startX, state.endX)
|
||||
const y = Math.min(state.startY, state.endY)
|
||||
const w = Math.abs(state.endX - state.startX)
|
||||
const h = Math.abs(state.endY - state.startY)
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.clearRect(x, y, w, h)
|
||||
|
||||
ctx.strokeStyle = '#409EFF'
|
||||
ctx.lineWidth = 2.5
|
||||
ctx.strokeRect(x, y, w, h)
|
||||
|
||||
ctx.fillStyle = 'rgba(64, 158, 255, 0.08)'
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText(`${w} × ${h}`, x + 6, y - 8)
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
cancel()
|
||||
}
|
||||
if (e.key === 'Enter' && hasSelection.value) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
confirmCapture()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.screenshot-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.screenshot-overlay.has-selection {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.screenshot-canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.screenshot-tip {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screenshot-actions {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.action-btn.confirm {
|
||||
background: #409EFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.confirm:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
.action-btn.cancel {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn.cancel:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="设置" width="480px" :modal="false" :close-on-click-modal="false" @close="handleClose">
|
||||
<el-form :model="form" label-width="100px" size="small">
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="form.apiKey" type="password" show-password placeholder="请输入 API Key" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="form.baseUrl" placeholder="https://api.openai.com/v1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型">
|
||||
<el-select v-model="form.model" style="width: 100%">
|
||||
<el-option label="Qwen/Qwen3.6-35B-A3B" value="Qwen/Qwen3.6-35B-A3B" />
|
||||
<el-option label="Qwen/Qwen2.5-32B-Instruct" value="Qwen/Qwen2.5-32B-Instruct" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="快捷键">
|
||||
<el-input v-model="form.shortcut" placeholder="Alt+Q" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="超时(秒)">
|
||||
<el-input-number v-model="form.timeout" :min="5" :max="300" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="主题">
|
||||
<el-select v-model="form.theme" style="width: 100%">
|
||||
<el-option label="浅色" value="light" />
|
||||
<el-option label="深色" value="dark" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="字体大小">
|
||||
<el-input-number v-model="form.fontSize" :min="12" :max="20" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="代理">
|
||||
<el-input v-model="form.proxy" placeholder="可选,如 http://127.0.0.1:7890" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="截图保存路径">
|
||||
<div style="display:flex;gap:8px;width:100%">
|
||||
<el-input v-model="form.screenshotSavePath" placeholder="留空不自动保存截图" />
|
||||
<el-button @click="selectDir">选择</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button size="small" @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
watch(() => appStore.isSettingDialogOpen, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
form.value = { ...settingsStore.settings }
|
||||
}
|
||||
})
|
||||
|
||||
const form = ref({ ...settingsStore.settings })
|
||||
|
||||
function handleClose() {
|
||||
window.electronAPI?.dialog.close()
|
||||
}
|
||||
|
||||
async function selectDir() {
|
||||
if (window.electronAPI) {
|
||||
const dir = await window.electronAPI.settings.selectDirectory()
|
||||
if (dir) form.value.screenshotSavePath = dir
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await settingsStore.saveSettings(JSON.parse(JSON.stringify(form.value)))
|
||||
window.electronAPI?.dialog.close()
|
||||
} catch (err) {
|
||||
ElMessage.error('保存失败:' + (err as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="历史记录" width="800px" fullscreen :modal="false" @close="handleClose">
|
||||
<el-table :data="records" style="width: 100%" size="small" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="ai_score" label="AI分数" width="80" />
|
||||
<el-table-column prop="teacher_score" label="教师分数" width="80" />
|
||||
<el-table-column prop="confidence" label="可信度" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ Math.round(row.confidence * 100) }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="student_answer" label="答案摘要" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="时间" width="160" />
|
||||
<el-table-column label="操作" width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text size="small" type="danger" @click="deleteRecord(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button size="small" @click="handleClose">关闭</el-button>
|
||||
<el-button size="small" @click="refresh">刷新</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import type { HistoryRecord } from '@/types'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const records = ref<HistoryRecord[]>([])
|
||||
|
||||
watch(() => appStore.isHistoryDialogOpen, (val) => {
|
||||
visible.value = val
|
||||
if (val) refresh()
|
||||
})
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (window.electronAPI) {
|
||||
records.value = await window.electronAPI.history.list()
|
||||
}
|
||||
} catch {
|
||||
records.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该记录?', '提示')
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.history.delete(id)
|
||||
ElMessage.success('已删除')
|
||||
refresh()
|
||||
}
|
||||
} catch { /* cancelled */ }
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
window.electronAPI?.dialog.close()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="评分模板管理" width="600px" :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-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">
|
||||
<template #default="{ row }">
|
||||
<el-button text size="small" @click="selectTemplate(row)">使用</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button size="small" @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGradingStore } from '@/stores/grading'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const gradingStore = useGradingStore()
|
||||
const visible = ref(false)
|
||||
const templates = ref<any[]>([])
|
||||
const form = ref({ name: '', course: '' })
|
||||
|
||||
watch(() => appStore.isTemplateDialogOpen, (val) => {
|
||||
visible.value = val
|
||||
if (val) loadTemplates()
|
||||
})
|
||||
|
||||
async function loadTemplates() {
|
||||
if (window.electronAPI) {
|
||||
templates.value = await window.electronAPI.template.list()
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!form.value.name || !form.value.course) {
|
||||
ElMessage.warning('请填写考试名称和课程')
|
||||
return
|
||||
}
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.template.save(form.value)
|
||||
ElMessage.success('模板已保存')
|
||||
form.value = { name: '', course: '' }
|
||||
loadTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
function selectTemplate(tpl: any) {
|
||||
gradingStore.setRubric(`考试: ${tpl.name}\n课程: ${tpl.course}\n请根据标准答案评分。`)
|
||||
ElMessage.success('已选择模板:' + tpl.name)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
window.electronAPI?.dialog.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-section {
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import FloatingToolbar from '@/components/FloatingToolbar.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'toolbar',
|
||||
component: FloatingToolbar
|
||||
},
|
||||
{
|
||||
path: '/screenshot',
|
||||
name: 'screenshot',
|
||||
component: () => import('@/components/ScreenshotOverlay.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,52 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const isToolbarVisible = ref(true)
|
||||
const isResultDrawerOpen = ref(false)
|
||||
const isSettingDialogOpen = ref(false)
|
||||
const isHistoryDialogOpen = ref(false)
|
||||
const isTemplateDialogOpen = ref(false)
|
||||
const isRubricEditorOpen = ref(false)
|
||||
const isGrading = ref(false)
|
||||
|
||||
function toggleToolbar() {
|
||||
isToolbarVisible.value = !isToolbarVisible.value
|
||||
}
|
||||
|
||||
function openResultDrawer() { isResultDrawerOpen.value = true }
|
||||
function closeResultDrawer() { isResultDrawerOpen.value = false }
|
||||
|
||||
function openSettingDialog() { isSettingDialogOpen.value = true }
|
||||
function closeSettingDialog() { isSettingDialogOpen.value = false }
|
||||
|
||||
function openHistoryDialog() { isHistoryDialogOpen.value = true }
|
||||
function closeHistoryDialog() { isHistoryDialogOpen.value = false }
|
||||
|
||||
function openTemplateDialog() { isTemplateDialogOpen.value = true }
|
||||
function closeTemplateDialog() { isTemplateDialogOpen.value = false }
|
||||
|
||||
function openRubricEditor() { isRubricEditorOpen.value = true }
|
||||
function closeRubricEditor() { isRubricEditorOpen.value = false }
|
||||
|
||||
return {
|
||||
isToolbarVisible,
|
||||
isResultDrawerOpen,
|
||||
isSettingDialogOpen,
|
||||
isHistoryDialogOpen,
|
||||
isTemplateDialogOpen,
|
||||
isRubricEditorOpen,
|
||||
isGrading,
|
||||
toggleToolbar,
|
||||
openResultDrawer,
|
||||
closeResultDrawer,
|
||||
openSettingDialog,
|
||||
closeSettingDialog,
|
||||
openHistoryDialog,
|
||||
closeHistoryDialog,
|
||||
openTemplateDialog,
|
||||
closeTemplateDialog,
|
||||
openRubricEditor,
|
||||
closeRubricEditor
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { GradeResult } from '@/types'
|
||||
|
||||
export const useGradingStore = defineStore('grading', () => {
|
||||
const currentImage = ref<string | null>(null)
|
||||
const currentResult = ref<GradeResult | null>(null)
|
||||
const rubric = ref('')
|
||||
const questionTitle = ref('')
|
||||
const maxScore = ref(100)
|
||||
const referenceAnswer = ref('')
|
||||
const teacherScore = ref<number | null>(null)
|
||||
const teacherComment = ref('')
|
||||
|
||||
function setImage(base64: string) {
|
||||
currentImage.value = base64
|
||||
}
|
||||
|
||||
function setResult(result: GradeResult) {
|
||||
currentResult.value = result
|
||||
}
|
||||
|
||||
function setRubric(text: string) {
|
||||
rubric.value = text
|
||||
}
|
||||
|
||||
function setQuestionTitle(text: string) { questionTitle.value = text }
|
||||
function setMaxScore(val: number) { maxScore.value = val }
|
||||
function setReferenceAnswer(val: string) { referenceAnswer.value = val }
|
||||
|
||||
function reset() {
|
||||
currentImage.value = null
|
||||
currentResult.value = null
|
||||
teacherScore.value = null
|
||||
teacherComment.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
currentImage,
|
||||
currentResult,
|
||||
rubric,
|
||||
questionTitle,
|
||||
maxScore,
|
||||
referenceAnswer,
|
||||
teacherScore,
|
||||
teacherComment,
|
||||
setImage,
|
||||
setResult,
|
||||
setRubric,
|
||||
setQuestionTitle,
|
||||
setMaxScore,
|
||||
setReferenceAnswer,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { AppSettings } from '@/types'
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const settings = ref<AppSettings>({
|
||||
apiKey: '',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
model: 'gpt-4o',
|
||||
shortcut: 'Alt+Q',
|
||||
theme: 'light',
|
||||
fontSize: 14,
|
||||
proxy: '',
|
||||
timeout: 120,
|
||||
screenshotSavePath: ''
|
||||
})
|
||||
|
||||
async function loadSettings() {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.settings.get()
|
||||
if (result) {
|
||||
settings.value = { ...settings.value, ...result }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(partial: Partial<AppSettings>) {
|
||||
Object.assign(settings.value, partial)
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.settings.set(partial)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
settings,
|
||||
loadSettings,
|
||||
saveSettings
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-app-region: no-drag;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
export interface GradeResult {
|
||||
score: number
|
||||
confidence: number
|
||||
deductions: string[]
|
||||
comment: string
|
||||
commit?: string
|
||||
}
|
||||
|
||||
export interface ApiConfig {
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
model: string
|
||||
shortcut: string
|
||||
theme: string
|
||||
fontSize: number
|
||||
proxy: string
|
||||
timeout: number
|
||||
screenshotSavePath: string
|
||||
}
|
||||
|
||||
export interface HistoryRecord {
|
||||
id: number
|
||||
question_id: number
|
||||
image_path: string
|
||||
student_answer: string
|
||||
ai_score: number
|
||||
teacher_score: number
|
||||
confidence: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ElectronAPI {
|
||||
window: {
|
||||
dragMove: (deltaX: number, deltaY: number) => Promise<void>
|
||||
dragEnd: (x: number, y: number) => Promise<void>
|
||||
}
|
||||
screenshot: {
|
||||
start: () => Promise<void>
|
||||
capture: (region: { x: number; y: number; width: number; height: number }) => Promise<string | null>
|
||||
cancel: () => Promise<void>
|
||||
getLastImage: () => Promise<string | null>
|
||||
onCompleted: (callback: (base64: string) => void) => () => void
|
||||
}
|
||||
ai: {
|
||||
grade: (data: { imageBase64: string; rubric: string; apiConfig: ApiConfig; maxScore?: number; referenceAnswer?: string; questionTitle?: string }) => Promise<GradeResult>
|
||||
}
|
||||
settings: {
|
||||
get: () => Promise<AppSettings>
|
||||
set: (settings: Record<string, unknown>) => Promise<boolean>
|
||||
selectDirectory: () => Promise<string | null>
|
||||
}
|
||||
history: {
|
||||
list: () => Promise<HistoryRecord[]>
|
||||
delete: (id: number) => Promise<boolean>
|
||||
}
|
||||
grading: {
|
||||
submit: (data: {
|
||||
questionId: number
|
||||
imagePath: string
|
||||
aiScore: number
|
||||
teacherScore: number
|
||||
confidence: number
|
||||
studentAnswer?: string
|
||||
}) => Promise<boolean>,
|
||||
storeData: (data: any) => Promise<void>,
|
||||
getData: () => Promise<any>
|
||||
}
|
||||
rubric: {
|
||||
storeData: (data: any) => Promise<void>
|
||||
getData: () => Promise<any>
|
||||
}
|
||||
template: {
|
||||
list: () => Promise<unknown[]>
|
||||
save: (data: { name: string; course: string }) => Promise<number | null>
|
||||
}
|
||||
dialog: {
|
||||
open: (type: string, data?: any) => Promise<void>
|
||||
close: () => void
|
||||
getPayload: (type: string) => Promise<any>
|
||||
openImage: () => Promise<{ base64: string; mime: string; filePath: string } | null>
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
Reference in New Issue
Block a user