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>
|
||||
import type { ExamQuestionWithTasksResponse } from '@/api'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useMarkingSettings } from '@/components/marking/composables/useMarkingSettings'
|
||||
import { useMarkingHistory } from '@/composables/marking/useMarkingHistory'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -49,6 +50,7 @@ const emit = defineEmits<Emits>()
|
|||
|
||||
// 获取历史记录管理
|
||||
const markingHistory = useMarkingHistory()
|
||||
const settings = useMarkingSettings()
|
||||
const currentHistoryPage = ref(1)
|
||||
const { data: historyPageData, isLoading: isLoadingHistory } = markingHistory.useHistoryPage(
|
||||
computed(() => currentHistoryPage.value),
|
||||
|
|
@ -129,14 +131,29 @@ const currentHistoryIndexInList = computed(() => {
|
|||
</view>
|
||||
|
||||
<!-- 中间题号信息 -->
|
||||
<view class="flex flex-1 flex-col justify-center">
|
||||
<view class="flex flex-1 items-center justify-center gap-4">
|
||||
<!-- 题目选择器 -->
|
||||
<wd-picker
|
||||
: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>
|
||||
<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-18px text-slate-800 font-bold leading-none">
|
||||
<text class="text-16px text-slate-600 font-medium leading-none">
|
||||
{{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }}
|
||||
</text>
|
||||
<text class="text-12px text-slate-400 leading-none">
|
||||
题号
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -195,12 +212,17 @@ const currentHistoryIndexInList = computed(() => {
|
|||
</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" />
|
||||
</view>
|
||||
|
||||
<!-- 右侧打分区域 -->
|
||||
<slot name="scoring" />
|
||||
<view :class="settings.quickScorePosition === 'left' ? 'order-2' : 'order-3'">
|
||||
<slot name="scoring" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface Props {
|
|||
|
||||
interface Emits {
|
||||
(e: 'score-selected', score: number): void
|
||||
(e: 'toggle-collapse'): void
|
||||
}
|
||||
|
||||
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 持久化)
|
||||
const floatingButtonPosition = ref(uni.getStorageSync('quick-score-submit-button-position') || {
|
||||
|
|
@ -166,6 +170,9 @@ const twoColumnLayout = computed(() => {
|
|||
// 收起/展开功能
|
||||
function toggleCollapse() {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
setTimeout(() => {
|
||||
emit('toggle-collapse')
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 选择分数
|
||||
|
|
@ -274,7 +281,7 @@ async function submitCurrentScore() {
|
|||
>
|
||||
<div
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const settings = useMarkingSettings()
|
|||
|
||||
// 初始值保存
|
||||
const initialSettings = ref({
|
||||
quickScoreMode: '',
|
||||
quickScoreMode: 'onekey' as 'onekey' | 'quick',
|
||||
stepSize: 0,
|
||||
quickScoreScores: [] as number[],
|
||||
onekeyScores: [] as number[],
|
||||
|
|
@ -335,6 +335,27 @@ function resetQuickScoreScores() {
|
|||
/>
|
||||
</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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useSimpleDomLayer } from '../../composables/renderer/useMarkingDom'
|
|||
interface Props {
|
||||
imageUrl: string
|
||||
scale?: number
|
||||
containerWidth?: number // 容器宽度,用于自适应缩放
|
||||
markingData?: DomMarkingData
|
||||
currentTool: MarkingTool
|
||||
readOnly?: boolean
|
||||
|
|
@ -19,6 +20,7 @@ interface Props {
|
|||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
containerWidth: 0,
|
||||
readOnly: false,
|
||||
scrollOffset: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
|
@ -31,9 +33,27 @@ const emit = defineEmits<{
|
|||
const naturalWidth = 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 displayHeight = computed(() => naturalHeight.value * props.scale)
|
||||
const displayWidth = computed(() => naturalWidth.value * finalScale.value)
|
||||
const displayHeight = computed(() => naturalHeight.value * finalScale.value)
|
||||
function encodeSvg(svg: string) {
|
||||
return svg.replace('<svg', (~svg.indexOf('xmlns') ? '<svg' : '<svg xmlns="http://www.w3.org/2000/svg"'))
|
||||
.replace(/"/g, '\'')
|
||||
|
|
@ -127,7 +147,7 @@ function generateMarkSvg(type: 'correct' | 'wrong' | 'half'): string {
|
|||
// Layer管理器
|
||||
const message = useMessage()
|
||||
const layerManager = useSimpleDomLayer({
|
||||
scale: computed(() => props.scale),
|
||||
scale: finalScale,
|
||||
currentTool: computed(() => props.currentTool),
|
||||
initialData: props.markingData,
|
||||
onQuickScore: (value: number) => props.onQuickScore?.(value),
|
||||
|
|
@ -261,7 +281,11 @@ function handleTouchEnd(e: TouchEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
layerManager.handleMouseUp()
|
||||
layerManager.handleMouseUp(e, {
|
||||
...cachedRect,
|
||||
scrollLeft: props.scrollOffset.left,
|
||||
scrollTop: props.scrollOffset.top,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -342,7 +366,7 @@ onMounted(async () => {
|
|||
:style="{
|
||||
width: `${naturalWidth}px`,
|
||||
height: `${naturalHeight}px`,
|
||||
transform: `scale(${scale})`,
|
||||
transform: `scale(${finalScale})`,
|
||||
transformOrigin: 'top left',
|
||||
pointerEvents: 'none',
|
||||
}"
|
||||
|
|
@ -440,7 +464,7 @@ onMounted(async () => {
|
|||
<view
|
||||
v-for="annotation in layerManager.markingData.value.annotations"
|
||||
:key="annotation.id"
|
||||
class="absolute font-bold flex items-center"
|
||||
class="absolute flex items-center font-bold"
|
||||
:style="{
|
||||
left: `${annotation.x}px`,
|
||||
top: `${annotation.y}px`,
|
||||
|
|
@ -454,7 +478,7 @@ onMounted(async () => {
|
|||
<text>{{ annotation.text }}</text>
|
||||
<view
|
||||
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="{
|
||||
width: `${annotation.fontSize * 0.8}px`,
|
||||
height: `${annotation.fontSize * 0.8}px`,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,15 @@ export function useSimpleDomLayer({
|
|||
const isDrawing = ref(false)
|
||||
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(() => {
|
||||
return (
|
||||
|
|
@ -138,9 +147,13 @@ export function useSimpleDomLayer({
|
|||
if (!pos)
|
||||
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) {
|
||||
handleQuickScoreClick(pos)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -156,16 +169,10 @@ export function useSimpleDomLayer({
|
|||
e.preventDefault()
|
||||
break
|
||||
case MarkingTool.CORRECT:
|
||||
addSpecialMark('correct', pos)
|
||||
break
|
||||
case MarkingTool.WRONG:
|
||||
addSpecialMark('wrong', pos)
|
||||
break
|
||||
case MarkingTool.HALF:
|
||||
addSpecialMark('half', pos)
|
||||
break
|
||||
case MarkingTool.TEXT:
|
||||
addTextAnnotation(pos)
|
||||
// 这些操作移到 handleMouseUp 中处理
|
||||
break
|
||||
case MarkingTool.ERASER:
|
||||
eraseAt(pos)
|
||||
|
|
@ -177,7 +184,7 @@ export function useSimpleDomLayer({
|
|||
* 处理鼠标移动事件
|
||||
*/
|
||||
const handleMouseMove = (e: MouseEvent | TouchEvent, containerRect: DOMRect & { scrollLeft?: number, scrollTop?: number }) => {
|
||||
if (!isDrawing.value || readOnly)
|
||||
if (readOnly)
|
||||
return
|
||||
|
||||
// 检测双指手势,直接返回不处理(让全局缩放处理)
|
||||
|
|
@ -190,6 +197,17 @@ export function useSimpleDomLayer({
|
|||
if (!pos)
|
||||
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) {
|
||||
e.preventDefault()
|
||||
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)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export interface MarkingSettings {
|
|||
quickScoreScores: number[]
|
||||
quickScoreMode: 'onekey' | 'quick'
|
||||
quickScoreLayout: 'single' | 'double'
|
||||
// 打分板位置
|
||||
quickScorePosition: 'left' | 'right'
|
||||
|
||||
// 一键打分配置:key 为 "stepSize_fullScore",value 为排序后的分数数组
|
||||
onekeyScoreConfigs: Record<string, number[]>
|
||||
|
|
@ -90,6 +92,7 @@ const defaultSettings: MarkingSettings = {
|
|||
quickScoreMode: 'onekey',
|
||||
quickScoreScores: [0.5, 1, 2, 3],
|
||||
quickScoreLayout: 'single',
|
||||
quickScorePosition: 'right',
|
||||
|
||||
// 一键打分默认值
|
||||
onekeyScoreConfigs: {},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const emit = defineEmits<{
|
|||
|
||||
// 筛选条件
|
||||
const selectedScore = ref<number | undefined>()
|
||||
const selectedOrderBy = ref<number>(1) // 默认按打分从小到大
|
||||
const selectedOrderBy = ref<number>(4) // 默认按打分从小到大
|
||||
|
||||
// 排序选项
|
||||
const orderOptions = [
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ interface Props {
|
|||
markingData?: string
|
||||
score?: number
|
||||
fullScore?: number
|
||||
scale?: number
|
||||
containerWidth?: number // 容器宽度,用于自适应缩放
|
||||
}
|
||||
|
||||
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">
|
||||
<DomImageRenderer
|
||||
:image-url="imageUrl"
|
||||
:scale="scale"
|
||||
:container-width="containerWidth"
|
||||
:marking-data="processedMarkingData[index]"
|
||||
:current-tool="MarkingTool.SELECT"
|
||||
:read-only="true"
|
||||
:adaptive-width="true"
|
||||
@layer-ready="handleLayerReady"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
|||
let startX = 0
|
||||
let startY = 0
|
||||
let startTime = 0
|
||||
let lastSwipeTime = 0 // 上次触发手势的时间
|
||||
const swipeThrottleMs = 400 // 手势节流时间:400ms
|
||||
|
||||
const onTouchStart = (event: TouchEvent) => {
|
||||
const touch = event.touches[0]
|
||||
|
|
@ -171,6 +173,12 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
|||
const deltaY = endY - startY
|
||||
const deltaTime = endTime - startTime
|
||||
|
||||
// 检查节流:距离上次触发是否超过400ms
|
||||
if (endTime - lastSwipeTime < swipeThrottleMs) {
|
||||
console.log('🤚 手势触发过于频繁,已忽略')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
if (deltaTime > swipeConfig.timeout) {
|
||||
return
|
||||
|
|
@ -185,6 +193,9 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
|||
return
|
||||
}
|
||||
|
||||
// 更新最后触发时间
|
||||
lastSwipeTime = endTime
|
||||
|
||||
// 判断滑动方向(优先水平方向)
|
||||
if (absX > absY) {
|
||||
// 水平滑动
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
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 AnswerDialog from '@/components/marking/components/dialog/AnswerDialog.vue'
|
||||
import AvgScoreDialog from '@/components/marking/components/dialog/AvgScoreDialog.vue'
|
||||
|
|
@ -289,6 +289,11 @@ onMounted(async () => {
|
|||
try {
|
||||
containerSize.value = await getContainerSize()
|
||||
console.log('Container size:', containerSize.value)
|
||||
|
||||
// 延迟1秒再次计算比例,确保图片铺满
|
||||
setTimeout(() => {
|
||||
recalculateScale()
|
||||
}, 1000)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to get container size:', error)
|
||||
|
|
@ -379,6 +384,10 @@ const currentQuestion = computed(() => {
|
|||
return questions.value[0]
|
||||
})
|
||||
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 || [])
|
||||
|
|
@ -608,10 +617,10 @@ const nextQuestionImages = computed(() => {
|
|||
<!-- 打分区域(横屏和竖屏共用) -->
|
||||
<template #scoring>
|
||||
<QuickScorePanel
|
||||
v-if="currentQuestion"
|
||||
:is-landscape="isLandscape"
|
||||
:full-score="currentQuestion.full_score"
|
||||
:full-score="fullScore"
|
||||
@score-selected="handleQuickScoreSelect"
|
||||
@toggle-collapse="recalculateScale"
|
||||
/>
|
||||
</template>
|
||||
</MarkingLayout>
|
||||
|
|
@ -638,7 +647,7 @@ const nextQuestionImages = computed(() => {
|
|||
v-if="currentQuestion"
|
||||
v-model="showAnswer"
|
||||
:question-title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
|
||||
:full-score="currentQuestion?.full_score"
|
||||
:full-score="fullScore"
|
||||
:standard-answer="currentQuestion?.standard_answer || ''"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
</route>
|
||||
|
||||
<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 { computed, ref } from 'vue'
|
||||
import { examMarkingTaskApi } from '@/api'
|
||||
|
|
@ -55,46 +56,65 @@ const {
|
|||
select: data => data?.history_scores || [],
|
||||
})
|
||||
|
||||
// 获取历史记录数据
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
// 每页数量
|
||||
const pageSize = 20
|
||||
|
||||
// 使用 useInfiniteQuery 获取历史记录数据
|
||||
const {
|
||||
data: historyData,
|
||||
isLoading: isLoadingHistory,
|
||||
error: historyError,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
refetch: refetchHistory,
|
||||
} = useQuery({
|
||||
queryKey: ['review-history', taskId, currentPage, selectedScore, selectedOrderBy, useUserId()],
|
||||
queryFn: () => examMarkingTaskApi.historyDetail(
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['review-history-infinite', taskId, selectedScore, selectedOrderBy, useUserId()],
|
||||
queryFn: ({ pageParam = 1 }) => examMarkingTaskApi.historyDetail(
|
||||
taskId.value!,
|
||||
{
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
page: pageParam,
|
||||
page_size: pageSize,
|
||||
is_review: true,
|
||||
history_score: selectedScore.value,
|
||||
order_by: selectedOrderBy.value,
|
||||
},
|
||||
),
|
||||
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 totalCount = computed(() => historyData.value?.total || 0)
|
||||
// 历史记录列表 - 合并所有页面的数据
|
||||
const historyList = computed(() => {
|
||||
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) {
|
||||
console.log('分数筛选变化:', score)
|
||||
selectedScore.value = score
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
// 筛选变化时,useInfiniteQuery 会自动重置到第一页
|
||||
}
|
||||
|
||||
// 处理排序变化
|
||||
function handleOrderChange(orderBy: number) {
|
||||
console.log('排序变化:', orderBy)
|
||||
selectedOrderBy.value = orderBy
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
// 排序变化时,useInfiniteQuery 会自动重置到第一页
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
|
|
@ -105,6 +125,23 @@ function goBack() {
|
|||
// 分数编辑对话框
|
||||
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) {
|
||||
try {
|
||||
|
|
@ -131,14 +168,13 @@ async function handleImageClick(record: any) {
|
|||
|
||||
// 加载更多
|
||||
function loadMore() {
|
||||
if (historyData.value?.has_more) {
|
||||
currentPage.value += 1
|
||||
if (hasNextPage.value && !isFetchingNextPage.value) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
function handleRefresh() {
|
||||
currentPage.value = 1
|
||||
refetchHistory()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -174,7 +210,7 @@ function handleRefresh() {
|
|||
</view>
|
||||
|
||||
<!-- 历史记录列表 -->
|
||||
<view v-else-if="historyList.length > 0" class="history-list">
|
||||
<view v-else-if="historyList.length > 0" :key="containerWidth" class="history-list">
|
||||
<view
|
||||
v-for="record in historyList"
|
||||
:key="record.id"
|
||||
|
|
@ -200,6 +236,7 @@ function handleRefresh() {
|
|||
:marking-data="record.remark"
|
||||
:score="record.score"
|
||||
:full-score="record.full_score"
|
||||
:container-width="containerWidth"
|
||||
/>
|
||||
<view v-else class="no-image flex-center h-32 rounded-2 bg-gray-100">
|
||||
<text class="text-gray-400">暂无图片</text>
|
||||
|
|
@ -208,14 +245,16 @@ function handleRefresh() {
|
|||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="historyData?.has_more" class="load-more py-4">
|
||||
<view v-if="hasNextPage" class="load-more py-4">
|
||||
<wd-button
|
||||
type="primary"
|
||||
size="small"
|
||||
block
|
||||
:loading="isFetchingNextPage"
|
||||
:disabled="isFetchingNextPage"
|
||||
@click="loadMore"
|
||||
>
|
||||
加载更多
|
||||
{{ isFetchingNextPage ? '加载中...' : '加载更多' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue