refactor: 优化阅卷
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
8504c031f6
commit
9e81aa4bec
|
|
@ -117,183 +117,171 @@ const currentHistoryIndexInList = computed(() => {
|
|||
<!-- 横屏布局 -->
|
||||
<view v-if="isLandscape" class="h-100vh w-100vw flex flex-grow flex-col bg-gray-100">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="h-48px flex items-center border-b bg-white pr-16px shadow-sm">
|
||||
<view class="z-10 h-56px flex flex-shrink-0 items-center border-b border-slate-100 bg-white px-16px shadow-sm">
|
||||
<!-- 左侧返回按钮 -->
|
||||
<view class="w-60px flex items-center justify-center border-r">
|
||||
<view class="mr-16px flex items-center justify-center">
|
||||
<view
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
class="size-36px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 transition-all active:scale-95 active:bg-slate-200 hover:bg-slate-100"
|
||||
@click="emit('goBack')"
|
||||
>
|
||||
<view class="i-carbon-arrow-left size-20px text-gray-700" />
|
||||
<view class="i-carbon-arrow-left size-20px" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧导航栏 -->
|
||||
<view class="flex flex-1 items-center justify-between pl-16px">
|
||||
<!-- 题号信息 -->
|
||||
<view class="mr-16px flex items-center">
|
||||
<text class="text-16px text-gray-800 font-medium">
|
||||
<!-- 中间题号信息 -->
|
||||
<view class="flex flex-1 flex-col justify-center">
|
||||
<view class="flex items-baseline gap-2">
|
||||
<text class="text-18px text-slate-800 font-bold leading-none">
|
||||
{{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }}
|
||||
</text>
|
||||
<text class="text-12px text-slate-400 leading-none">
|
||||
题号
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧操作按钮组 -->
|
||||
<view class="flex items-center gap-12px">
|
||||
<view
|
||||
v-for="(btn, idx) in [
|
||||
{ text: '设置', icon: 'i-carbon-settings', action: 'openScoreSettings' },
|
||||
{ text: '均分', icon: 'i-carbon-chart-bar', action: 'viewAvgScore' },
|
||||
{ text: '答案', icon: 'i-carbon-document', action: 'viewAnswer' },
|
||||
]"
|
||||
:key="idx"
|
||||
class="flex flex-col cursor-pointer items-center justify-center gap-2px rounded-lg px-8px py-4px transition-colors active:bg-slate-100 hover:bg-slate-50"
|
||||
@click="emit(btn.action as any)"
|
||||
>
|
||||
<view :class="btn.icon" class="text-18px text-slate-600" />
|
||||
<text class="text-10px text-slate-500">{{ btn.text }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<view class="flex items-center gap-16px text-14px">
|
||||
<view
|
||||
class="cursor-pointer text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('openScoreSettings')"
|
||||
>
|
||||
打分设置
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('viewAvgScore')"
|
||||
>
|
||||
查看均分
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('viewAnswer')"
|
||||
>
|
||||
查看答案
|
||||
</view>
|
||||
<view
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
@click="emit('toggleOrientation')"
|
||||
>
|
||||
<view class="i-carbon:mobile-view-orientation size-18px text-gray-700" />
|
||||
</view>
|
||||
<view
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
@click="emit('toggleFullscreen')"
|
||||
>
|
||||
<view
|
||||
class="i-carbon-image-search size-18px text-gray-700"
|
||||
/>
|
||||
</view>
|
||||
<view class="mx-4px h-24px w-1px bg-slate-200" />
|
||||
|
||||
<view
|
||||
class="size-36px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 transition-all active:scale-95 active:bg-slate-200 hover:bg-slate-100"
|
||||
@click="emit('toggleFullscreen')"
|
||||
>
|
||||
<view class="i-carbon-image-search size-22px" />
|
||||
</view>
|
||||
<view
|
||||
class="size-36px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 transition-all active:scale-95 active:bg-slate-200 hover:bg-slate-100"
|
||||
@click="emit('toggleOrientation')"
|
||||
>
|
||||
<view class="i-carbon:mobile-view-orientation size-22px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 - 左右分栏 -->
|
||||
<view class="flex flex-1">
|
||||
<view class="flex flex-1 overflow-hidden">
|
||||
<!-- 左侧打分信息面板 -->
|
||||
<view class="w-60px flex flex-col border-r border-gray-200 bg-white">
|
||||
<view class="flex flex-col items-center px-4px py-12px text-center">
|
||||
<view v-if="isViewingHistory" class="mb-2px text-12px text-gray-600">
|
||||
历史打分
|
||||
<view class="z-5 w-72px flex flex-col items-center border-r border-slate-100 bg-white py-16px shadow-sm">
|
||||
<view class="w-full flex flex-col items-center gap-12px">
|
||||
<view class="flex flex-col items-center gap-2px">
|
||||
<text class="text-10px text-slate-400">满分</text>
|
||||
<text class="text-14px text-slate-600 font-semibold font-mono">{{ formatScore(currentQuestionFullScore) }}</text>
|
||||
</view>
|
||||
<view v-else class="mb-2px text-12px text-gray-600">
|
||||
满分/{{ `${formatScore(currentQuestionFullScore)}分` }}
|
||||
</view>
|
||||
<view class="text-16px text-blue-600 font-semibold">
|
||||
{{ isViewingHistory ? `${formatScore(currentQuestionFinalScore)}/${formatScore(currentQuestionFullScore)}` : '' }}
|
||||
|
||||
<view class="h-1px w-32px bg-slate-100" />
|
||||
|
||||
<view class="flex flex-col items-center gap-2px">
|
||||
<text class="text-10px text-slate-400">得分</text>
|
||||
<text class="text-20px text-blue-600 font-bold font-mono">
|
||||
{{ isViewingHistory ? formatScore(currentQuestionFinalScore) : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间图片区域 -->
|
||||
<view class="relative h-full flex-1 overflow-hidden bg-gray-50">
|
||||
<!-- 左右箭头按钮 -->
|
||||
<!-- <view
|
||||
class="arrow-btn-landscape fixed left-80px z-100 size-32px flex cursor-pointer items-center justify-center rounded transition-colors"
|
||||
:class="canGoPrev ? 'bg-gray-100 hover:bg-gray-200' : 'bg-gray-50 opacity-50 cursor-not-allowed'"
|
||||
@click="canGoPrev && emit('prevQuestion')"
|
||||
>
|
||||
<view class="i-carbon-chevron-left size-16px text-gray-700" />
|
||||
</view>
|
||||
<view
|
||||
class="arrow-btn-landscape fixed z-100 size-32px flex cursor-pointer items-center justify-center rounded transition-colors"
|
||||
:class="canGoNext ? 'bg-gray-100 hover:bg-gray-200' : 'bg-gray-50 opacity-50 cursor-not-allowed'"
|
||||
:style="{ right: '280px' }"
|
||||
@click="canGoNext && emit('nextQuestion')"
|
||||
>
|
||||
<view class="i-carbon-chevron-right size-16px text-gray-700" />
|
||||
</view> -->
|
||||
<view class="relative flex-1 overflow-hidden bg-slate-50">
|
||||
<slot name="content" :current-question="currentQuestion" />
|
||||
</view>
|
||||
|
||||
<!-- 右侧打分区域 -->
|
||||
<view style="height: calc(100vh - 60px)" class="flex flex-col overflow-auto border-l border-gray-200 bg-white">
|
||||
<slot name="scoring" />
|
||||
</view>
|
||||
<slot name="scoring" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 竖屏布局 -->
|
||||
<view v-else class="h-100vh w-100vw flex flex-grow flex-col bg-gray-100">
|
||||
<view v-else class="h-100vh w-100vw flex flex-grow flex-col bg-slate-50">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="h-12 flex items-center bg-white px-4 py-2 shadow-sm">
|
||||
<!-- 左侧 -->
|
||||
<view class="flex items-center gap-3">
|
||||
<view
|
||||
class="size-8 flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
@click="emit('goBack')"
|
||||
>
|
||||
<view class="i-carbon-arrow-left size-5 text-gray-700" />
|
||||
</view>
|
||||
|
||||
<!-- 题目选择器弹窗 -->
|
||||
<wd-picker
|
||||
:model-value="currentQuestionIndex"
|
||||
:columns="questionPickerColumns"
|
||||
:value="String(currentQuestionIndex)"
|
||||
:title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
|
||||
@confirm="({ value }) => emit('selectQuestion', Number(value))"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二层导航栏 -->
|
||||
<view class="h-12 flex items-center border-t border-gray-200 bg-white px-4 py-2">
|
||||
<!-- 题号信息 -->
|
||||
<view class="flex items-center gap-4">
|
||||
<view v-if="!isViewingHistory" class="text-base text-gray-800 font-medium">
|
||||
{{ Math.min(totalQuestions, currentTaskSubmit) }}/{{ totalQuestions }}
|
||||
</view>
|
||||
<view v-else class="flex flex-col leading-tight">
|
||||
<text class="text-base text-gray-800 font-medium">
|
||||
{{ Math.min(totalQuestions, currentTaskSubmit) }}/{{ totalQuestions }}
|
||||
</text>
|
||||
<text class="text-xs text-blue-600">
|
||||
分值 {{ formatScore(currentQuestionFinalScore) }}/{{ formatScore(currentQuestionFullScore) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<view class="flex flex-1 items-center justify-end gap-3">
|
||||
<view
|
||||
class="cursor-pointer text-sm text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('openScoreSettings')"
|
||||
>
|
||||
打分设置
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer text-sm text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('viewAvgScore')"
|
||||
>
|
||||
查看均分
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer text-sm text-gray-700 transition-colors hover:text-blue-500"
|
||||
@click="emit('viewAnswer')"
|
||||
>
|
||||
查看答案
|
||||
</view>
|
||||
<view
|
||||
class="size-28px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
@click="emit('toggleOrientation')"
|
||||
>
|
||||
<view class="i-carbon:mobile-view-orientation size-16px text-gray-700" />
|
||||
</view>
|
||||
<view
|
||||
class="size-28px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
@click="emit('toggleFullscreen')"
|
||||
>
|
||||
<view class="z-10 flex flex-col bg-white shadow-sm">
|
||||
<view class="h-44px flex items-center justify-between px-16px">
|
||||
<view class="flex items-center gap-8px">
|
||||
<view
|
||||
class="i-carbon-image-search size-16px text-gray-700"
|
||||
/>
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 active:bg-slate-100"
|
||||
@click="emit('goBack')"
|
||||
>
|
||||
<view class="i-carbon-arrow-left size-18px" />
|
||||
</view>
|
||||
|
||||
<!-- 题目选择器 -->
|
||||
<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-14px 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>
|
||||
|
||||
<view class="flex items-center gap-2">
|
||||
<!-- 题目序号 -->
|
||||
<text class="mr-2 text-14px text-slate-600 font-medium">
|
||||
{{ currentTaskSubmit }}/{{ totalQuestions }}
|
||||
</text>
|
||||
|
||||
<view class="flex gap-8px">
|
||||
<view
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 active:bg-slate-100"
|
||||
@click="emit('toggleFullscreen')"
|
||||
>
|
||||
<view class="i-carbon-image-search size-18px" />
|
||||
</view>
|
||||
<view
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded-full bg-slate-50 text-slate-600 active:bg-slate-100"
|
||||
@click="emit('toggleOrientation')"
|
||||
>
|
||||
<view class="i-carbon:mobile-view-orientation size-18px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二层信息栏 -->
|
||||
<view class="h-36px flex items-center justify-between border-t border-slate-100 bg-slate-50 px-16px">
|
||||
<view class="flex items-baseline gap-2">
|
||||
<text class="text-12px text-slate-500">满分</text>
|
||||
<text class="text-12px text-slate-800 font-bold font-mono">{{ formatScore(currentQuestionFullScore) }}</text>
|
||||
<div class="mx-2 h-10px w-1px bg-slate-300" />
|
||||
<text class="text-12px text-slate-500">得分</text>
|
||||
<text class="text-16px text-blue-600 font-bold font-mono">
|
||||
{{ isViewingHistory ? formatScore(currentQuestionFinalScore) : '--' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center gap-16px">
|
||||
<text
|
||||
v-for="(action, idx) in [
|
||||
{ t: '设置', a: 'openScoreSettings' },
|
||||
{ t: '均分', a: 'viewAvgScore' },
|
||||
{ t: '答案', a: 'viewAnswer' },
|
||||
]"
|
||||
:key="idx"
|
||||
class="text-12px text-slate-500 active:text-blue-600"
|
||||
@click="emit(action.a as any)"
|
||||
>
|
||||
{{ action.t }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -301,14 +289,12 @@ const currentHistoryIndexInList = computed(() => {
|
|||
<!-- 作答区域和打分区域 - 左右分栏 -->
|
||||
<view class="relative flex flex-1 overflow-hidden">
|
||||
<!-- 左侧图片区域 -->
|
||||
<view class="relative flex-1 overflow-hidden bg-gray-50">
|
||||
<view class="relative flex-1 overflow-hidden bg-slate-50">
|
||||
<slot name="content" :current-question="currentQuestion" />
|
||||
</view>
|
||||
|
||||
<!-- 右侧打分区域 -->
|
||||
<view class="h-full flex flex-col overflow-auto border-l border-gray-200 bg-white">
|
||||
<slot name="scoring" />
|
||||
</view>
|
||||
<slot name="scoring" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -238,6 +238,18 @@ async function submitScore(score: number) {
|
|||
// 提交当前分数
|
||||
async function submitCurrentScore() {
|
||||
try {
|
||||
// 如果当前分数是-1(未打分),根据模式设置默认分数
|
||||
if (currentScore.value === -1) {
|
||||
if (currentMode.value === 'subtract') {
|
||||
// 减分模式:不打分提交就是满分
|
||||
currentScore.value = props.fullScore
|
||||
}
|
||||
else {
|
||||
// 加分模式:不打分提交就是0分
|
||||
currentScore.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
await markingData.submitRecord()
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
|
|
@ -261,10 +273,12 @@ async function submitCurrentScore() {
|
|||
|
||||
<template>
|
||||
<!-- 横屏布局 -->
|
||||
<div
|
||||
<scroll-view
|
||||
v-if="isLandscape"
|
||||
class="h-full flex flex-col overflow-x-hidden overflow-y-auto py-8px"
|
||||
:class="(isCollapsed && scoreMode === 'onekey') || scoreMode === 'quick' ? 'w-60px px-4px' : 'w-120px px-8px'"
|
||||
scroll-y
|
||||
scroll-x
|
||||
class="z-5 h-full flex flex-grow-0 flex-col py-8px"
|
||||
:class="(isCollapsed && scoreMode === 'onekey') || scoreMode === 'quick' ? 'w-60px min-w-60px px-4px' : 'w-120px min-w-120px px-8px'"
|
||||
>
|
||||
<div class="flex flex-1 flex-col gap-4px">
|
||||
<!-- 收起时:一列布局 -->
|
||||
|
|
@ -290,7 +304,7 @@ async function submitCurrentScore() {
|
|||
<div
|
||||
v-for="item in row"
|
||||
:key="item.label"
|
||||
class="h-32px flex flex-1 cursor-pointer select-none items-center justify-center border-2 rounded-4px text-12px font-medium transition-all active:scale-95"
|
||||
class="h-40px flex flex-1 cursor-pointer select-none items-center justify-center border rounded-lg text-14px font-bold shadow-sm transition-all active:scale-95"
|
||||
:class="item.class"
|
||||
@click="item.action"
|
||||
>
|
||||
|
|
@ -300,44 +314,46 @@ async function submitCurrentScore() {
|
|||
<div v-if="row.length === 1" class="flex-1" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="mt-6px h-32px flex flex-shrink-0 cursor-pointer items-center justify-center border-2 border-blue-200 rounded-4px bg-blue-50 transition-all active:scale-95"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
class="size-14px text-blue-500 transition-transform"
|
||||
:class="!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left'"
|
||||
/>
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="mb-40px mt-6px h-40px flex flex-shrink-0 cursor-pointer items-center justify-center border border-blue-100 rounded-lg bg-blue-50 transition-all active:scale-95"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<div
|
||||
class="size-16px text-blue-500 transition-transform"
|
||||
:class="!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷打分模式切换 -->
|
||||
<div v-if="scoreMode === 'quick'" class="mt-12px flex flex-col gap-6px">
|
||||
<div v-if="scoreMode === 'quick'" class="mt-12px flex flex-col gap-8px">
|
||||
<div
|
||||
class="h-36px flex cursor-pointer items-center justify-center border rounded-4px text-11px transition-all"
|
||||
:class="currentMode === 'add' ? 'border-green-500 bg-green-50 text-green-600' : 'border-gray-300 bg-white text-gray-600'"
|
||||
class="h-40px flex cursor-pointer items-center justify-center border rounded-lg text-12px font-bold shadow-sm transition-all"
|
||||
:class="currentMode === 'add' ? 'border-green-500 bg-green-500 text-white ring-2 ring-green-200' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
@click="currentMode = 'add'"
|
||||
>
|
||||
加分
|
||||
</div>
|
||||
<div
|
||||
class="h-36px flex cursor-pointer items-center justify-center border rounded-4px text-11px transition-all"
|
||||
:class="currentMode === 'subtract' ? 'border-red-500 bg-red-50 text-red-600' : 'border-gray-300 bg-white text-gray-600'"
|
||||
class="h-40px flex cursor-pointer items-center justify-center border rounded-lg text-12px font-bold shadow-sm transition-all"
|
||||
:class="currentMode === 'subtract' ? 'border-red-500 bg-red-500 text-white ring-2 ring-red-200' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
@click="currentMode = 'subtract'"
|
||||
>
|
||||
减分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 竖屏布局 -->
|
||||
<div
|
||||
<scroll-view
|
||||
v-else
|
||||
class="h-full flex flex-col overflow-x-hidden overflow-y-auto py-16rpx"
|
||||
:class="(isCollapsed && scoreMode === 'onekey') || scoreMode === 'quick' ? 'w-100rpx px-8rpx' : 'w-160rpx px-12rpx'"
|
||||
scroll-y
|
||||
scroll-x
|
||||
class="z-5 h-full flex flex-grow-0 flex-col py-16rpx"
|
||||
:class="(isCollapsed && scoreMode === 'onekey') || scoreMode === 'quick' ? 'w-100rpx min-w-100rpx px-8rpx' : 'w-160rpx min-w-160rpx px-12rpx'"
|
||||
>
|
||||
<div class="flex flex-1 flex-col gap-8rpx">
|
||||
<!-- 收起时:一列布局 -->
|
||||
|
|
@ -363,7 +379,7 @@ async function submitCurrentScore() {
|
|||
<div
|
||||
v-for="item in row"
|
||||
:key="item.label"
|
||||
class="h-64rpx flex flex-1 cursor-pointer select-none items-center justify-center border-2 rounded-8rpx text-24rpx font-medium transition-all active:scale-95"
|
||||
class="h-80rpx flex flex-1 cursor-pointer select-none items-center justify-center border rounded-16rpx text-28rpx font-bold shadow-sm transition-all active:scale-95"
|
||||
:class="item.class"
|
||||
@click="item.action"
|
||||
>
|
||||
|
|
@ -373,43 +389,43 @@ async function submitCurrentScore() {
|
|||
<div v-if="row.length === 1" class="flex-1" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="mt-12rpx h-64rpx flex flex-shrink-0 cursor-pointer items-center justify-center border-2 border-blue-200 rounded-8rpx bg-blue-50 transition-all active:scale-95"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
class="size-28rpx text-blue-500 transition-transform"
|
||||
:class="!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left'"
|
||||
/>
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="mb-40px mt-12rpx h-80rpx flex flex-shrink-0 cursor-pointer items-center justify-center border border-blue-100 rounded-16rpx bg-blue-50 transition-all active:scale-95"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<div
|
||||
class="size-32rpx text-blue-500 transition-transform"
|
||||
:class="!isCollapsed ? 'i-carbon-chevron-right' : 'i-carbon-chevron-left'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷打分模式切换 -->
|
||||
<div v-if="scoreMode === 'quick'" class="mt-16rpx flex flex-col gap-8rpx">
|
||||
<div v-if="scoreMode === 'quick'" class="mt-16rpx flex flex-col gap-12rpx">
|
||||
<div
|
||||
class="h-56rpx flex cursor-pointer items-center justify-center border rounded-8rpx text-22rpx transition-all"
|
||||
:class="currentMode === 'add' ? 'border-green-500 bg-green-50 text-green-600' : 'border-gray-300 bg-white text-gray-600'"
|
||||
class="h-80rpx flex cursor-pointer items-center justify-center border rounded-16rpx text-26rpx font-bold shadow-sm transition-all"
|
||||
:class="currentMode === 'add' ? 'border-green-500 bg-green-500 text-white ring-2 ring-green-200' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
@click="currentMode = 'add'"
|
||||
>
|
||||
加分
|
||||
</div>
|
||||
<div
|
||||
class="h-56rpx flex cursor-pointer items-center justify-center border rounded-8rpx text-22rpx transition-all"
|
||||
:class="currentMode === 'subtract' ? 'border-red-500 bg-red-50 text-red-600' : 'border-gray-300 bg-white text-gray-600'"
|
||||
class="h-80rpx flex cursor-pointer items-center justify-center border rounded-16rpx text-26rpx font-bold shadow-sm transition-all"
|
||||
:class="currentMode === 'subtract' ? 'border-red-500 bg-red-500 text-white ring-2 ring-red-200' : 'border-slate-200 bg-white text-slate-600 hover:bg-slate-50'"
|
||||
@click="currentMode = 'subtract'"
|
||||
>
|
||||
减分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 快捷打分模式的悬浮提交按钮 -->
|
||||
<div
|
||||
v-if="scoreMode === 'quick'"
|
||||
class="fixed z-10 size-48px flex select-none items-center justify-center border-2 border-blue-500 rounded-full bg-blue-500 text-white shadow-xl transition-transform lg:size-56px active:scale-95"
|
||||
class="fixed z-10 size-48px flex select-none items-center justify-center border-2 border-blue-500 rounded-full bg-blue-500 text-14px text-white shadow-xl transition-transform lg:size-56px active:scale-95"
|
||||
:class="{
|
||||
'cursor-move': isDragging,
|
||||
'cursor-pointer': !isDragging,
|
||||
|
|
|
|||
|
|
@ -66,16 +66,16 @@ const expandedImageUrls = computed(() => {
|
|||
>
|
||||
<!-- 顶部工具栏 -->
|
||||
<view
|
||||
class="flex items-center justify-between bg-black/80 px-4 py-3"
|
||||
class="flex items-center justify-between bg-black/80 px-16px py-12px"
|
||||
:style="{ paddingTop: `${(safeAreaInsets?.top || 0) + 12}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<text class="text-base text-white font-medium">全屏查看</text>
|
||||
<text class="text-16px text-white font-medium">全屏查看</text>
|
||||
<view
|
||||
class="size-8 flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-white/20"
|
||||
class="size-32px flex cursor-pointer items-center justify-center rounded transition-colors hover:bg-white/20"
|
||||
@click="visible = false"
|
||||
>
|
||||
<view class="i-carbon-close size-5 text-white" />
|
||||
<view class="i-carbon-close size-20px text-white" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ const expandedImageUrls = computed(() => {
|
|||
</view>
|
||||
|
||||
<!-- 多张图片 -->
|
||||
<view v-else class="flex flex-col gap-4 p-4">
|
||||
<view v-else class="flex flex-col gap-12px p-16px">
|
||||
<view
|
||||
v-for="(imageUrl, index) in expandedImageUrls"
|
||||
:key="index"
|
||||
|
|
@ -113,8 +113,8 @@ const expandedImageUrls = computed(() => {
|
|||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="bg-black/80 px-4 py-2 text-center" @click.stop>
|
||||
<text class="text-sm text-white/70">长按图片可保存</text>
|
||||
<view class="bg-black/80 px-16px py-8px text-center" @click.stop>
|
||||
<text class="text-12px text-white/70">长按图片可保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ const emit = defineEmits<{
|
|||
'marking-change': [questionIndex: number, imageIndex: number, data: DomMarkingData]
|
||||
'ready': [imageInfo: { width: number, height: number }]
|
||||
'scroll-edge': [atLeftEdge: boolean, atRightEdge: boolean]
|
||||
'touch-start': [event: TouchEvent]
|
||||
'touch-move': [event: TouchEvent]
|
||||
'touch-end': [event: TouchEvent]
|
||||
}>()
|
||||
|
||||
const scale = defineModel<number>('scale', { default: 1.0 })
|
||||
|
|
@ -31,6 +34,21 @@ const firstImageSize = ref({ width: 0, height: 0 })
|
|||
// 滚动容器ID(用于 uni.createSelectorQuery)
|
||||
const scrollContainerId = `scroll-container-${Math.random().toString(36).slice(2)}`
|
||||
|
||||
/**
|
||||
* 手势事件透传
|
||||
*/
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
emit('touch-start', e)
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent) {
|
||||
emit('touch-move', e)
|
||||
}
|
||||
|
||||
function handleTouchEnd(e: TouchEvent) {
|
||||
emit('touch-end', e)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查滚动边缘状态(使用 uni.createSelectorQuery)
|
||||
*/
|
||||
|
|
@ -39,12 +57,14 @@ function checkScrollEdge() {
|
|||
query.select(`#${scrollContainerId}`).scrollOffset()
|
||||
query.select(`#${scrollContainerId}`).boundingClientRect()
|
||||
query.exec((res) => {
|
||||
if (!res || res.length < 2) return
|
||||
if (!res || res.length < 2)
|
||||
return
|
||||
|
||||
const scrollInfo = res[0]
|
||||
const rectInfo = res[1]
|
||||
|
||||
if (!scrollInfo || !rectInfo) return
|
||||
if (!scrollInfo || !rectInfo)
|
||||
return
|
||||
|
||||
const scrollLeft = scrollInfo.scrollLeft || 0
|
||||
const scrollWidth = scrollInfo.scrollWidth || 0
|
||||
|
|
@ -153,10 +173,15 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
<scroll-view
|
||||
:id="scrollContainerId"
|
||||
class="h-full w-full overflow-auto"
|
||||
class="h-full w-full"
|
||||
scroll-y="true"
|
||||
scroll-x="true"
|
||||
@scroll="handleScroll"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<!-- 题目列表 -->
|
||||
<view
|
||||
|
|
@ -191,8 +216,8 @@ defineExpose({
|
|||
v-for="(url, index) in nextQuestionImages"
|
||||
:key="`preload_${index}`"
|
||||
:src="url"
|
||||
class="w-0 h-0"
|
||||
class="h-0 w-0"
|
||||
/>
|
||||
</view>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -40,89 +40,44 @@ const emit = defineEmits<{
|
|||
'toggle-special-mark': [type: 'excellent' | 'typical' | 'problem']
|
||||
}>()
|
||||
const currentTool = defineModel<MarkingTool>('currentTool', { required: true })
|
||||
const showToolOptions = computed(() => {
|
||||
return [MarkingTool.PEN, MarkingTool.TEXT].includes(currentTool.value)
|
||||
})
|
||||
|
||||
// 工具栏引用
|
||||
const toolbarRef = ref<HTMLElement>()
|
||||
|
||||
// 工具栏位置状态,使用 SessionStorage 持久化,默认位于左下角
|
||||
const position = useSessionStorage('marking-toolbar-position', { x: 16, y: 16 })
|
||||
|
||||
// 收起状态
|
||||
const isCollapsed = ref(uni.getStorageSync('marking-toolbar-collapsed') || false)
|
||||
watch(isCollapsed, (newVal) => {
|
||||
uni.setStorageSync('marking-toolbar-collapsed', newVal)
|
||||
})
|
||||
|
||||
// 拖动状态
|
||||
const isDragging = ref(false)
|
||||
const dragState = ref({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startPositionX: 0,
|
||||
startPositionY: 0,
|
||||
})
|
||||
|
||||
const { getDictOptionsComputed } = useDict()
|
||||
const { options: problemTypeOptions } = getDictOptionsComputed(DictCode.SCAN_ANOMALY_STATUS)
|
||||
const problemTooltipContent = computed(() => {
|
||||
if (props.currentMarkingData?.isProblem) {
|
||||
return `当前问题类型:${problemTypeOptions.value.find(item => item.value === props.currentMarkingData?.problemType)?.label}`
|
||||
}
|
||||
return '标记为问题卷'
|
||||
})
|
||||
|
||||
/**
|
||||
* 将 SVG 转换为 data URL(用于小程序)
|
||||
*/
|
||||
function svgToDataURL(svg: string): string {
|
||||
// 将被设置到 dataset 中的属性还原出来
|
||||
svg = svg.replace(/data-(.*?=(['"]).*?\2)/g, '$1')
|
||||
|
||||
// 将被设置到 data-xlink-href 的属性还原出来
|
||||
svg = svg.replace(/xlink-href=/g, 'xlink:href=')
|
||||
|
||||
// 将 dataset 中被变成 kebab-case 写法的 viewBox 还原出来
|
||||
svg = svg.replace(/view-box=/g, 'viewBox=')
|
||||
|
||||
// 清除 SVG 中不应该显示的 title、desc、defs 元素
|
||||
svg = svg.replace(/<(title|desc|defs)>[\s\S]*?<\/\1>/g, '')
|
||||
|
||||
// 为非标准 XML 的 SVG 添加 xmlns,防止视图层解析出错
|
||||
if (!/xmlns=/.test(svg))
|
||||
svg = svg.replace(/<svg/, '<svg xmlns=\'http://www.w3.org/2000/svg\'')
|
||||
|
||||
// 对 SVG 中出现的浮点数统一取最多两位小数,缓解数据量过大问题
|
||||
svg = svg.replace(/\d+\.\d+/g, match => Number.parseFloat(Number.parseFloat(match).toFixed(2)) as any)
|
||||
|
||||
// 清除注释,缓解数据量过大的问题
|
||||
svg = svg.replace(/<!--[\s\S]*?-->/g, '')
|
||||
|
||||
// 模拟 HTML 的 white-space 行为,将多个空格或换行符换成一个空格,减少数据量
|
||||
svg = svg.replace(/\s+/g, ' ')
|
||||
|
||||
// 对特殊符号进行转义,这里参考了 https://github.com/bhovhannes/svg-url-loader/blob/master/src/loader.js
|
||||
svg = svg.replace(/[{}|\\^~[\]`"<>#%]/g, (match) => {
|
||||
return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}`
|
||||
})
|
||||
|
||||
// 单引号替换为 \',由于 kbone 的 bug,节点属性中的双引号在生成 outerHTML 时不会被转义导致出错
|
||||
// 因此 background-image: url( 后面只能跟单引号,所以生成的 URI 内部也就只能用斜杠转义单引号了
|
||||
svg = svg.replace(/'/g, '\\\'')
|
||||
|
||||
// 最后添加 mime 头部,变成 Webview 可以识别的 Data URI
|
||||
return `data:image/svg+xml,${svg.trim()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成半对标记的 SVG data URL
|
||||
* 根据当前按钮状态动态生成颜色
|
||||
*/
|
||||
function generateHalfMarkSvg(isActive: boolean): string {
|
||||
// 根据激活状态选择颜色
|
||||
const color = isActive ? '#ffffff' : '#eab308' // 白色(激活)或黄色(未激活)
|
||||
const color = isActive ? '#ffffff' : '#eab308'
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<line x1="6" y1="12" x2="10.5" y2="16.5" stroke="${color}" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||
<line x1="10.5" y1="16.5" x2="18" y2="7.5" stroke="${color}" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||
|
|
@ -142,9 +97,6 @@ const pendingMarkType = ref<'correct' | 'wrong' | 'half' | null>(null)
|
|||
// 画笔提示相关
|
||||
const PEN_TOOLTIP_KEY = 'marking-pen-tooltip-shown'
|
||||
|
||||
/**
|
||||
* 显示画笔提示(仅首次显示)
|
||||
*/
|
||||
function showPenTooltip() {
|
||||
const hasShown = uni.getStorageSync(PEN_TOOLTIP_KEY)
|
||||
if (!hasShown) {
|
||||
|
|
@ -160,91 +112,6 @@ function showPenTooltip() {
|
|||
}
|
||||
}
|
||||
|
||||
// 统一的按钮基础样式
|
||||
const baseButtonClass = computed(() => {
|
||||
return props.isLandscape
|
||||
? 'size-32rpx lg:size-8 !p-0 rounded-lg flex items-center justify-center transition-all duration-200 ease-in-out shadow-sm hover:shadow-md active:scale-95 after:border-none'
|
||||
: 'size-8 lg:size-10 !p-0 rounded-lg flex items-center justify-center transition-all duration-200 ease-in-out shadow-sm hover:shadow-md active:scale-95 after:border-none'
|
||||
})
|
||||
|
||||
const separatorClass = computed(() => {
|
||||
return props.isLandscape
|
||||
? 'mx-4rpx lg:mx-1 h-4 lg:h-6 w-px flex-shrink-0 bg-gray-300'
|
||||
: 'h-5 lg:h-6 w-px bg-gray-300'
|
||||
})
|
||||
const iconClass = computed(() => {
|
||||
return props.isLandscape
|
||||
? 'size-20rpx lg:size-4'
|
||||
: 'size-5 lg:size-5 w-4 lg:w-5'
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取工具按钮样式类
|
||||
*/
|
||||
function getToolButtonClass(tool: MarkingTool) {
|
||||
const isActive = currentTool.value === tool
|
||||
return [
|
||||
baseButtonClass.value,
|
||||
isActive
|
||||
? 'bg-blue-600 text-white border border-blue-500'
|
||||
: 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700',
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标记按钮样式类(对错半对)
|
||||
*/
|
||||
function getMarkButtonClass(type: 'correct' | 'wrong' | 'half') {
|
||||
const toolMap = {
|
||||
correct: MarkingTool.CORRECT,
|
||||
wrong: MarkingTool.WRONG,
|
||||
half: MarkingTool.HALF,
|
||||
}
|
||||
const colorMap = {
|
||||
correct: {
|
||||
active: 'bg-green-500 text-white border border-green-400',
|
||||
inactive: 'bg-white text-green-600 border border-green-200 hover:bg-green-50 hover:border-green-300 hover:text-green-700',
|
||||
},
|
||||
wrong: {
|
||||
active: 'bg-red-500 text-white border border-red-400',
|
||||
inactive: 'bg-white text-red-600 border border-red-200 hover:bg-red-50 hover:border-red-300 hover:text-red-700',
|
||||
},
|
||||
half: {
|
||||
active: 'bg-yellow-500 text-white border border-yellow-400',
|
||||
inactive: 'bg-white text-yellow-600 border border-yellow-200 hover:bg-yellow-50 hover:border-yellow-300 hover:text-yellow-700',
|
||||
},
|
||||
}
|
||||
|
||||
const isActive = currentTool.value === toolMap[type]
|
||||
const colors = colorMap[type]
|
||||
|
||||
return [baseButtonClass.value, isActive ? colors.active : colors.inactive]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特殊标记按钮样式类
|
||||
*/
|
||||
function getSpecialButtonClass(type: 'excellent' | 'typical' | 'problem') {
|
||||
const isActive = props.specialMarks[type]
|
||||
const colorMap = {
|
||||
excellent: {
|
||||
active: 'bg-yellow-500 text-white border border-yellow-400',
|
||||
inactive: 'bg-white text-yellow-600 border border-yellow-200 hover:bg-yellow-50 hover:border-yellow-300 hover:text-yellow-700',
|
||||
},
|
||||
typical: {
|
||||
active: 'bg-blue-600 text-white border border-blue-500',
|
||||
inactive: 'bg-white text-blue-600 border border-blue-200 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700',
|
||||
},
|
||||
problem: {
|
||||
active: 'bg-red-500 text-white border border-red-400',
|
||||
inactive: 'bg-white text-red-600 border border-red-200 hover:bg-red-50 hover:border-red-300 hover:text-red-700',
|
||||
},
|
||||
}
|
||||
|
||||
const colors = colorMap[type]
|
||||
return [baseButtonClass.value, props.isLandscape ? 'text-12rpx lg:text-sm' : 'text-sm lg:text-base', 'font-medium', isActive ? colors.active : colors.inactive]
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换工具(支持取消激活)
|
||||
*/
|
||||
|
|
@ -253,17 +120,15 @@ function switchTool(tool: MarkingTool) {
|
|||
currentTool.value = wasDeactivated ? MarkingTool.SELECT : tool
|
||||
pendingMarkType.value = null
|
||||
|
||||
// 如果是激活画笔工具,显示提示
|
||||
if (!wasDeactivated && tool === MarkingTool.PEN) {
|
||||
showPenTooltip()
|
||||
}
|
||||
|
||||
// 显示/隐藏工具选项
|
||||
settings.value.showToolOptions = currentTool.value === 'pen' || currentTool.value === 'text'
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理标记工具切换(正确/错误/半对)
|
||||
* 处理标记工具切换
|
||||
*/
|
||||
function handleMarkTool(type: 'correct' | 'wrong' | 'half') {
|
||||
const toolMap = {
|
||||
|
|
@ -277,7 +142,7 @@ function handleMarkTool(type: 'correct' | 'wrong' | 'half') {
|
|||
}
|
||||
|
||||
/**
|
||||
* 处理图片点击(用于放置标记)
|
||||
* 处理图片点击
|
||||
*/
|
||||
function handleImageClick(position: { x: number, y: number }) {
|
||||
if (pendingMarkType.value) {
|
||||
|
|
@ -292,46 +157,10 @@ function handleImageClick(position: { x: number, y: number }) {
|
|||
emit('add-half-mark', position)
|
||||
break
|
||||
}
|
||||
|
||||
pendingMarkType.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理撤销
|
||||
*/
|
||||
function handleUndo() {
|
||||
if (props.canUndo) {
|
||||
emit('undo')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重做
|
||||
*/
|
||||
function handleRedo() {
|
||||
if (props.canRedo) {
|
||||
emit('redo')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理清除所有
|
||||
*/
|
||||
function handleClearAll() {
|
||||
if (props.hasMarks) {
|
||||
uni.showModal({
|
||||
confirmButtonText: '确认清除',
|
||||
cancelButtonText: '取消',
|
||||
success: (result) => {
|
||||
if (result.confirm) {
|
||||
emit('clear-all')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换特殊标记
|
||||
*/
|
||||
|
|
@ -339,9 +168,6 @@ function toggleSpecialMark(type: 'excellent' | 'typical' | 'problem') {
|
|||
emit('toggle-special-mark', type)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收起/展开状态
|
||||
*/
|
||||
function toggleCollapse() {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
|
@ -355,135 +181,193 @@ defineExpose({
|
|||
<template>
|
||||
<div
|
||||
ref="toolbarRef"
|
||||
class="rounded-tr-base fixed bottom-0 left-0 z-10 select-none border border-white/20 shadow-xl backdrop-blur-sm transition-all duration-300"
|
||||
class="fixed z-5 flex flex-col items-start transition-all duration-300 ease-out"
|
||||
:class="[
|
||||
{
|
||||
'px-2 py-1 w-fit cursor-move lg:px-3 lg:py-2': !isLandscape && !isCollapsed,
|
||||
'px-4rpx py-2rpx': isLandscape && !isCollapsed,
|
||||
'bg-slate-800/95': !isCollapsed,
|
||||
'backdrop-saturate-150': !isCollapsed,
|
||||
},
|
||||
isLandscape
|
||||
? 'bottom-12px left-1/2 -translate-x-1/2 items-center'
|
||||
: 'bottom-48rpx left-12rpx items-start max-w-[75vw]',
|
||||
]"
|
||||
@touchend.stop
|
||||
@touchmove.stop
|
||||
@touchstart.stop
|
||||
>
|
||||
<!-- 收起按钮 -->
|
||||
<!-- 收起状态 (FAB) -->
|
||||
<div
|
||||
v-if="isCollapsed"
|
||||
class="h-full w-8 flex transform cursor-pointer items-center justify-center border border-gray-200 rounded-tr-2xl bg-white shadow-inner"
|
||||
:class="{
|
||||
'w-64px': isLandscape,
|
||||
}"
|
||||
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"
|
||||
@mousedown.stop
|
||||
>
|
||||
<div class="i-carbon:chevron-right text-base text-blue-600 transition-colors lg:text-lg hover:text-blue-700" />
|
||||
<div class="i-fluent:edit-24-regular text-blue-600" :class="isLandscape ? 'size-24px' : 'size-40rpx'" />
|
||||
</div>
|
||||
|
||||
<!-- 工具栏内容 -->
|
||||
<!-- 展开状态 -->
|
||||
<div
|
||||
v-show="!isCollapsed"
|
||||
class="flex items-center" :class="{
|
||||
'gap-4rpx lg:gap-2': !isLandscape,
|
||||
'gap-4rpx lg:gap-1': isLandscape,
|
||||
}"
|
||||
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 },
|
||||
]"
|
||||
>
|
||||
<!-- 收起按钮(展开状态时显示) -->
|
||||
<button
|
||||
class="border border-gray-200 bg-white text-slate-600 transition-all hover:border-gray-300 hover:bg-gray-50 hover:text-slate-700"
|
||||
:class="baseButtonClass"
|
||||
<!-- 收起按钮 -->
|
||||
<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"
|
||||
>
|
||||
<div :class="iconClass" class="i-carbon:chevron-left" />
|
||||
</button>
|
||||
<div
|
||||
:class="[
|
||||
isLandscape ? 'i-carbon:chevron-down size-20px' : 'i-carbon:chevron-left size-36rpx',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="separatorClass" />
|
||||
<!-- 对错半对标记 -->
|
||||
<wd-tooltip content="正确标记" placement="top">
|
||||
<button :class="getMarkButtonClass('correct')" @click="handleMarkTool('correct')">
|
||||
<div :class="iconClass" class="i-fluent:checkmark-24-regular" />
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-4/5 w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<wd-tooltip content="错误标记" placement="top">
|
||||
<button :class="getMarkButtonClass('wrong')" @click="handleMarkTool('wrong')">
|
||||
<div :class="iconClass" class="i-fluent:dismiss-24-regular" />
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 工具内容区域 (竖屏时可滚动) -->
|
||||
<scroll-view
|
||||
:scroll-x="true"
|
||||
:show-scrollbar="false"
|
||||
class="flex whitespace-nowrap"
|
||||
:class="isLandscape ? 'w-auto' : 'max-w-[65vw]'"
|
||||
>
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-16rpx'">
|
||||
<!-- 标记组 -->
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
|
||||
<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',
|
||||
]"
|
||||
@click="handleMarkTool('correct')"
|
||||
>
|
||||
<div class="i-fluent:checkmark-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
<wd-tooltip content="半对标记" placement="top">
|
||||
<button :class="getMarkButtonClass('half')" @click="handleMarkTool('half')">
|
||||
<view
|
||||
class="h-48rpx w-48rpx" :class="iconClass"
|
||||
:style="{
|
||||
backgroundImage: `url('${halfMarkSvg}')`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<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.WRONG ? 'bg-red-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-red-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
]"
|
||||
@click="handleMarkTool('wrong')"
|
||||
>
|
||||
<div class="i-fluent:dismiss-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
<div :class="separatorClass" />
|
||||
<!-- 橡皮擦 -->
|
||||
<wd-tooltip content="擦除" placement="top">
|
||||
<button
|
||||
:class="getToolButtonClass(MarkingTool.ERASER)"
|
||||
@click="switchTool(MarkingTool.ERASER)"
|
||||
>
|
||||
<div :class="iconClass" class="i-fluent:eraser-24-regular" />
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<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.HALF ? 'bg-yellow-500 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-yellow-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
]"
|
||||
@click="handleMarkTool('half')"
|
||||
>
|
||||
<view
|
||||
:class="isLandscape ? 'size-20px' : 'size-40rpx'"
|
||||
:style="{
|
||||
backgroundImage: `url('${halfMarkSvg}')`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 文字工具 -->
|
||||
<wd-tooltip content="添加文字" placement="top">
|
||||
<button :class="getToolButtonClass(MarkingTool.TEXT)" @click="switchTool(MarkingTool.TEXT)">
|
||||
<div :class="iconClass" class="i-fluent:text-24-regular" />
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<!-- 任意笔画 -->
|
||||
<wd-tooltip content="自由绘制" placement="top">
|
||||
<button :class="getToolButtonClass(MarkingTool.PEN)" @click="switchTool(MarkingTool.PEN)">
|
||||
<div :class="iconClass" class="i-fluent:pen-24-regular" />
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 绘图组 -->
|
||||
<div class="flex items-center rounded-full bg-slate-50" :class="isLandscape ? 'p-4px gap-4px' : 'p-6rpx gap-8rpx'">
|
||||
<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',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.PEN)"
|
||||
>
|
||||
<div class="i-fluent:pen-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
<div :class="separatorClass" />
|
||||
<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.TEXT ? 'bg-blue-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-blue-600',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.TEXT)"
|
||||
>
|
||||
<div class="i-fluent:text-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
|
||||
<!-- 撤回 -->
|
||||
<!-- <wd-tooltip content="撤回一步" placement="top">
|
||||
<button :class="getActionButtonClass('undo')" :disabled="!canUndo" @click="handleUndo">
|
||||
<div class="i-fluent:arrow-undo-24-regular h-4 w-4" />
|
||||
</button>
|
||||
</wd-tooltip> -->
|
||||
<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.ERASER ? 'bg-slate-600 text-white shadow-md' : 'text-slate-500 hover:bg-white hover:text-slate-700',
|
||||
isLandscape ? 'size-32px' : 'size-64rpx',
|
||||
]"
|
||||
@click="switchTool(MarkingTool.ERASER)"
|
||||
>
|
||||
<div class="i-fluent:eraser-24-regular" :class="isLandscape ? 'size-20px' : 'size-40rpx'" />
|
||||
</div>
|
||||
</wd-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 特殊标记 -->
|
||||
<wd-tooltip content="标记为优秀卷" placement="top">
|
||||
<button
|
||||
:class="getSpecialButtonClass('excellent')"
|
||||
@click="toggleSpecialMark('excellent')"
|
||||
>
|
||||
优秀
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 分隔线 -->
|
||||
<div class="h-20px w-1px flex-shrink-0 bg-slate-200" />
|
||||
|
||||
<wd-tooltip content="标记为典例卷" placement="top">
|
||||
<button
|
||||
:class="getSpecialButtonClass('typical')"
|
||||
@click="toggleSpecialMark('typical')"
|
||||
>
|
||||
典例
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
|
||||
<wd-tooltip :content="problemTooltipContent" placement="top">
|
||||
<button
|
||||
:class="getSpecialButtonClass('problem')"
|
||||
@click="toggleSpecialMark('problem')"
|
||||
>
|
||||
问题
|
||||
</button>
|
||||
</wd-tooltip>
|
||||
<!-- 特殊标记组 -->
|
||||
<div class="flex items-center" :class="isLandscape ? 'gap-6px' : 'gap-12rpx'">
|
||||
<div
|
||||
v-for="type in ['excellent', 'typical', 'problem']"
|
||||
:key="type"
|
||||
class="flex flex-shrink-0 cursor-pointer items-center justify-center rounded-full font-medium transition-all active:scale-95"
|
||||
:class="[
|
||||
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',
|
||||
]"
|
||||
@click="toggleSpecialMark(type as any)"
|
||||
>
|
||||
{{ type === 'excellent' ? '优秀' : type === 'typical' ? '典例' : '问题' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</scroll-view>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ function createMarkingData(options: UseMarkingDataOptions) {
|
|||
|
||||
const currentScore = computed({
|
||||
get: () => {
|
||||
return currentMarkingSubmitData.value[firstNotScoredIndex.value]?.score || -1
|
||||
const score = currentMarkingSubmitData.value[firstNotScoredIndex.value]?.score
|
||||
return score !== undefined ? score : -1
|
||||
},
|
||||
set: (value) => {
|
||||
if (currentMarkingSubmitData.value[firstNotScoredIndex.value]) {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const defaultSettings: MarkingSettings = {
|
|||
|
||||
// 快捷打分默认值
|
||||
quickScoreMode: 'onekey',
|
||||
quickScoreScores: [1, 2],
|
||||
quickScoreScores: [0.5, 1, 2, 3],
|
||||
quickScoreLayout: 'single',
|
||||
|
||||
// 一键打分默认值
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ export interface MarkingContext {
|
|||
defaultPosition?: 'first' | 'last' // 默认定位:first-第一个,last-最后一个
|
||||
}
|
||||
|
||||
export const cachedImages = ref<string[]>([])
|
||||
|
||||
/**
|
||||
* 默认的阅卷数据提供者(使用原有的API)
|
||||
* 带滑动窗口缓存优化
|
||||
|
|
@ -115,8 +117,6 @@ export class DefaultMarkingDataProvider implements MarkingDataProvider {
|
|||
private hasSubmitted = false
|
||||
private fetchingPromise: Promise<ExamStudentMarkingQuestionsResponse | null> | null = null
|
||||
|
||||
cachedImages = ref<string[]>([])
|
||||
|
||||
async getSingleQuestion(taskId: number): Promise<ExamStudentMarkingQuestionResponse | null> {
|
||||
// 如果任务ID变化,清空缓存
|
||||
if (this.lastTaskId !== taskId) {
|
||||
|
|
@ -176,7 +176,8 @@ export class DefaultMarkingDataProvider implements MarkingDataProvider {
|
|||
|
||||
// 预加载图片资源
|
||||
if (this.cachedQuestion.questions?.[0]?.image_urls) {
|
||||
this.cachedImages.value = this.cachedQuestion.questions?.[0]?.image_urls || []
|
||||
cachedImages.value = this.cachedQuestion.questions?.[0]?.image_urls || []
|
||||
console.log('cachedImages', cachedImages.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ function handleExamChange() {
|
|||
uni.showActionSheet({
|
||||
itemList: examNames,
|
||||
success: (res) => {
|
||||
const selectedOption = homeStore.examOptions[res.tapIndex].filter(item => item.)
|
||||
const selectedOption = homeStore.examOptions[res.tapIndex]
|
||||
homeStore.selectedExamId = selectedOption.value as number
|
||||
// 选择考试后重新获取统计数据
|
||||
fetchExamStats()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import QuickScorePanel from '@/components/marking/components/QuickScorePanel.vue
|
|||
import MarkingImageViewerNew from '@/components/marking/components/renderer/MarkingImageViewerNew.vue'
|
||||
import { provideMarkingData } from '@/components/marking/composables/useMarkingData'
|
||||
import MarkingLayout from '@/components/marking/MarkingLayout.vue'
|
||||
import { DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext, useMarkingContext } from '@/composables/marking/MarkingContext'
|
||||
import { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext, useMarkingContext } from '@/composables/marking/MarkingContext'
|
||||
import { provideMarkingHistory } from '@/composables/marking/useMarkingHistory'
|
||||
import { provideMarkingNavigation } from '@/composables/marking/useMarkingNavigation'
|
||||
import { useSafeArea } from '@/composables/useSafeArea'
|
||||
|
|
@ -473,7 +473,7 @@ function handleGlobalTouchEnd(e: TouchEvent) {
|
|||
|
||||
// 下一题的图片列表(用于预加载)
|
||||
const nextQuestionImages = computed(() => {
|
||||
return markingDataProvider.cachedImages.value
|
||||
return cachedImages.value
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -481,9 +481,6 @@ const nextQuestionImages = computed(() => {
|
|||
<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` }"
|
||||
@touchstart.capture="handleGlobalTouchStart"
|
||||
@touchmove.capture="handleGlobalTouchMove"
|
||||
@touchend.capture="handleGlobalTouchEnd"
|
||||
>
|
||||
<!-- 缩放提示 -->
|
||||
<view
|
||||
|
|
@ -496,6 +493,7 @@ const nextQuestionImages = computed(() => {
|
|||
>
|
||||
{{ Math.round(imageScale * 100) }}%
|
||||
</view>
|
||||
|
||||
<MarkingLayout
|
||||
:is-landscape="isLandscape"
|
||||
:is-fullscreen="false"
|
||||
|
|
@ -533,6 +531,9 @@ const nextQuestionImages = computed(() => {
|
|||
:next-question-images="nextQuestionImages"
|
||||
@ready="handleImageReady"
|
||||
@scroll-edge="handleScrollEdge"
|
||||
@touch-start="handleGlobalTouchStart"
|
||||
@touch-move="handleGlobalTouchMove"
|
||||
@touch-end="handleGlobalTouchEnd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in New Issue