refactor: 一些优化
continuous-integration/drone/push Build is passing
Details
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 增加分数
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -221,23 +204,8 @@ 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') {
|
||||
|
|
@ -250,24 +218,7 @@ async function submitCurrentScore() {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<scroll-view
|
||||
:id="scrollContainerId"
|
||||
class="h-full w-full"
|
||||
:scroll-y="scrollEnabled"
|
||||
:scroll-x="scrollEnabled"
|
||||
:scroll-y="scrollEnabled && !onTouchScale"
|
||||
:scroll-x="scrollEnabled && !onTouchScale"
|
||||
@scroll="handleScroll"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
|
|
@ -209,7 +216,7 @@ defineExpose({
|
|||
'items-center': markingSettings.imagePosition === 'center',
|
||||
'items-end': markingSettings.imagePosition === 'right',
|
||||
}"
|
||||
:style="{ gap: '24px', padding: '12px' }"
|
||||
:style="{ gap: '24px', padding: '12px 12px 48px' }"
|
||||
>
|
||||
<QuestionRenderer
|
||||
v-for="(question, questionIndex) in questionDataList"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
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<MarkingTool>('currentTool', { required: true })
|
||||
|
||||
// 工具栏引用
|
||||
const toolbarRef = ref<HTMLElement>()
|
||||
|
||||
// 收起状态
|
||||
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(/<svg/, '<svg xmlns=\'http://www.w3.org/2000/svg\'')
|
||||
svg = svg.replace(/\d+\.\d+/g, match => Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any)
|
||||
svg = svg.replace(/<!--[\s\S]*?-->/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,46 +289,47 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<wd-root-portal>
|
||||
<div
|
||||
ref="toolbarRef"
|
||||
class="fixed z-5 flex flex-col items-start transition-all duration-300 ease-out"
|
||||
class="trace-toolbar fixed z-15 flex flex-col items-start transition-opacity duration-300 ease-out"
|
||||
:class="[
|
||||
isLandscape
|
||||
!position && (isLandscape
|
||||
? 'bottom-12px left-1/2 -translate-x-1/2 items-center'
|
||||
: 'bottom-48rpx left-12rpx items-start max-w-[75vw]',
|
||||
: 'bottom-48rpx left-12rpx items-start'), // Removed max-w-[75vw]
|
||||
]"
|
||||
@touchend.stop
|
||||
@touchmove.stop
|
||||
@touchstart.stop
|
||||
:style="position ? { left: `${position.x}px`, top: `${position.y}px` } : {}"
|
||||
>
|
||||
<!-- 收起状态 (FAB) -->
|
||||
<!-- 收起状态 (FAB) - 可以拖拽 -->
|
||||
<div
|
||||
v-if="isCollapsed"
|
||||
class="flex cursor-pointer items-center justify-center rounded-full bg-white shadow-lg transition-all active:scale-95"
|
||||
:class="isLandscape ? 'size-48px' : 'size-80rpx'"
|
||||
@click="toggleCollapse"
|
||||
@click.stop="toggleCollapse"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchmove.stop.prevent="onTouchMove"
|
||||
@touchstart.stop="onTouchStart"
|
||||
>
|
||||
<div class="i-fluent:edit-24-regular text-blue-600" :class="isLandscape ? 'size-24px' : 'size-40rpx'" />
|
||||
</div>
|
||||
|
||||
<!-- 展开状态 -->
|
||||
<!-- 展开状态 - 不可拖拽 -->
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center rounded-full bg-white shadow-xl ring-1 ring-slate-900 ring-opacity-5 transition-all"
|
||||
:class="[
|
||||
isLandscape ? 'px-8px py-4px gap-4px' : 'pl-12rpx pr-6rpx py-10rpx gap-4rpx',
|
||||
{ 'animate-fade-in-up': !isCollapsed },
|
||||
isLandscape ? 'px-8px py-4px gap-4px' : 'pl-8rpx pr-4rpx py-8rpx gap-2rpx', // 竖屏减小内边距和间距
|
||||
]"
|
||||
>
|
||||
<!-- 收起按钮 -->
|
||||
<div
|
||||
class="flex flex-shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 active:scale-95 hover:bg-slate-100"
|
||||
:class="isLandscape ? 'size-32px' : 'size-60rpx'"
|
||||
@click="toggleCollapse"
|
||||
:class="isLandscape ? 'size-32px' : 'size-48rpx'"
|
||||
@click.stop="toggleCollapse"
|
||||
>
|
||||
<!-- 竖屏尺寸减小 size-60rpx -> size-48rpx -->
|
||||
<div
|
||||
:class="[
|
||||
isLandscape ? 'i-carbon:chevron-down size-20px' : 'i-carbon:chevron-left size-36rpx',
|
||||
isLandscape ? 'i-carbon:chevron-down size-20px' : 'i-carbon:chevron-left size-32rpx', // 竖屏 icon 减小
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -226,26 +337,26 @@ defineExpose({
|
|||
<!-- 分隔线 -->
|
||||
<div class="h-4/5 w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<!-- 工具内容区域 (竖屏时可滚动) -->
|
||||
<scroll-view
|
||||
:scroll-x="true"
|
||||
:show-scrollbar="false"
|
||||
<!-- 工具内容区域 (竖屏时不滚动,全显示) -->
|
||||
<!-- 移除 scroll-view,改为 div flex -->
|
||||
<div
|
||||
class="flex whitespace-nowrap"
|
||||
:class="isLandscape ? 'w-auto' : 'max-w-[65vw]'"
|
||||
:class="isLandscape ? 'w-auto' : ''"
|
||||
>
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-16rpx'">
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-8rpx'">
|
||||
<!-- 标记组 -->
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-4rpx gap-4rpx'">
|
||||
<!-- 竖屏 padding/gap 减小 -->
|
||||
<wd-tooltip :content="isLandscape ? '正确' : ''" placement="top">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.CORRECT ? 'bg-green-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-green-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx', // 竖屏尺寸减小
|
||||
]"
|
||||
@click="handleMarkTool('correct')"
|
||||
@click.stop="handleMarkTool('correct')"
|
||||
>
|
||||
<div class="i-fluent:checkmark-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
<div class="i-fluent:checkmark-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
|
|
@ -254,11 +365,11 @@ defineExpose({
|
|||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.WRONG ? 'bg-red-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-red-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx',
|
||||
]"
|
||||
@click="handleMarkTool('wrong')"
|
||||
@click.stop="handleMarkTool('wrong')"
|
||||
>
|
||||
<div class="i-fluent:dismiss-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
<div class="i-fluent:dismiss-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
|
|
@ -267,12 +378,12 @@ defineExpose({
|
|||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.HALF ? 'bg-yellow-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-yellow-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx',
|
||||
]"
|
||||
@click="handleMarkTool('half')"
|
||||
@click.stop="handleMarkTool('half')"
|
||||
>
|
||||
<view
|
||||
:class="isLandscape ? 'size-20px' : 'size-40rpx'"
|
||||
:class="isLandscape ? 'size-20px' : 'size-32rpx'"
|
||||
:style="{
|
||||
backgroundImage: `url('${halfMarkSvg}')`,
|
||||
backgroundSize: 'contain',
|
||||
|
|
@ -288,17 +399,17 @@ defineExpose({
|
|||
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<!-- 绘图组 -->
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-4rpx gap-4rpx'">
|
||||
<wd-tooltip :content="isLandscape ? '画笔' : ''" placement="top">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.PEN ? 'bg-blue-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-blue-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.PEN)"
|
||||
@click.stop="switchTool(MarkingTool.PEN)"
|
||||
>
|
||||
<div class="i-fluent:pen-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
<div class="i-fluent:pen-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
|
|
@ -307,11 +418,11 @@ defineExpose({
|
|||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.TEXT ? 'bg-blue-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-blue-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.TEXT)"
|
||||
@click.stop="switchTool(MarkingTool.TEXT)"
|
||||
>
|
||||
<div class="i-fluent:text-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
<div class="i-fluent:text-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
|
|
@ -320,11 +431,11 @@ defineExpose({
|
|||
class="flex cursor-pointer items-center justify-center rounded-full transition-all active:scale-95"
|
||||
:class="[
|
||||
currentTool === MarkingTool.ERASER ? 'bg-slate-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-slate-700',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
isLandscape ? 'size-32px' : 'size-48rpx',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.ERASER)"
|
||||
@click.stop="switchTool(MarkingTool.ERASER)"
|
||||
>
|
||||
<div class="i-fluent:eraser-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
<div class="i-fluent:eraser-24-regular" :class="isLandscape ? 'size-20px' : 'size-32rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
</div>
|
||||
|
|
@ -333,7 +444,7 @@ defineExpose({
|
|||
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<!-- 特殊标记组 -->
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-12rpx'">
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-6rpx'">
|
||||
<div
|
||||
v-for="type in ['excellent', 'typical', 'problem']"
|
||||
:key="type"
|
||||
|
|
@ -342,32 +453,16 @@ defineExpose({
|
|||
specialMarks[type as keyof typeof specialMarks]
|
||||
? (type === 'excellent' ? 'bg-yellow-100 text-yellow-700' : type === 'typical' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700')
|
||||
: 'bg-slate-50 text-slate-500 hover:bg-slate-100',
|
||||
isLandscape ? 'h-32px px-12px text-12px' : 'h-64rpx px-20rpx text-24rpx',
|
||||
isLandscape ? 'h-32px px-12px text-12px' : 'h-48rpx px-12rpx text-20rpx', // 竖屏减小
|
||||
]"
|
||||
@click="toggleSpecialMark(type as any)"
|
||||
@click.stop="toggleSpecialMark(type as any)"
|
||||
>
|
||||
{{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</wd-root-portal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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<MarkingSettings>(uni.getStorageSync('marking_settings') || defaultSettings)
|
||||
watch(rawSettings, (newVal) => {
|
||||
watchDeep(rawSettings, (newVal) => {
|
||||
uni.setStorageSync('marking_settings', newVal)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `<svg width="100%" height="100%" viewBox="0 0 120 40" fill="none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
<path d="M5 12 Q 60 5 115 12" stroke="red" stroke-width="5" stroke-linecap="round" fill="none" />
|
||||
<path d="M10 28 Q 60 22 110 30" stroke="red" stroke-width="5" stroke-linecap="round" fill="none" />
|
||||
</svg>`
|
||||
|
||||
// 快捷打分选择
|
||||
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(() => {
|
|||
<template>
|
||||
<div
|
||||
class="relative h-screen w-100vw flex flex-col touch-pan-x touch-pan-y overflow-hidden overscroll-none"
|
||||
:style="{ paddingTop: `${(safeAreaInsets?.top || 0)}px` }"
|
||||
:style="{ paddingTop: `${(isLandscape ? 0 : safeAreaInsets?.top || 0)}px` }"
|
||||
@touchstart="handleGlobalTouchStart"
|
||||
@touchmove="handleGlobalTouchMove"
|
||||
@touchend="handleGlobalTouchEnd"
|
||||
|
|
@ -614,6 +647,18 @@ const nextQuestionImages = computed(() => {
|
|||
v-model="showFullscreenImage"
|
||||
:image-urls="currentImageUrls"
|
||||
/>
|
||||
|
||||
<!-- 提交分数反馈动画 -->
|
||||
<view
|
||||
v-if="showScoreFeedback"
|
||||
class="pointer-events-none fixed left-1/2 top-1/2 z-999 flex flex-col items-center justify-center text-red-500"
|
||||
style="transform: translate(-50%, -50%) rotate(-10deg)"
|
||||
>
|
||||
<text class="text-96px font-bold leading-none" style="font-family: 'KaiTi', 'STKaiti', serif">
|
||||
{{ feedbackScore }}
|
||||
</text>
|
||||
<view class="h-7 w-20" :style="{ backgroundImage: `url('${svgToDataURL(doubleUnderlineSvg)}')`, backgroundSize: 'contain', backgroundRepeat: 'no-repeat', backgroundPosition: 'center' }" />
|
||||
</view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 780 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 985 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.39 113.39">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d14328;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #2c8d3a;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层 1">
|
||||
<g>
|
||||
<rect class="cls-1" width="113.39" height="113.39" />
|
||||
<g>
|
||||
<path class="cls-3"
|
||||
d="M86.31,11.34H25.08c-8.14,0-14.74,6.6-14.74,14.74v61.23c0,8.14,6.6,14.74,14.74,14.74h61.23c.12,0,.24-.02,.37-.02-9.76-.2-17.64-8.18-17.64-17.99,0-.56,.03-1.12,.08-1.67H34.1c-1.57,0-2.83-1.27-2.83-2.83V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v36.52c0,.78,.63,1.42,1.42,1.42h22.02c.78,0,1.42-.63,1.42-1.42V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v34.99c2.13-.89,4.47-1.39,6.92-1.39,5.66,0,10.7,2.63,14.01,6.72V26.08c0-8.14-6.6-14.74-14.74-14.74Z" />
|
||||
<g>
|
||||
<path class="cls-2"
|
||||
d="M87.04,68.03c-8.83,0-16.01,7.18-16.01,16.01s7.18,16.01,16.01,16.01,16.01-7.18,16.01-16.01-7.18-16.01-16.01-16.01Zm-.27,24.84h-7.2v-3h1.18v-10.48h4.58v2.81h1.42c.84,0,1.46-.16,1.88-.48s.62-.87,.62-1.64c0-.69-.25-1.17-.74-1.45s-1.19-.42-2.09-.42h-6.84v-3h7.2c2.38,0,4.15,.38,5.31,1.15,1.16,.77,1.74,1.93,1.74,3.48,0,1.71-.83,2.93-2.5,3.64,1.07,.4,1.87,.95,2.39,1.65s.79,1.56,.79,2.58c0,3.44-2.58,5.16-7.73,5.16Z" />
|
||||
<path class="cls-2"
|
||||
d="M86.49,85.17h-1.16v4.7h1.8c.81,0,1.46-.18,1.94-.55s.72-.95,.72-1.73c0-.86-.25-1.48-.74-1.85s-1.35-.56-2.56-.56Z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
|
@ -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(/<svg/, '<svg xmlns=\'http://www.w3.org/2000/svg\'')
|
||||
svg = svg.replace(/\d+\.\d+/g, match => Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any)
|
||||
svg = svg.replace(/<!--[\s\S]*?-->/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()}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 指令
|
||||
|
|
|
|||