refactor: 一些优化
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
9d100efbf4
commit
2ed92d3f7a
|
|
@ -2,6 +2,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { ExamQuestionWithTasksResponse } from '@/api'
|
import type { ExamQuestionWithTasksResponse } from '@/api'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useMarkingSettings } from '@/components/marking/composables/useMarkingSettings'
|
||||||
import { useMarkingHistory } from '@/composables/marking/useMarkingHistory'
|
import { useMarkingHistory } from '@/composables/marking/useMarkingHistory'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -49,6 +50,7 @@ const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
// 获取历史记录管理
|
// 获取历史记录管理
|
||||||
const markingHistory = useMarkingHistory()
|
const markingHistory = useMarkingHistory()
|
||||||
|
const settings = useMarkingSettings()
|
||||||
const currentHistoryPage = ref(1)
|
const currentHistoryPage = ref(1)
|
||||||
const { data: historyPageData, isLoading: isLoadingHistory } = markingHistory.useHistoryPage(
|
const { data: historyPageData, isLoading: isLoadingHistory } = markingHistory.useHistoryPage(
|
||||||
computed(() => currentHistoryPage.value),
|
computed(() => currentHistoryPage.value),
|
||||||
|
|
@ -129,13 +131,28 @@ const currentHistoryIndexInList = computed(() => {
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 中间题号信息 -->
|
<!-- 中间题号信息 -->
|
||||||
<view class="flex flex-1 flex-col justify-center">
|
<view class="flex flex-1 items-center justify-center gap-4">
|
||||||
<view class="flex items-baseline gap-2">
|
<!-- 题目选择器 -->
|
||||||
<text class="text-18px text-slate-800 font-bold leading-none">
|
<wd-picker
|
||||||
{{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }}
|
:model-value="currentQuestionIndex"
|
||||||
|
:columns="questionPickerColumns"
|
||||||
|
:value="String(currentQuestionIndex)"
|
||||||
|
:title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
|
||||||
|
@confirm="({ value }) => emit('selectQuestion', Number(value))"
|
||||||
|
>
|
||||||
|
<view class="flex items-center gap-4px rounded-full bg-slate-50 px-12px py-4px active:bg-slate-100">
|
||||||
|
<text class="text-16px text-slate-800 font-bold">
|
||||||
|
{{ currentQuestion?.question_major }}.{{ currentQuestion?.question_minor }}
|
||||||
</text>
|
</text>
|
||||||
<text class="text-12px text-slate-400 leading-none">
|
<view class="i-carbon-chevron-down text-12px text-slate-400" />
|
||||||
题号
|
</view>
|
||||||
|
</wd-picker>
|
||||||
|
|
||||||
|
<view class="h-16px w-1px bg-slate-200" />
|
||||||
|
|
||||||
|
<view class="flex items-baseline gap-2">
|
||||||
|
<text class="text-16px text-slate-600 font-medium leading-none">
|
||||||
|
{{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -195,14 +212,19 @@ const currentHistoryIndexInList = computed(() => {
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 中间图片区域 -->
|
<!-- 中间图片区域 -->
|
||||||
<view class="relative flex-1 overflow-hidden bg-slate-50">
|
<view
|
||||||
|
class="relative flex-1 overflow-hidden bg-slate-50"
|
||||||
|
:class="settings.quickScorePosition === 'left' ? 'order-3' : 'order-2'"
|
||||||
|
>
|
||||||
<slot name="content" :current-question="currentQuestion" />
|
<slot name="content" :current-question="currentQuestion" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 右侧打分区域 -->
|
<!-- 右侧打分区域 -->
|
||||||
|
<view :class="settings.quickScorePosition === 'left' ? 'order-2' : 'order-3'">
|
||||||
<slot name="scoring" />
|
<slot name="scoring" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 竖屏布局 -->
|
<!-- 竖屏布局 -->
|
||||||
<view v-else class="h-100vh w-100vw flex flex-grow flex-col bg-slate-50">
|
<view v-else class="h-100vh w-100vw flex flex-grow flex-col bg-slate-50">
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'score-selected', score: number): void
|
(e: 'score-selected', score: number): void
|
||||||
|
(e: 'toggle-collapse'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -33,7 +34,10 @@ const currentMode = computed({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否收起
|
// 是否收起
|
||||||
const isCollapsed = ref(true)
|
const isCollapsed = ref(uni.getStorageSync('quick-score-collapse') || true)
|
||||||
|
watch(isCollapsed, (newVal) => {
|
||||||
|
uni.setStorageSync('quick-score-collapse', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
// 悬浮按钮的位置 (使用 SessionStorage 持久化)
|
// 悬浮按钮的位置 (使用 SessionStorage 持久化)
|
||||||
const floatingButtonPosition = ref(uni.getStorageSync('quick-score-submit-button-position') || {
|
const floatingButtonPosition = ref(uni.getStorageSync('quick-score-submit-button-position') || {
|
||||||
|
|
@ -166,6 +170,9 @@ const twoColumnLayout = computed(() => {
|
||||||
// 收起/展开功能
|
// 收起/展开功能
|
||||||
function toggleCollapse() {
|
function toggleCollapse() {
|
||||||
isCollapsed.value = !isCollapsed.value
|
isCollapsed.value = !isCollapsed.value
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('toggle-collapse')
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择分数
|
// 选择分数
|
||||||
|
|
@ -274,7 +281,7 @@ async function submitCurrentScore() {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="size-16px text-blue-500 transition-transform"
|
class="size-16px text-blue-500 transition-transform"
|
||||||
:class="!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left'"
|
:class="settings.quickScorePosition === 'right' ? (!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left') : (!isCollapsed ? 'i-carbon-chevron-left' : 'i-carbon-chevron-right')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const settings = useMarkingSettings()
|
||||||
|
|
||||||
// 初始值保存
|
// 初始值保存
|
||||||
const initialSettings = ref({
|
const initialSettings = ref({
|
||||||
quickScoreMode: '',
|
quickScoreMode: 'onekey' as 'onekey' | 'quick',
|
||||||
stepSize: 0,
|
stepSize: 0,
|
||||||
quickScoreScores: [] as number[],
|
quickScoreScores: [] as number[],
|
||||||
onekeyScores: [] as number[],
|
onekeyScores: [] as number[],
|
||||||
|
|
@ -335,6 +335,27 @@ function resetQuickScoreScores() {
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 打分板位置设置 -->
|
||||||
|
<view class="mb-0.75em">
|
||||||
|
<text class="mb-0.375em block text-0.875em text-gray-700">打分板位置</text>
|
||||||
|
<view class="flex rounded-1 bg-gray-100 p-0.25em">
|
||||||
|
<view
|
||||||
|
class="flex-1 cursor-pointer rounded-1 py-0.5em text-center text-0.875em text-gray-600 transition-all"
|
||||||
|
:class="settings.quickScorePosition === 'left' ? 'bg-white text-blue-500 shadow-sm' : ''"
|
||||||
|
@click="settings.quickScorePosition = 'left'"
|
||||||
|
>
|
||||||
|
左侧
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="flex-1 cursor-pointer rounded-1 py-0.5em text-center text-0.875em text-gray-600 transition-all"
|
||||||
|
:class="settings.quickScorePosition === 'right' ? 'bg-white text-blue-500 shadow-sm' : ''"
|
||||||
|
@click="settings.quickScorePosition = 'right'"
|
||||||
|
>
|
||||||
|
右侧
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 一键打分 / 快捷打分 切换 -->
|
<!-- 一键打分 / 快捷打分 切换 -->
|
||||||
<view class="mb-0.75em flex rounded-1 bg-gray-100 p-0.25em">
|
<view class="mb-0.75em flex rounded-1 bg-gray-100 p-0.25em">
|
||||||
<view
|
<view
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useSimpleDomLayer } from '../../composables/renderer/useMarkingDom'
|
||||||
interface Props {
|
interface Props {
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
scale?: number
|
scale?: number
|
||||||
|
containerWidth?: number // 容器宽度,用于自适应缩放
|
||||||
markingData?: DomMarkingData
|
markingData?: DomMarkingData
|
||||||
currentTool: MarkingTool
|
currentTool: MarkingTool
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
|
@ -19,6 +20,7 @@ interface Props {
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
containerWidth: 0,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
scrollOffset: () => ({ left: 0, top: 0 }),
|
scrollOffset: () => ({ left: 0, top: 0 }),
|
||||||
})
|
})
|
||||||
|
|
@ -31,9 +33,27 @@ const emit = defineEmits<{
|
||||||
const naturalWidth = ref(0)
|
const naturalWidth = ref(0)
|
||||||
const naturalHeight = ref(0)
|
const naturalHeight = ref(0)
|
||||||
|
|
||||||
|
// 计算最终使用的缩放比例
|
||||||
|
const finalScale = computed(() => {
|
||||||
|
// 如果提供了 containerWidth,则根据图片宽度自动计算 scale
|
||||||
|
console.log('props.containerWidth', props.containerWidth)
|
||||||
|
console.log('naturalWidth.value', naturalWidth.value)
|
||||||
|
if (props.containerWidth > 0 && naturalWidth.value > 0) {
|
||||||
|
const calculatedScale = props.containerWidth / naturalWidth.value
|
||||||
|
console.log('[DomImageRenderer] Auto calculated scale:', {
|
||||||
|
containerWidth: props.containerWidth,
|
||||||
|
naturalWidth: naturalWidth.value,
|
||||||
|
calculatedScale,
|
||||||
|
})
|
||||||
|
return calculatedScale
|
||||||
|
}
|
||||||
|
// 否则使用传入的 scale
|
||||||
|
return props.scale
|
||||||
|
})
|
||||||
|
|
||||||
// 计算显示尺寸(应用缩放)
|
// 计算显示尺寸(应用缩放)
|
||||||
const displayWidth = computed(() => naturalWidth.value * props.scale)
|
const displayWidth = computed(() => naturalWidth.value * finalScale.value)
|
||||||
const displayHeight = computed(() => naturalHeight.value * props.scale)
|
const displayHeight = computed(() => naturalHeight.value * finalScale.value)
|
||||||
function encodeSvg(svg: string) {
|
function encodeSvg(svg: string) {
|
||||||
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
|
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
|
||||||
.replace(/"/g, '\'')
|
.replace(/"/g, '\'')
|
||||||
|
|
@ -127,7 +147,7 @@ function generateMarkSvg(type: 'correct' | 'wrong' | 'half'): string {
|
||||||
// Layer管理器
|
// Layer管理器
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const layerManager = useSimpleDomLayer({
|
const layerManager = useSimpleDomLayer({
|
||||||
scale: computed(() => props.scale),
|
scale: finalScale,
|
||||||
currentTool: computed(() => props.currentTool),
|
currentTool: computed(() => props.currentTool),
|
||||||
initialData: props.markingData,
|
initialData: props.markingData,
|
||||||
onQuickScore: (value: number) => props.onQuickScore?.(value),
|
onQuickScore: (value: number) => props.onQuickScore?.(value),
|
||||||
|
|
@ -261,7 +281,11 @@ function handleTouchEnd(e: TouchEvent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
layerManager.handleMouseUp()
|
layerManager.handleMouseUp(e, {
|
||||||
|
...cachedRect,
|
||||||
|
scrollLeft: props.scrollOffset.left,
|
||||||
|
scrollTop: props.scrollOffset.top,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -342,7 +366,7 @@ onMounted(async () => {
|
||||||
:style="{
|
:style="{
|
||||||
width: `${naturalWidth}px`,
|
width: `${naturalWidth}px`,
|
||||||
height: `${naturalHeight}px`,
|
height: `${naturalHeight}px`,
|
||||||
transform: `scale(${scale})`,
|
transform: `scale(${finalScale})`,
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}"
|
}"
|
||||||
|
|
@ -440,7 +464,7 @@ onMounted(async () => {
|
||||||
<view
|
<view
|
||||||
v-for="annotation in layerManager.markingData.value.annotations"
|
v-for="annotation in layerManager.markingData.value.annotations"
|
||||||
:key="annotation.id"
|
:key="annotation.id"
|
||||||
class="absolute font-bold flex items-center"
|
class="absolute flex items-center font-bold"
|
||||||
:style="{
|
:style="{
|
||||||
left: `${annotation.x}px`,
|
left: `${annotation.x}px`,
|
||||||
top: `${annotation.y}px`,
|
top: `${annotation.y}px`,
|
||||||
|
|
@ -454,7 +478,7 @@ onMounted(async () => {
|
||||||
<text>{{ annotation.text }}</text>
|
<text>{{ annotation.text }}</text>
|
||||||
<view
|
<view
|
||||||
v-if="annotation.scoreValue !== undefined && !readOnly"
|
v-if="annotation.scoreValue !== undefined && !readOnly"
|
||||||
class="flex items-center justify-center bg-red-500 text-white rounded-full"
|
class="flex items-center justify-center rounded-full bg-red-500 text-white"
|
||||||
:style="{
|
:style="{
|
||||||
width: `${annotation.fontSize * 0.8}px`,
|
width: `${annotation.fontSize * 0.8}px`,
|
||||||
height: `${annotation.fontSize * 0.8}px`,
|
height: `${annotation.fontSize * 0.8}px`,
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,15 @@ export function useSimpleDomLayer({
|
||||||
const isDrawing = ref(false)
|
const isDrawing = ref(false)
|
||||||
const currentDrawingShape = ref<DomShape | null>(null)
|
const currentDrawingShape = ref<DomShape | null>(null)
|
||||||
|
|
||||||
|
// 点击/触摸状态追踪(用于区分点击和拖拽)
|
||||||
|
const touchStartTime = ref(0)
|
||||||
|
const touchStartPos = ref<{ x: number, y: number } | null>(null)
|
||||||
|
const touchMoveDistance = ref(0)
|
||||||
|
const clickThreshold = {
|
||||||
|
maxMoveDistance: 10, // 最大移动距离(px),超过则视为拖拽
|
||||||
|
maxDuration: 300, // 最大时长(ms),超过则不触发快捷操作
|
||||||
|
}
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const hasMarks = computed(() => {
|
const hasMarks = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -138,9 +147,13 @@ export function useSimpleDomLayer({
|
||||||
if (!pos)
|
if (!pos)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 快捷打分点击模式
|
// 记录按下时间和位置
|
||||||
|
touchStartTime.value = Date.now()
|
||||||
|
touchStartPos.value = { x: pos.x, y: pos.y }
|
||||||
|
touchMoveDistance.value = 0
|
||||||
|
|
||||||
|
// 快捷打分点击模式 - 不在这里处理,移到 handleMouseUp
|
||||||
if (markingSettings.value.quickScoreMode === 'quick' && currentTool.value === MarkingTool.SELECT) {
|
if (markingSettings.value.quickScoreMode === 'quick' && currentTool.value === MarkingTool.SELECT) {
|
||||||
handleQuickScoreClick(pos)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,16 +169,10 @@ export function useSimpleDomLayer({
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
break
|
break
|
||||||
case MarkingTool.CORRECT:
|
case MarkingTool.CORRECT:
|
||||||
addSpecialMark('correct', pos)
|
|
||||||
break
|
|
||||||
case MarkingTool.WRONG:
|
case MarkingTool.WRONG:
|
||||||
addSpecialMark('wrong', pos)
|
|
||||||
break
|
|
||||||
case MarkingTool.HALF:
|
case MarkingTool.HALF:
|
||||||
addSpecialMark('half', pos)
|
|
||||||
break
|
|
||||||
case MarkingTool.TEXT:
|
case MarkingTool.TEXT:
|
||||||
addTextAnnotation(pos)
|
// 这些操作移到 handleMouseUp 中处理
|
||||||
break
|
break
|
||||||
case MarkingTool.ERASER:
|
case MarkingTool.ERASER:
|
||||||
eraseAt(pos)
|
eraseAt(pos)
|
||||||
|
|
@ -177,7 +184,7 @@ export function useSimpleDomLayer({
|
||||||
* 处理鼠标移动事件
|
* 处理鼠标移动事件
|
||||||
*/
|
*/
|
||||||
const handleMouseMove = (e: MouseEvent | TouchEvent, containerRect: DOMRect & { scrollLeft?: number, scrollTop?: number }) => {
|
const handleMouseMove = (e: MouseEvent | TouchEvent, containerRect: DOMRect & { scrollLeft?: number, scrollTop?: number }) => {
|
||||||
if (!isDrawing.value || readOnly)
|
if (readOnly)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 检测双指手势,直接返回不处理(让全局缩放处理)
|
// 检测双指手势,直接返回不处理(让全局缩放处理)
|
||||||
|
|
@ -190,6 +197,17 @@ export function useSimpleDomLayer({
|
||||||
if (!pos)
|
if (!pos)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// 计算移动距离
|
||||||
|
if (touchStartPos.value) {
|
||||||
|
const dx = pos.x - touchStartPos.value.x
|
||||||
|
const dy = pos.y - touchStartPos.value.y
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
touchMoveDistance.value = Math.max(touchMoveDistance.value, distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDrawing.value)
|
||||||
|
return
|
||||||
|
|
||||||
if (currentTool.value === MarkingTool.PEN) {
|
if (currentTool.value === MarkingTool.PEN) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
continuePenDrawing(pos)
|
continuePenDrawing(pos)
|
||||||
|
|
@ -207,7 +225,56 @@ export function useSimpleDomLayer({
|
||||||
/**
|
/**
|
||||||
* 处理鼠标抬起事件
|
* 处理鼠标抬起事件
|
||||||
*/
|
*/
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = (e: MouseEvent | TouchEvent, containerRect: DOMRect & { scrollLeft?: number, scrollTop?: number }) => {
|
||||||
|
const endTime = Date.now()
|
||||||
|
const duration = endTime - touchStartTime.value
|
||||||
|
const moveDistance = touchMoveDistance.value
|
||||||
|
|
||||||
|
// 判断是否为有效点击(移动距离小且时长短)
|
||||||
|
const isValidClick = moveDistance < clickThreshold.maxMoveDistance && duration < clickThreshold.maxDuration
|
||||||
|
|
||||||
|
console.log('[SimpleDomLayer] handleMouseUp', {
|
||||||
|
duration,
|
||||||
|
moveDistance,
|
||||||
|
isValidClick,
|
||||||
|
currentTool: currentTool.value,
|
||||||
|
isDrawing: isDrawing.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理快捷打分(SELECT工具)
|
||||||
|
if (isValidClick && markingSettings.value.quickScoreMode === 'quick' && currentTool.value === MarkingTool.SELECT) {
|
||||||
|
const pos = getRelativePosition(e, containerRect)
|
||||||
|
if (pos) {
|
||||||
|
handleQuickScoreClick(pos)
|
||||||
|
}
|
||||||
|
isDrawing.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理记号和文本标注工具的点击操作
|
||||||
|
if (isValidClick && touchStartPos.value) {
|
||||||
|
const pos = touchStartPos.value
|
||||||
|
switch (currentTool.value) {
|
||||||
|
case MarkingTool.CORRECT:
|
||||||
|
addSpecialMark('correct', pos)
|
||||||
|
isDrawing.value = false
|
||||||
|
return
|
||||||
|
case MarkingTool.WRONG:
|
||||||
|
addSpecialMark('wrong', pos)
|
||||||
|
isDrawing.value = false
|
||||||
|
return
|
||||||
|
case MarkingTool.HALF:
|
||||||
|
addSpecialMark('half', pos)
|
||||||
|
isDrawing.value = false
|
||||||
|
return
|
||||||
|
case MarkingTool.TEXT:
|
||||||
|
addTextAnnotation(pos)
|
||||||
|
isDrawing.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理绘制工具的完成
|
||||||
if (!isDrawing.value)
|
if (!isDrawing.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ export interface MarkingSettings {
|
||||||
quickScoreScores: number[]
|
quickScoreScores: number[]
|
||||||
quickScoreMode: 'onekey' | 'quick'
|
quickScoreMode: 'onekey' | 'quick'
|
||||||
quickScoreLayout: 'single' | 'double'
|
quickScoreLayout: 'single' | 'double'
|
||||||
|
// 打分板位置
|
||||||
|
quickScorePosition: 'left' | 'right'
|
||||||
|
|
||||||
// 一键打分配置:key 为 "stepSize_fullScore",value 为排序后的分数数组
|
// 一键打分配置:key 为 "stepSize_fullScore",value 为排序后的分数数组
|
||||||
onekeyScoreConfigs: Record<string, number[]>
|
onekeyScoreConfigs: Record<string, number[]>
|
||||||
|
|
@ -90,6 +92,7 @@ const defaultSettings: MarkingSettings = {
|
||||||
quickScoreMode: 'onekey',
|
quickScoreMode: 'onekey',
|
||||||
quickScoreScores: [0.5, 1, 2, 3],
|
quickScoreScores: [0.5, 1, 2, 3],
|
||||||
quickScoreLayout: 'single',
|
quickScoreLayout: 'single',
|
||||||
|
quickScorePosition: 'right',
|
||||||
|
|
||||||
// 一键打分默认值
|
// 一键打分默认值
|
||||||
onekeyScoreConfigs: {},
|
onekeyScoreConfigs: {},
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
// 筛选条件
|
// 筛选条件
|
||||||
const selectedScore = ref<number | undefined>()
|
const selectedScore = ref<number | undefined>()
|
||||||
const selectedOrderBy = ref<number>(1) // 默认按打分从小到大
|
const selectedOrderBy = ref<number>(4) // 默认按打分从小到大
|
||||||
|
|
||||||
// 排序选项
|
// 排序选项
|
||||||
const orderOptions = [
|
const orderOptions = [
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ interface Props {
|
||||||
markingData?: string
|
markingData?: string
|
||||||
score?: number
|
score?: number
|
||||||
fullScore?: number
|
fullScore?: number
|
||||||
scale?: number
|
containerWidth?: number // 容器宽度,用于自适应缩放
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
scale: 0.5, // 默认缩小显示
|
containerWidth: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理标记数据,确保每个图片都有对应的数据
|
// 处理标记数据,确保每个图片都有对应的数据
|
||||||
|
|
@ -74,11 +74,10 @@ function handleLayerReady() {
|
||||||
<view class="image-wrapper relative overflow-hidden rounded-2 shadow-sm">
|
<view class="image-wrapper relative overflow-hidden rounded-2 shadow-sm">
|
||||||
<DomImageRenderer
|
<DomImageRenderer
|
||||||
:image-url="imageUrl"
|
:image-url="imageUrl"
|
||||||
:scale="scale"
|
:container-width="containerWidth"
|
||||||
:marking-data="processedMarkingData[index]"
|
:marking-data="processedMarkingData[index]"
|
||||||
:current-tool="MarkingTool.SELECT"
|
:current-tool="MarkingTool.SELECT"
|
||||||
:read-only="true"
|
:read-only="true"
|
||||||
:adaptive-width="true"
|
|
||||||
@layer-ready="handleLayerReady"
|
@layer-ready="handleLayerReady"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,8 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
||||||
let startX = 0
|
let startX = 0
|
||||||
let startY = 0
|
let startY = 0
|
||||||
let startTime = 0
|
let startTime = 0
|
||||||
|
let lastSwipeTime = 0 // 上次触发手势的时间
|
||||||
|
const swipeThrottleMs = 400 // 手势节流时间:400ms
|
||||||
|
|
||||||
const onTouchStart = (event: TouchEvent) => {
|
const onTouchStart = (event: TouchEvent) => {
|
||||||
const touch = event.touches[0]
|
const touch = event.touches[0]
|
||||||
|
|
@ -171,6 +173,12 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
||||||
const deltaY = endY - startY
|
const deltaY = endY - startY
|
||||||
const deltaTime = endTime - startTime
|
const deltaTime = endTime - startTime
|
||||||
|
|
||||||
|
// 检查节流:距离上次触发是否超过400ms
|
||||||
|
if (endTime - lastSwipeTime < swipeThrottleMs) {
|
||||||
|
console.log('🤚 手势触发过于频繁,已忽略')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否超时
|
// 检查是否超时
|
||||||
if (deltaTime > swipeConfig.timeout) {
|
if (deltaTime > swipeConfig.timeout) {
|
||||||
return
|
return
|
||||||
|
|
@ -185,6 +193,9 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新最后触发时间
|
||||||
|
lastSwipeTime = endTime
|
||||||
|
|
||||||
// 判断滑动方向(优先水平方向)
|
// 判断滑动方向(优先水平方向)
|
||||||
if (absX > absY) {
|
if (absX > absY) {
|
||||||
// 水平滑动
|
// 水平滑动
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { whenever } from '@vueuse/core'
|
import { watchImmediate, whenever } from '@vueuse/core'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import AnswerDialog from '@/components/marking/components/dialog/AnswerDialog.vue'
|
import AnswerDialog from '@/components/marking/components/dialog/AnswerDialog.vue'
|
||||||
import AvgScoreDialog from '@/components/marking/components/dialog/AvgScoreDialog.vue'
|
import AvgScoreDialog from '@/components/marking/components/dialog/AvgScoreDialog.vue'
|
||||||
|
|
@ -289,6 +289,11 @@ onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
containerSize.value = await getContainerSize()
|
containerSize.value = await getContainerSize()
|
||||||
console.log('Container size:', containerSize.value)
|
console.log('Container size:', containerSize.value)
|
||||||
|
|
||||||
|
// 延迟1秒再次计算比例,确保图片铺满
|
||||||
|
setTimeout(() => {
|
||||||
|
recalculateScale()
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.warn('Failed to get container size:', error)
|
console.warn('Failed to get container size:', error)
|
||||||
|
|
@ -379,6 +384,10 @@ const currentQuestion = computed(() => {
|
||||||
return questions.value[0]
|
return questions.value[0]
|
||||||
})
|
})
|
||||||
const currentTask = computed(() => currentQuestions.value?.tasks?.[taskType.value])
|
const currentTask = computed(() => currentQuestions.value?.tasks?.[taskType.value])
|
||||||
|
const fullScore = ref(0)
|
||||||
|
watchImmediate(currentQuestion, () => {
|
||||||
|
fullScore.value = currentQuestion.value?.full_score || 0
|
||||||
|
})
|
||||||
|
|
||||||
// 当前答题卡图片列表
|
// 当前答题卡图片列表
|
||||||
const currentImageUrls = computed(() => currentQuestion.value?.image_urls || [])
|
const currentImageUrls = computed(() => currentQuestion.value?.image_urls || [])
|
||||||
|
|
@ -608,10 +617,10 @@ const nextQuestionImages = computed(() => {
|
||||||
<!-- 打分区域(横屏和竖屏共用) -->
|
<!-- 打分区域(横屏和竖屏共用) -->
|
||||||
<template #scoring>
|
<template #scoring>
|
||||||
<QuickScorePanel
|
<QuickScorePanel
|
||||||
v-if="currentQuestion"
|
|
||||||
:is-landscape="isLandscape"
|
:is-landscape="isLandscape"
|
||||||
:full-score="currentQuestion.full_score"
|
:full-score="fullScore"
|
||||||
@score-selected="handleQuickScoreSelect"
|
@score-selected="handleQuickScoreSelect"
|
||||||
|
@toggle-collapse="recalculateScale"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</MarkingLayout>
|
</MarkingLayout>
|
||||||
|
|
@ -638,7 +647,7 @@ const nextQuestionImages = computed(() => {
|
||||||
v-if="currentQuestion"
|
v-if="currentQuestion"
|
||||||
v-model="showAnswer"
|
v-model="showAnswer"
|
||||||
:question-title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
|
:question-title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
|
||||||
:full-score="currentQuestion?.full_score"
|
:full-score="fullScore"
|
||||||
:standard-answer="currentQuestion?.standard_answer || ''"
|
:standard-answer="currentQuestion?.standard_answer || ''"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
</route>
|
</route>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useInfiniteQuery, useQuery } from '@tanstack/vue-query'
|
||||||
|
import { whenever } from '@vueuse/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { examMarkingTaskApi } from '@/api'
|
import { examMarkingTaskApi } from '@/api'
|
||||||
|
|
@ -55,46 +56,65 @@ const {
|
||||||
select: data => data?.history_scores || [],
|
select: data => data?.history_scores || [],
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取历史记录数据
|
// 每页数量
|
||||||
const currentPage = ref(1)
|
const pageSize = 20
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
|
// 使用 useInfiniteQuery 获取历史记录数据
|
||||||
const {
|
const {
|
||||||
data: historyData,
|
data: historyData,
|
||||||
isLoading: isLoadingHistory,
|
isLoading: isLoadingHistory,
|
||||||
error: historyError,
|
error: historyError,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
refetch: refetchHistory,
|
refetch: refetchHistory,
|
||||||
} = useQuery({
|
} = useInfiniteQuery({
|
||||||
queryKey: ['review-history', taskId, currentPage, selectedScore, selectedOrderBy, useUserId()],
|
queryKey: ['review-history-infinite', taskId, selectedScore, selectedOrderBy, useUserId()],
|
||||||
queryFn: () => examMarkingTaskApi.historyDetail(
|
queryFn: ({ pageParam = 1 }) => examMarkingTaskApi.historyDetail(
|
||||||
taskId.value!,
|
taskId.value!,
|
||||||
{
|
{
|
||||||
page: currentPage.value,
|
page: pageParam,
|
||||||
page_size: pageSize.value,
|
page_size: pageSize,
|
||||||
is_review: true,
|
is_review: true,
|
||||||
history_score: selectedScore.value,
|
history_score: selectedScore.value,
|
||||||
order_by: selectedOrderBy.value,
|
order_by: selectedOrderBy.value,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
enabled: enableQuery,
|
enabled: enableQuery,
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||||
|
// 如果还有更多数据,返回下一页页码
|
||||||
|
if (lastPage?.has_more) {
|
||||||
|
return lastPageParam + 1
|
||||||
|
}
|
||||||
|
// 没有更多数据时返回 undefined
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 历史记录列表
|
// 历史记录列表 - 合并所有页面的数据
|
||||||
const historyList = computed(() => historyData.value?.list || [])
|
const historyList = computed(() => {
|
||||||
const totalCount = computed(() => historyData.value?.total || 0)
|
if (!historyData.value?.pages) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return historyData.value.pages.flatMap(page => page?.list || [])
|
||||||
|
})
|
||||||
|
|
||||||
|
// 总数(从第一页获取)
|
||||||
|
const totalCount = computed(() => historyData.value?.pages?.[0]?.total || 0)
|
||||||
|
|
||||||
// 处理分数筛选变化
|
// 处理分数筛选变化
|
||||||
function handleScoreChange(score?: number) {
|
function handleScoreChange(score?: number) {
|
||||||
console.log('分数筛选变化:', score)
|
console.log('分数筛选变化:', score)
|
||||||
selectedScore.value = score
|
selectedScore.value = score
|
||||||
currentPage.value = 1 // 重置到第一页
|
// 筛选变化时,useInfiniteQuery 会自动重置到第一页
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理排序变化
|
// 处理排序变化
|
||||||
function handleOrderChange(orderBy: number) {
|
function handleOrderChange(orderBy: number) {
|
||||||
console.log('排序变化:', orderBy)
|
console.log('排序变化:', orderBy)
|
||||||
selectedOrderBy.value = orderBy
|
selectedOrderBy.value = orderBy
|
||||||
currentPage.value = 1 // 重置到第一页
|
// 排序变化时,useInfiniteQuery 会自动重置到第一页
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
|
|
@ -105,6 +125,23 @@ function goBack() {
|
||||||
// 分数编辑对话框
|
// 分数编辑对话框
|
||||||
const scoreEditDialogRef = ref<InstanceType<typeof ScoreEditDialog>>()
|
const scoreEditDialogRef = ref<InstanceType<typeof ScoreEditDialog>>()
|
||||||
|
|
||||||
|
// 容器宽度
|
||||||
|
const containerWidth = ref(0)
|
||||||
|
|
||||||
|
// 获取容器宽度
|
||||||
|
whenever(() => historyData.value?.pages?.length, () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const query = uni.createSelectorQuery()
|
||||||
|
query.select('.image-container').boundingClientRect((data) => {
|
||||||
|
if (data && !Array.isArray(data)) {
|
||||||
|
// 减去padding和边框,获取实际可用宽度
|
||||||
|
containerWidth.value = data.width - 32 // 32 = padding 16px * 2
|
||||||
|
console.log('[Review] Container width:', containerWidth.value)
|
||||||
|
}
|
||||||
|
}).exec()
|
||||||
|
}, 100)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// 处理图片点击编辑分数
|
// 处理图片点击编辑分数
|
||||||
async function handleImageClick(record: any) {
|
async function handleImageClick(record: any) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -131,14 +168,13 @@ async function handleImageClick(record: any) {
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
if (historyData.value?.has_more) {
|
if (hasNextPage.value && !isFetchingNextPage.value) {
|
||||||
currentPage.value += 1
|
fetchNextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
function handleRefresh() {
|
function handleRefresh() {
|
||||||
currentPage.value = 1
|
|
||||||
refetchHistory()
|
refetchHistory()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -174,7 +210,7 @@ function handleRefresh() {
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 历史记录列表 -->
|
<!-- 历史记录列表 -->
|
||||||
<view v-else-if="historyList.length > 0" class="history-list">
|
<view v-else-if="historyList.length > 0" :key="containerWidth" class="history-list">
|
||||||
<view
|
<view
|
||||||
v-for="record in historyList"
|
v-for="record in historyList"
|
||||||
:key="record.id"
|
:key="record.id"
|
||||||
|
|
@ -200,6 +236,7 @@ function handleRefresh() {
|
||||||
:marking-data="record.remark"
|
:marking-data="record.remark"
|
||||||
:score="record.score"
|
:score="record.score"
|
||||||
:full-score="record.full_score"
|
:full-score="record.full_score"
|
||||||
|
:container-width="containerWidth"
|
||||||
/>
|
/>
|
||||||
<view v-else class="no-image flex-center h-32 rounded-2 bg-gray-100">
|
<view v-else class="no-image flex-center h-32 rounded-2 bg-gray-100">
|
||||||
<text class="text-gray-400">暂无图片</text>
|
<text class="text-gray-400">暂无图片</text>
|
||||||
|
|
@ -208,14 +245,16 @@ function handleRefresh() {
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
<!-- 加载更多 -->
|
||||||
<view v-if="historyData?.has_more" class="load-more py-4">
|
<view v-if="hasNextPage" class="load-more py-4">
|
||||||
<wd-button
|
<wd-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
block
|
block
|
||||||
|
:loading="isFetchingNextPage"
|
||||||
|
:disabled="isFetchingNextPage"
|
||||||
@click="loadMore"
|
@click="loadMore"
|
||||||
>
|
>
|
||||||
加载更多
|
{{ isFetchingNextPage ? '加载中...' : '加载更多' }}
|
||||||
</wd-button>
|
</wd-button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue