refactor: 一些优化
continuous-integration/drone/push Build is passing Details

This commit is contained in:
AfyerCu 2025-11-20 22:28:47 +08:00
parent 9d100efbf4
commit 2ed92d3f7a
11 changed files with 259 additions and 57 deletions

View File

@ -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">

View File

@ -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>

View File

@ -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

View File

@ -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`,

View File

@ -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

View File

@ -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: {},

View File

@ -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 = [

View File

@ -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"
/> />

View File

@ -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) {
// 水平滑动 // 水平滑动

View File

@ -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 || ''"
/> />

View File

@ -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>