diff --git a/src/components/marking/components/QuickScorePanel.vue b/src/components/marking/components/QuickScorePanel.vue index 490c61d..276299a 100644 --- a/src/components/marking/components/QuickScorePanel.vue +++ b/src/components/marking/components/QuickScorePanel.vue @@ -170,30 +170,13 @@ function toggleCollapse() { // 选择分数 function selectScore(score: number) { - if (scoreMode.value === 'onekey') { - // 一键打分模式:直接设置分数并提交 - currentScore.value = score - emit('score-selected', score) - submitScore(score) - } - else { - // 快捷打分模式:激活点击模式 - enableQuickScoreClickMode(score) - } + currentScore.value = score + emit('score-selected', score) } // 增加分数 function addToScore(addValue: number) { - if (scoreMode.value === 'onekey') { - const newScore = Math.min(Math.max(currentScore.value + addValue, 0), props.fullScore) - currentScore.value = newScore - emit('score-selected', newScore) - submitScore(newScore) - } - else { - // 快捷打分模式:激活点击模式 - enableQuickScoreClickMode(Math.abs(addValue)) - } + enableQuickScoreClickMode(Math.abs(addValue)) } /** @@ -221,53 +204,21 @@ function enableQuickScoreClickMode(value: number) { } } -// 提交分数 -async function submitScore(score: number) { - try { - await markingData.submitRecord() - } - catch (error) { - console.error('提交分数失败:', error) - uni.showToast({ - title: '提交失败', - icon: 'none', - }) - } -} - // 提交当前分数 async function submitCurrentScore() { - try { - // 如果当前分数是-1(未打分),根据模式设置默认分数 - if (currentScore.value === -1) { - if (currentMode.value === 'subtract') { - // 减分模式:不打分提交就是满分 - currentScore.value = props.fullScore - } - else { - // 加分模式:不打分提交就是0分 - currentScore.value = 0 - } + // 如果当前分数是-1(未打分),根据模式设置默认分数 + if (currentScore.value === -1) { + if (currentMode.value === 'subtract') { + // 减分模式:不打分提交就是满分 + currentScore.value = props.fullScore } + else { + // 加分模式:不打分提交就是0分 + currentScore.value = 0 + } + } - await markingData.submitRecord() - uni.showToast({ - title: '提交成功', - icon: 'success', - }) - // 关闭快捷打分点击模式 - if (settings.value.quickScoreClickMode) { - settings.value.quickScoreClickMode = false - settings.value.quickScoreClickValue = 0 - } - } - catch (error) { - console.error('提交分数失败:', error) - uni.showToast({ - title: '提交失败', - icon: 'none', - }) - } + emit('score-selected', currentScore.value) } diff --git a/src/components/marking/components/renderer/MarkingImageViewerNew.vue b/src/components/marking/components/renderer/MarkingImageViewerNew.vue index 43b4a6e..a4ce516 100644 --- a/src/components/marking/components/renderer/MarkingImageViewerNew.vue +++ b/src/components/marking/components/renderer/MarkingImageViewerNew.vue @@ -2,9 +2,9 @@ import type { DomMarkingData } from '../../composables/renderer/useMarkingDom' import type { ExamStudentMarkingQuestionResponse } from '@/api' import { parseOSSImageSize } from '@/utils/image' +import { MarkingTool } from '../../composables/renderer/useMarkingDom' import { markingSettings } from '../../composables/useMarkingSettings' import { useSimpleMarkingTool } from '../../composables/useSimpleMarkingTool' -import { MarkingTool } from '../../composables/renderer/useMarkingDom' import QuestionRenderer from './QuestionRenderer.vue' interface Props { @@ -46,10 +46,16 @@ const firstImageSize = ref({ width: 0, height: 0 }) // 滚动容器ID(用于 uni.createSelectorQuery) const scrollContainerId = `scroll-container-${Math.random().toString(36).slice(2)}` +const onTouchScale = ref(false) + /** * 手势事件透传 */ function handleTouchStart(e: TouchEvent) { + // 如果双指 则禁用 Y 轴滚动 + if (e.touches.length >= 2) { + onTouchScale.value = true + } emit('touch-start', e) } @@ -58,6 +64,7 @@ function handleTouchMove(e: TouchEvent) { } function handleTouchEnd(e: TouchEvent) { + onTouchScale.value = false emit('touch-end', e) } @@ -194,8 +201,8 @@ defineExpose({ import type { MarkingSubmitData } from '../../composables/useMarkingData' -import { useSessionStorage } from '@vueuse/core' import { DictCode, useDict } from '@/composables/useDict' +import { svgToDataURL } from '@/utils/dom' import { MarkingTool } from '../../composables/renderer/useMarkingDom' import { markingSettings as settings } from '../../composables/useMarkingSettings' @@ -41,36 +41,146 @@ const emit = defineEmits<{ }>() const currentTool = defineModel('currentTool', { required: true }) -// 工具栏引用 -const toolbarRef = ref() - // 收起状态 const isCollapsed = ref(uni.getStorageSync('marking-toolbar-collapsed') || false) +// 用于控制动画,只有用户主动展开时才显示动画 +const shouldAnimate = ref(false) + watch(isCollapsed, (newVal) => { uni.setStorageSync('marking-toolbar-collapsed', newVal) + // 只有从收起到展开时才需要动画 + if (!newVal) { + shouldAnimate.value = true + } + // 收起/展开后重新计算位置,或者保持位置不变(保持 top/left 不变自然就是 anchored to top-left) }) const { getDictOptionsComputed } = useDict() const { options: problemTypeOptions } = getDictOptionsComputed(DictCode.SCAN_ANOMALY_STATUS) -/** - * 将 SVG 转换为 data URL(用于小程序) - */ -function svgToDataURL(svg: string): string { - svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1') - svg = svg.replace(/xlink-href=/g, 'xlink:href=') - svg = svg.replace(/view-box=/g, 'viewBox=') - svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '') - if (!/xmlns=/.test(svg)) - svg = svg.replace(/ Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any) - svg = svg.replace(//g, '') - svg = svg.replace(/\s+/g, ' ') - svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => { - return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}` - }) - svg = svg.replace(/'/g, '\\\'') - return `data:image/svg+xml,${svg.trim()}` +// --- Position & Dragging Logic --- +const position = ref<{ x: number, y: number } | null>(null) +const storageKey = computed(() => `marking-toolbar-pos-${props.isLandscape ? 'landscape' : 'portrait'}`) + +// 初始化或重置位置 +function loadPosition() { + const stored = uni.getStorageSync(storageKey.value) + if (stored) { + position.value = stored + } + else { + // 如果没有存储的位置,设置默认位置为左下角 + const { windowHeight } = uni.getSystemInfoSync() + const offset = props.isLandscape ? 12 : 48 // 与 CSS 中的 bottom/left 保持一致 + position.value = { + x: offset, + y: windowHeight - offset - (props.isLandscape ? 48 : 80), // 减去按钮高度 + } + } +} + +watch(() => props.isLandscape, () => { + loadPosition() +}, { immediate: true }) + +// 拖拽状态 +let isDragging = false +let hasMoved = false // 是否有实际移动 +let startTouch = { x: 0, y: 0 } +let startPos = { x: 0, y: 0 } +let touchStartTime = 0 // 触摸开始时间 + +// 判断是否为点击的阈值 +const CLICK_DISTANCE_THRESHOLD = 20 // 移动距离小于20px认为是点击 +const CLICK_TIME_THRESHOLD = 300 // 时间小于300ms认为是点击 + +function onTouchStart(e: TouchEvent) { + if (e.touches.length !== 1) + return + + // 阻止冒泡,防止触发画布点击等 + // e.stopPropagation() // 在模板中使用 .stop + + const touch = e.touches[0] + startTouch = { x: touch.clientX, y: touch.clientY } + touchStartTime = Date.now() + isDragging = true + hasMoved = false + + if (position.value) { + startPos = { ...position.value } + } + else { + // 如果当前是默认位置(CSS定位),需要先获取当前实际位置 + // 使用 uni.createSelectorQuery 兼容小程序 + const query = uni.createSelectorQuery().in(getCurrentInstance()) + query.select('.trace-toolbar').boundingClientRect((data) => { + const rect = data as UniApp.NodeInfo + if (rect) { + position.value = { x: rect.left || 0, y: rect.top || 0 } + startPos = { x: rect.left || 0, y: rect.top || 0 } + } + }).exec() + } +} + +function onTouchMove(e: TouchEvent) { + if (!isDragging || e.touches.length !== 1) + return + + const touch = e.touches[0] + const dx = touch.clientX - startTouch.x + const dy = touch.clientY - startTouch.y + const distance = Math.sqrt(dx * dx + dy * dy) + + // 如果移动距离超过阈值,标记为已移动 + if (distance > CLICK_DISTANCE_THRESHOLD) { + hasMoved = true + } + + // 如果起始位置还没获取到(异步Query),则跳过 + if (!position.value && dx === 0 && dy === 0) + return + + // 简单的防抖或阈值可以加在这里,但为了流畅性直接更新 + // 如果 position 刚被 query 设置,可能在这里会突变? + // 应该基于 startPos 更新 + + // 如果 startPos 还是 0,0 且 position 也是 null (Query 还没回来),忽略 + // 这里简化处理,假设 Query 很快或下一次 move 生效 + + if (position.value) { + const newX = startPos.x + dx + const newY = startPos.y + dy + + // 边界限制(可选,这里暂不严格限制,允许拖到边缘) + const { windowWidth, windowHeight } = uni.getSystemInfoSync() + // 简单限制在屏幕内 + // position.value = { + // x: Math.max(0, Math.min(newX, windowWidth - 40)), + // y: Math.max(0, Math.min(newY, windowHeight - 40)) + // } + position.value = { x: newX, y: newY } + } +} + +function onTouchEnd() { + const touchDuration = Date.now() - touchStartTime + + // 判断是否为点击:移动距离短 且 时间短 + const isClick = !hasMoved && touchDuration < CLICK_TIME_THRESHOLD + + if (isDragging && position.value && !isClick) { + // 只有在真正拖拽时才保存位置 + uni.setStorageSync(storageKey.value, position.value) + } + else if (isClick) { + // 如果是点击,恢复原始位置 + position.value = startPos.x !== 0 || startPos.y !== 0 ? { ...startPos } : position.value + } + + isDragging = false + hasMoved = false } /** @@ -179,195 +289,180 @@ defineExpose({
- +
-
-
- - -
- +
-
+
- -
- - - +
-
- -
- + +
+ +
+
+ + +
+ + + +
+
+ +
+ + +
+
+
+ + + +
+
+
+ + + +
+ +
+
+
+ + +
+ + +
+ +
+
+
+ + + +
+
+
+ + + +
+
+
+ +
+ + +
+ + +
-
+ {{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }}
- - - -
-
-
- - - -
- -
-
-
- - -
- - -
- -
-
-
- - - -
-
-
- - - -
-
-
- -
- - -
- - -
-
- {{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }}
- +
-
+ - - diff --git a/src/components/marking/composables/useMarkingSettings.ts b/src/components/marking/composables/useMarkingSettings.ts index fa31f15..4c4d565 100644 --- a/src/components/marking/composables/useMarkingSettings.ts +++ b/src/components/marking/composables/useMarkingSettings.ts @@ -1,4 +1,4 @@ -import { useStorage } from '@vueuse/core' +import { useStorage, watchDeep } from '@vueuse/core' const SETTINGS_VERSION = 2 // 当前版本号 @@ -100,7 +100,7 @@ const defaultSettings: MarkingSettings = { } const rawSettings = ref(uni.getStorageSync('marking_settings') || defaultSettings) -watch(rawSettings, (newVal) => { +watchDeep(rawSettings, (newVal) => { uni.setStorageSync('marking_settings', newVal) }) diff --git a/src/pages/marking/grading.vue b/src/pages/marking/grading.vue index 2c1c3ba..8adeafc 100644 --- a/src/pages/marking/grading.vue +++ b/src/pages/marking/grading.vue @@ -23,6 +23,7 @@ import { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider import { provideMarkingHistory } from '@/composables/marking/useMarkingHistory' import { provideMarkingNavigation } from '@/composables/marking/useMarkingNavigation' import { useSafeArea } from '@/composables/useSafeArea' +import { svgToDataURL } from '@/utils/dom' defineOptions({ name: 'GradingPage', @@ -399,9 +400,41 @@ whenever(currentTask, (task, oldTask) => { queryClient.invalidateQueries({ queryKey: ['marking-question', task.id] }) }) +// 提交分数反馈状态 +const showScoreFeedback = ref(false) +const feedbackScore = ref(0) +const doubleUnderlineSvg = ` + + +` + // 快捷打分选择 -function handleQuickScoreSelect(score: number) { +async function handleQuickScoreSelect(score: number) { console.log('选择分数:', score) + + // 设置当前分数 + markingData.currentScore.value = score + + // 显示反馈动画 + feedbackScore.value = score + showScoreFeedback.value = true + + try { + await markingData.submitSingleRecord() + } + catch (error) { + console.error('提交失败:', error) + uni.showToast({ + title: error.message || '提交失败', + icon: 'none', + }) + } + finally { + // 延迟隐藏反馈 + setTimeout(() => { + showScoreFeedback.value = false + }, 400) + } } // 历史查看模式提示 @@ -511,7 +544,7 @@ const nextQuestionImages = computed(() => { diff --git a/src/static/app/icons/1024x1024.png b/src/static/app/icons/1024x1024.png index 08dbd5f..7614c20 100644 Binary files a/src/static/app/icons/1024x1024.png and b/src/static/app/icons/1024x1024.png differ diff --git a/src/static/app/icons/120x120.png b/src/static/app/icons/120x120.png index 718ca79..2ee5b7a 100644 Binary files a/src/static/app/icons/120x120.png and b/src/static/app/icons/120x120.png differ diff --git a/src/static/app/icons/144x144.png b/src/static/app/icons/144x144.png index f78346b..9e6149a 100644 Binary files a/src/static/app/icons/144x144.png and b/src/static/app/icons/144x144.png differ diff --git a/src/static/app/icons/152x152.png b/src/static/app/icons/152x152.png index f979721..738122e 100644 Binary files a/src/static/app/icons/152x152.png and b/src/static/app/icons/152x152.png differ diff --git a/src/static/app/icons/167x167.png b/src/static/app/icons/167x167.png index d0aef20..73f242c 100644 Binary files a/src/static/app/icons/167x167.png and b/src/static/app/icons/167x167.png differ diff --git a/src/static/app/icons/180x180.png b/src/static/app/icons/180x180.png index 24bd062..94fcb3e 100644 Binary files a/src/static/app/icons/180x180.png and b/src/static/app/icons/180x180.png differ diff --git a/src/static/app/icons/192x192.png b/src/static/app/icons/192x192.png index a8ea1a2..de9421f 100644 Binary files a/src/static/app/icons/192x192.png and b/src/static/app/icons/192x192.png differ diff --git a/src/static/app/icons/20x20.png b/src/static/app/icons/20x20.png index 0abed04..4e3bd81 100644 Binary files a/src/static/app/icons/20x20.png and b/src/static/app/icons/20x20.png differ diff --git a/src/static/app/icons/29x29.png b/src/static/app/icons/29x29.png index a20d373..e432142 100644 Binary files a/src/static/app/icons/29x29.png and b/src/static/app/icons/29x29.png differ diff --git a/src/static/app/icons/40x40.png b/src/static/app/icons/40x40.png index 2b41be6..b78b1c6 100644 Binary files a/src/static/app/icons/40x40.png and b/src/static/app/icons/40x40.png differ diff --git a/src/static/app/icons/58x58.png b/src/static/app/icons/58x58.png index 8e18b42..e7def38 100644 Binary files a/src/static/app/icons/58x58.png and b/src/static/app/icons/58x58.png differ diff --git a/src/static/app/icons/60x60.png b/src/static/app/icons/60x60.png index 167826b..4bbabc7 100644 Binary files a/src/static/app/icons/60x60.png and b/src/static/app/icons/60x60.png differ diff --git a/src/static/app/icons/72x72.png b/src/static/app/icons/72x72.png index ddb91e3..544c3fc 100644 Binary files a/src/static/app/icons/72x72.png and b/src/static/app/icons/72x72.png differ diff --git a/src/static/app/icons/76x76.png b/src/static/app/icons/76x76.png index 0d9d28e..771dfb2 100644 Binary files a/src/static/app/icons/76x76.png and b/src/static/app/icons/76x76.png differ diff --git a/src/static/app/icons/80x80.png b/src/static/app/icons/80x80.png index 1877042..585c850 100644 Binary files a/src/static/app/icons/80x80.png and b/src/static/app/icons/80x80.png differ diff --git a/src/static/app/icons/87x87.png b/src/static/app/icons/87x87.png index 251fb24..4dd9353 100644 Binary files a/src/static/app/icons/87x87.png and b/src/static/app/icons/87x87.png differ diff --git a/src/static/app/icons/96x96.png b/src/static/app/icons/96x96.png index eccf396..96c6ce5 100644 Binary files a/src/static/app/icons/96x96.png and b/src/static/app/icons/96x96.png differ diff --git a/src/static/logo.png b/src/static/logo.png new file mode 100644 index 0000000..078ca9c Binary files /dev/null and b/src/static/logo.png differ diff --git a/src/static/logo.svg b/src/static/logo.svg deleted file mode 100644 index eaee669..0000000 --- a/src/static/logo.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/utils/dom.ts b/src/utils/dom.ts index d2beed6..abe3597 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -145,3 +145,19 @@ export function getElementRectSync(element: HTMLElement | null): ElementRect | n y: top, } } +export function svgToDataURL(svg: string): string { + svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1') + svg = svg.replace(/xlink-href=/g, 'xlink:href=') + svg = svg.replace(/view-box=/g, 'viewBox=') + svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '') + if (!/xmlns=/.test(svg)) + svg = svg.replace(/ Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any) + svg = svg.replace(//g, '') + svg = svg.replace(/\s+/g, ' ') + svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => { + return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}` + }) + svg = svg.replace(/'/g, '\\\'') + return `data:image/svg+xml,${svg.trim()}` +} diff --git a/uno.config.ts b/uno.config.ts index 5055f45..1c8db99 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -2,12 +2,28 @@ import { presetUni } from '@uni-helper/unocss-preset-uni' import { defineConfig, + definePreset, presetAttributify, presetIcons, transformerDirectives, transformerVariantGroup, } from 'unocss' +export const presetRemToRpx = definePreset(() => { + const baseFontSize = 16 + const remRE = /(-?[.\d]+)rem/g + return { + name: '@unocss/preset-rem-to-px', + postprocess: (util) => { + util.entries.forEach((i) => { + const value = i[1] + if (typeof value === 'string' && remRE.test(value)) + i[1] = value.replace(remRE, (_, p1) => `${p1 * baseFontSize * 2}rpx`) + }) + }, + } +}) + export default defineConfig({ presets: [ presetUni({ @@ -26,6 +42,7 @@ export default defineConfig({ }), // 支持css class属性化 presetAttributify(), + presetRemToRpx(), ], transformers: [ // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令