diff --git a/src/components/marking/MarkingLayout.vue b/src/components/marking/MarkingLayout.vue index bf97fa1..a4f7885 100644 --- a/src/components/marking/MarkingLayout.vue +++ b/src/components/marking/MarkingLayout.vue @@ -117,183 +117,171 @@ const currentHistoryIndexInList = computed(() => { - + - + - + - - - - - + + + + {{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }} + + 题号 + + + + + + + + + {{ btn.text }} - - - - 打分设置 - - - 查看均分 - - - 查看答案 - - - - - - - + + + + + + + - + - - - - 历史打分 + + + + 满分 + {{ formatScore(currentQuestionFullScore) }} - - 满分/{{ `${formatScore(currentQuestionFullScore)}分` }} - - - {{ isViewingHistory ? `${formatScore(currentQuestionFinalScore)}/${formatScore(currentQuestionFullScore)}` : '' }} + + + + + 得分 + + {{ isViewingHistory ? formatScore(currentQuestionFinalScore) : '--' }} + - - - + - - - + - + - - - - - - - - - - - - - - - - - - {{ Math.min(totalQuestions, currentTaskSubmit) }}/{{ totalQuestions }} - - - - {{ Math.min(totalQuestions, currentTaskSubmit) }}/{{ totalQuestions }} - - - 分值 {{ formatScore(currentQuestionFinalScore) }}/{{ formatScore(currentQuestionFullScore) }} - - - - - - - - 打分设置 - - - 查看均分 - - - 查看答案 - - - - - + + + + class="size-32px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 active:bg-slate-100" + @click="emit('goBack')" + > + + + + + + + + {{ currentQuestion?.question_major }}.{{ currentQuestion?.question_minor }} + + + + + + + + + + {{ currentTaskSubmit }}/{{ totalQuestions }} + + + + + + + + + + + + + + + + + 满分 + {{ formatScore(currentQuestionFullScore) }} +
+ 得分 + + {{ isViewingHistory ? formatScore(currentQuestionFinalScore) : '--' }} + + + + + + {{ action.t }} + @@ -301,14 +289,12 @@ const currentHistoryIndexInList = computed(() => { - + - - - + diff --git a/src/components/marking/components/QuickScorePanel.vue b/src/components/marking/components/QuickScorePanel.vue index 81b85a9..490c61d 100644 --- a/src/components/marking/components/QuickScorePanel.vue +++ b/src/components/marking/components/QuickScorePanel.vue @@ -238,6 +238,18 @@ async function submitScore(score: number) { // 提交当前分数 async function submitCurrentScore() { try { + // 如果当前分数是-1(未打分),根据模式设置默认分数 + if (currentScore.value === -1) { + if (currentMode.value === 'subtract') { + // 减分模式:不打分提交就是满分 + currentScore.value = props.fullScore + } + else { + // 加分模式:不打分提交就是0分 + currentScore.value = 0 + } + } + await markingData.submitRecord() uni.showToast({ title: '提交成功', @@ -261,10 +273,12 @@ async function submitCurrentScore() { -
- -
+
+ v-if="scoreMode === 'onekey'" + class="mb-40px mt-6px h-40px flex flex-shrink-0 cursor-pointer items-center justify-center border border-blue-100 rounded-lg bg-blue-50 transition-all active:scale-95" + @click="toggleCollapse" + > +
+
-
+
加分
减分
-
+ -
@@ -363,7 +379,7 @@ async function submitCurrentScore() {
@@ -373,43 +389,43 @@ async function submitCurrentScore() {
-
- -
+
+ v-if="scoreMode === 'onekey'" + class="mb-40px mt-12rpx h-80rpx flex flex-shrink-0 cursor-pointer items-center justify-center border border-blue-100 rounded-16rpx bg-blue-50 transition-all active:scale-95" + @click="toggleCollapse" + > +
+
-
+
加分
减分
-
+
- 全屏查看 + 全屏查看 - + @@ -95,7 +95,7 @@ const expandedImageUrls = computed(() => { - + { - - 长按图片可保存 + + 长按图片可保存 diff --git a/src/components/marking/components/renderer/MarkingImageViewerNew.vue b/src/components/marking/components/renderer/MarkingImageViewerNew.vue index 19efee5..70d786a 100644 --- a/src/components/marking/components/renderer/MarkingImageViewerNew.vue +++ b/src/components/marking/components/renderer/MarkingImageViewerNew.vue @@ -17,6 +17,9 @@ const emit = defineEmits<{ 'marking-change': [questionIndex: number, imageIndex: number, data: DomMarkingData] 'ready': [imageInfo: { width: number, height: number }] 'scroll-edge': [atLeftEdge: boolean, atRightEdge: boolean] + 'touch-start': [event: TouchEvent] + 'touch-move': [event: TouchEvent] + 'touch-end': [event: TouchEvent] }>() const scale = defineModel('scale', { default: 1.0 }) @@ -31,6 +34,21 @@ const firstImageSize = ref({ width: 0, height: 0 }) // 滚动容器ID(用于 uni.createSelectorQuery) const scrollContainerId = `scroll-container-${Math.random().toString(36).slice(2)}` +/** + * 手势事件透传 + */ +function handleTouchStart(e: TouchEvent) { + emit('touch-start', e) +} + +function handleTouchMove(e: TouchEvent) { + emit('touch-move', e) +} + +function handleTouchEnd(e: TouchEvent) { + emit('touch-end', e) +} + /** * 检查滚动边缘状态(使用 uni.createSelectorQuery) */ @@ -39,12 +57,14 @@ function checkScrollEdge() { query.select(`#${scrollContainerId}`).scrollOffset() query.select(`#${scrollContainerId}`).boundingClientRect() query.exec((res) => { - if (!res || res.length < 2) return + if (!res || res.length < 2) + return const scrollInfo = res[0] const rectInfo = res[1] - if (!scrollInfo || !rectInfo) return + if (!scrollInfo || !rectInfo) + return const scrollLeft = scrollInfo.scrollLeft || 0 const scrollWidth = scrollInfo.scrollWidth || 0 @@ -153,10 +173,15 @@ defineExpose({ diff --git a/src/components/marking/components/renderer/TraceToolbar.vue b/src/components/marking/components/renderer/TraceToolbar.vue index ebc8af6..d6d4fe6 100644 --- a/src/components/marking/components/renderer/TraceToolbar.vue +++ b/src/components/marking/components/renderer/TraceToolbar.vue @@ -40,89 +40,44 @@ const emit = defineEmits<{ 'toggle-special-mark': [type: 'excellent' | 'typical' | 'problem'] }>() const currentTool = defineModel('currentTool', { required: true }) -const showToolOptions = computed(() => { - return [MarkingTool.PEN, MarkingTool.TEXT].includes(currentTool.value) -}) // 工具栏引用 const toolbarRef = ref() -// 工具栏位置状态,使用 SessionStorage 持久化,默认位于左下角 -const position = useSessionStorage('marking-toolbar-position', { x: 16, y: 16 }) - // 收起状态 const isCollapsed = ref(uni.getStorageSync('marking-toolbar-collapsed') || false) watch(isCollapsed, (newVal) => { uni.setStorageSync('marking-toolbar-collapsed', newVal) }) -// 拖动状态 -const isDragging = ref(false) -const dragState = ref({ - startX: 0, - startY: 0, - startPositionX: 0, - startPositionY: 0, -}) - const { getDictOptionsComputed } = useDict() const { options: problemTypeOptions } = getDictOptionsComputed(DictCode.SCAN_ANOMALY_STATUS) -const problemTooltipContent = computed(() => { - if (props.currentMarkingData?.isProblem) { - return `当前问题类型:${problemTypeOptions.value.find(item => item.value === props.currentMarkingData?.problemType)?.label}` - } - return '标记为问题卷' -}) /** * 将 SVG 转换为 data URL(用于小程序) */ function svgToDataURL(svg: string): string { - // 将被设置到 dataset 中的属性还原出来 svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1') - - // 将被设置到 data-xlink-href 的属性还原出来 svg = svg.replace(/xlink-href=/g, 'xlink:href=') - - // 将 dataset 中被变成 kebab-case 写法的 viewBox 还原出来 svg = svg.replace(/view-box=/g, 'viewBox=') - - // 清除 SVG 中不应该显示的 title、desc、defs 元素 svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '') - - // 为非标准 XML 的 SVG 添加 xmlns,防止视图层解析出错 if (!/xmlns=/.test(svg)) svg = svg.replace(/ Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any) - - // 清除注释,缓解数据量过大的问题 svg = svg.replace(//g, '') - - // 模拟 HTML 的 white-space 行为,将多个空格或换行符换成一个空格,减少数据量 svg = svg.replace(/\s+/g, ' ') - - // 对特殊符号进行转义,这里参考了 https://github.com/bhovhannes/svg-url-loader/blob/master/src/loader.js svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => { return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}` }) - - // 单引号替换为 \',由于 kbone 的 bug,节点属性中的双引号在生成 outerHTML 时不会被转义导致出错 - // 因此 background-image: url( 后面只能跟单引号,所以生成的 URI 内部也就只能用斜杠转义单引号了 svg = svg.replace(/'/g, '\\\'') - - // 最后添加 mime 头部,变成 Webview 可以识别的 Data URI return `data:image/svg+xml,${svg.trim()}` } /** * 生成半对标记的 SVG data URL - * 根据当前按钮状态动态生成颜色 */ function generateHalfMarkSvg(isActive: boolean): string { - // 根据激活状态选择颜色 - const color = isActive ? '#ffffff' : '#eab308' // 白色(激活)或黄色(未激活) + const color = isActive ? '#ffffff' : '#eab308' const svg = ` @@ -142,9 +97,6 @@ const pendingMarkType = ref<'correct' | 'wrong' | 'half' | null>(null) // 画笔提示相关 const PEN_TOOLTIP_KEY = 'marking-pen-tooltip-shown' -/** - * 显示画笔提示(仅首次显示) - */ function showPenTooltip() { const hasShown = uni.getStorageSync(PEN_TOOLTIP_KEY) if (!hasShown) { @@ -160,91 +112,6 @@ function showPenTooltip() { } } -// 统一的按钮基础样式 -const baseButtonClass = computed(() => { - return props.isLandscape - ? 'size-32rpx lg:size-8 !p-0 rounded-lg flex items-center justify-center transition-all duration-200 ease-in-out shadow-sm hover:shadow-md active:scale-95 after:border-none' - : 'size-8 lg:size-10 !p-0 rounded-lg flex items-center justify-center transition-all duration-200 ease-in-out shadow-sm hover:shadow-md active:scale-95 after:border-none' -}) - -const separatorClass = computed(() => { - return props.isLandscape - ? 'mx-4rpx lg:mx-1 h-4 lg:h-6 w-px flex-shrink-0 bg-gray-300' - : 'h-5 lg:h-6 w-px bg-gray-300' -}) -const iconClass = computed(() => { - return props.isLandscape - ? 'size-20rpx lg:size-4' - : 'size-5 lg:size-5 w-4 lg:w-5' -}) - -/** - * 获取工具按钮样式类 - */ -function getToolButtonClass(tool: MarkingTool) { - const isActive = currentTool.value === tool - return [ - baseButtonClass.value, - isActive - ? 'bg-blue-600 text-white border border-blue-500' - : 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700', - ] -} - -/** - * 获取标记按钮样式类(对错半对) - */ -function getMarkButtonClass(type: 'correct' | 'wrong' | 'half') { - const toolMap = { - correct: MarkingTool.CORRECT, - wrong: MarkingTool.WRONG, - half: MarkingTool.HALF, - } - const colorMap = { - correct: { - active: 'bg-green-500 text-white border border-green-400', - inactive: 'bg-white text-green-600 border border-green-200 hover:bg-green-50 hover:border-green-300 hover:text-green-700', - }, - wrong: { - active: 'bg-red-500 text-white border border-red-400', - inactive: 'bg-white text-red-600 border border-red-200 hover:bg-red-50 hover:border-red-300 hover:text-red-700', - }, - half: { - active: 'bg-yellow-500 text-white border border-yellow-400', - inactive: 'bg-white text-yellow-600 border border-yellow-200 hover:bg-yellow-50 hover:border-yellow-300 hover:text-yellow-700', - }, - } - - const isActive = currentTool.value === toolMap[type] - const colors = colorMap[type] - - return [baseButtonClass.value, isActive ? colors.active : colors.inactive] -} - -/** - * 获取特殊标记按钮样式类 - */ -function getSpecialButtonClass(type: 'excellent' | 'typical' | 'problem') { - const isActive = props.specialMarks[type] - const colorMap = { - excellent: { - active: 'bg-yellow-500 text-white border border-yellow-400', - inactive: 'bg-white text-yellow-600 border border-yellow-200 hover:bg-yellow-50 hover:border-yellow-300 hover:text-yellow-700', - }, - typical: { - active: 'bg-blue-600 text-white border border-blue-500', - inactive: 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700', - }, - problem: { - active: 'bg-red-500 text-white border border-red-400', - inactive: 'bg-white text-red-600 border border-red-200 hover:bg-red-50 hover:border-red-300 hover:text-red-700', - }, - } - - const colors = colorMap[type] - return [baseButtonClass.value, props.isLandscape ? 'text-12rpx lg:text-sm' : 'text-sm lg:text-base', 'font-medium', isActive ? colors.active : colors.inactive] -} - /** * 切换工具(支持取消激活) */ @@ -253,17 +120,15 @@ function switchTool(tool: MarkingTool) { currentTool.value = wasDeactivated ? MarkingTool.SELECT : tool pendingMarkType.value = null - // 如果是激活画笔工具,显示提示 if (!wasDeactivated && tool === MarkingTool.PEN) { showPenTooltip() } - // 显示/隐藏工具选项 settings.value.showToolOptions = currentTool.value === 'pen' || currentTool.value === 'text' } /** - * 处理标记工具切换(正确/错误/半对) + * 处理标记工具切换 */ function handleMarkTool(type: 'correct' | 'wrong' | 'half') { const toolMap = { @@ -277,7 +142,7 @@ function handleMarkTool(type: 'correct' | 'wrong' | 'half') { } /** - * 处理图片点击(用于放置标记) + * 处理图片点击 */ function handleImageClick(position: { x: number, y: number }) { if (pendingMarkType.value) { @@ -292,46 +157,10 @@ function handleImageClick(position: { x: number, y: number }) { emit('add-half-mark', position) break } - pendingMarkType.value = null } } -/** - * 处理撤销 - */ -function handleUndo() { - if (props.canUndo) { - emit('undo') - } -} - -/** - * 处理重做 - */ -function handleRedo() { - if (props.canRedo) { - emit('redo') - } -} - -/** - * 处理清除所有 - */ -function handleClearAll() { - if (props.hasMarks) { - uni.showModal({ - confirmButtonText: '确认清除', - cancelButtonText: '取消', - success: (result) => { - if (result.confirm) { - emit('clear-all') - } - }, - }) - } -} - /** * 切换特殊标记 */ @@ -339,9 +168,6 @@ function toggleSpecialMark(type: 'excellent' | 'typical' | 'problem') { emit('toggle-special-mark', type) } -/** - * 切换收起/展开状态 - */ function toggleCollapse() { isCollapsed.value = !isCollapsed.value } @@ -355,135 +181,193 @@ defineExpose({
- +
-
+
- +
- - +
+
-
- - - - + +
- - - + + +
+ +
+ +
+
+
+ - - - + +
+
+
+ -
- - - - + +
+ +
+
+
- - - - + +
- - - - + +
+ +
+
+
+ -
+ +
+
+
+ - - + +
+
+
+ +
- - - - + +
- - - - - - - + +
+
+ {{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }} +
+
+
+
+ + diff --git a/src/components/marking/composables/useMarkingData.ts b/src/components/marking/composables/useMarkingData.ts index c75b45d..c4ddfe9 100644 --- a/src/components/marking/composables/useMarkingData.ts +++ b/src/components/marking/composables/useMarkingData.ts @@ -52,7 +52,8 @@ function createMarkingData(options: UseMarkingDataOptions) { const currentScore = computed({ get: () => { - return currentMarkingSubmitData.value[firstNotScoredIndex.value]?.score || -1 + const score = currentMarkingSubmitData.value[firstNotScoredIndex.value]?.score + return score !== undefined ? score : -1 }, set: (value) => { if (currentMarkingSubmitData.value[firstNotScoredIndex.value]) { diff --git a/src/components/marking/composables/useMarkingSettings.ts b/src/components/marking/composables/useMarkingSettings.ts index 2d7a8d5..fa31f15 100644 --- a/src/components/marking/composables/useMarkingSettings.ts +++ b/src/components/marking/composables/useMarkingSettings.ts @@ -88,7 +88,7 @@ const defaultSettings: MarkingSettings = { // 快捷打分默认值 quickScoreMode: 'onekey', - quickScoreScores: [1, 2], + quickScoreScores: [0.5, 1, 2, 3], quickScoreLayout: 'single', // 一键打分默认值 diff --git a/src/composables/marking/MarkingContext.ts b/src/composables/marking/MarkingContext.ts index a34a1bf..8014b50 100644 --- a/src/composables/marking/MarkingContext.ts +++ b/src/composables/marking/MarkingContext.ts @@ -104,6 +104,8 @@ export interface MarkingContext { defaultPosition?: 'first' | 'last' // 默认定位:first-第一个,last-最后一个 } +export const cachedImages = ref([]) + /** * 默认的阅卷数据提供者(使用原有的API) * 带滑动窗口缓存优化 @@ -115,8 +117,6 @@ export class DefaultMarkingDataProvider implements MarkingDataProvider { private hasSubmitted = false private fetchingPromise: Promise | null = null - cachedImages = ref([]) - async getSingleQuestion(taskId: number): Promise { // 如果任务ID变化,清空缓存 if (this.lastTaskId !== taskId) { @@ -176,7 +176,8 @@ export class DefaultMarkingDataProvider implements MarkingDataProvider { // 预加载图片资源 if (this.cachedQuestion.questions?.[0]?.image_urls) { - this.cachedImages.value = this.cachedQuestion.questions?.[0]?.image_urls || [] + cachedImages.value = this.cachedQuestion.questions?.[0]?.image_urls || [] + console.log('cachedImages', cachedImages.value) } } else { diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index bf8f0ea..f1a0895 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -231,7 +231,7 @@ function handleExamChange() { uni.showActionSheet({ itemList: examNames, success: (res) => { - const selectedOption = homeStore.examOptions[res.tapIndex].filter(item => item.) + const selectedOption = homeStore.examOptions[res.tapIndex] homeStore.selectedExamId = selectedOption.value as number // 选择考试后重新获取统计数据 fetchExamStats() diff --git a/src/pages/marking/grading.vue b/src/pages/marking/grading.vue index b4b46d9..ecc1d3b 100644 --- a/src/pages/marking/grading.vue +++ b/src/pages/marking/grading.vue @@ -19,7 +19,7 @@ import QuickScorePanel from '@/components/marking/components/QuickScorePanel.vue import MarkingImageViewerNew from '@/components/marking/components/renderer/MarkingImageViewerNew.vue' import { provideMarkingData } from '@/components/marking/composables/useMarkingData' import MarkingLayout from '@/components/marking/MarkingLayout.vue' -import { DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext, useMarkingContext } from '@/composables/marking/MarkingContext' +import { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext, useMarkingContext } from '@/composables/marking/MarkingContext' import { provideMarkingHistory } from '@/composables/marking/useMarkingHistory' import { provideMarkingNavigation } from '@/composables/marking/useMarkingNavigation' import { useSafeArea } from '@/composables/useSafeArea' @@ -473,7 +473,7 @@ function handleGlobalTouchEnd(e: TouchEvent) { // 下一题的图片列表(用于预加载) const nextQuestionImages = computed(() => { - return markingDataProvider.cachedImages.value + return cachedImages.value }) @@ -481,9 +481,6 @@ const nextQuestionImages = computed(() => {
{ > {{ Math.round(imageScale * 100) }}% + { :next-question-images="nextQuestionImages" @ready="handleImageReady" @scroll-edge="handleScrollEdge" + @touch-start="handleGlobalTouchStart" + @touch-move="handleGlobalTouchMove" + @touch-end="handleGlobalTouchEnd" />