refactor: 优化阅卷
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
65e2d19089
commit
cab94aaf6f
|
|
@ -38,7 +38,7 @@ function handlePageNavigation(targetPath?: string) {
|
|||
const isLoggedIn = userStore.isLogin && userStore.accessToken
|
||||
|
||||
// 如果未登录且不是公开页面,跳转到加载页
|
||||
if (!isLoggedIn && targetPath && !isPublicPage(targetPath)) {
|
||||
if (!isLoggedIn) {
|
||||
console.log('用户未登录,跳转到加载页')
|
||||
uni.reLaunch({
|
||||
url: '/pages/auth/index',
|
||||
|
|
@ -47,11 +47,12 @@ function handlePageNavigation(targetPath?: string) {
|
|||
}
|
||||
|
||||
// 如果已登录且访问的是登录相关页面,跳转到首页
|
||||
if (isLoggedIn && targetPath && isPublicPage(targetPath)) {
|
||||
if (isLoggedIn) {
|
||||
console.log('用户已登录,跳转到首页')
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index',
|
||||
})
|
||||
tabbarStore.setCurIdx(0)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup generic="T">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
list: T[]
|
||||
columns?: number
|
||||
itemSize?: number
|
||||
gap?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'change', list: T[]): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
columns: 5,
|
||||
itemSize: 32,
|
||||
gap: 8,
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const draggingIndex = ref<number>(-1)
|
||||
const hoverIndex = ref<number>(-1)
|
||||
const startPos = ref({ x: 0, y: 0 })
|
||||
const currentPos = ref({ x: 0, y: 0 })
|
||||
const isDragging = ref(false)
|
||||
|
||||
const containerWidth = computed(() => props.columns * props.itemSize + (props.columns - 1) * props.gap)
|
||||
const containerHeight = computed(() => {
|
||||
const rows = Math.ceil(props.list.length / props.columns)
|
||||
return rows * props.itemSize + (rows - 1) * props.gap
|
||||
})
|
||||
|
||||
function getPosition(index: number) {
|
||||
const row = Math.floor(index / props.columns)
|
||||
const col = index % props.columns
|
||||
return {
|
||||
x: col * (props.itemSize + props.gap),
|
||||
y: row * (props.itemSize + props.gap),
|
||||
}
|
||||
}
|
||||
|
||||
function getIndexFromPosition(x: number, y: number) {
|
||||
const col = Math.round(x / (props.itemSize + props.gap))
|
||||
const row = Math.round(y / (props.itemSize + props.gap))
|
||||
const index = row * props.columns + col
|
||||
return Math.max(0, Math.min(index, props.list.length - 1))
|
||||
}
|
||||
|
||||
function handleStart(e: any, index: number) {
|
||||
const touch = e.touches?.[0] || e.changedTouches?.[0] || e
|
||||
const pos = getPosition(index)
|
||||
|
||||
startPos.value = { x: touch.clientX - pos.x, y: touch.clientY - pos.y }
|
||||
currentPos.value = pos
|
||||
draggingIndex.value = index
|
||||
hoverIndex.value = index
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
function handleMove(e: any) {
|
||||
if (draggingIndex.value === -1) return
|
||||
|
||||
const touch = e.touches?.[0] || e.changedTouches?.[0] || e
|
||||
const x = touch.clientX - startPos.value.x
|
||||
const y = touch.clientY - startPos.value.y
|
||||
|
||||
currentPos.value = { x, y }
|
||||
|
||||
const newIndex = getIndexFromPosition(x, y)
|
||||
if (newIndex !== hoverIndex.value && newIndex >= 0 && newIndex < props.list.length) {
|
||||
hoverIndex.value = newIndex
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnd() {
|
||||
if (draggingIndex.value === -1) return
|
||||
|
||||
if (draggingIndex.value !== hoverIndex.value) {
|
||||
const newList = [...props.list]
|
||||
const [item] = newList.splice(draggingIndex.value, 1)
|
||||
newList.splice(hoverIndex.value, 0, item)
|
||||
emit('change', newList)
|
||||
}
|
||||
|
||||
draggingIndex.value = -1
|
||||
hoverIndex.value = -1
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function getItemStyle(index: number) {
|
||||
const pos = getPosition(index)
|
||||
const isBeingDragged = index === draggingIndex.value
|
||||
const isShifted = isDragging.value && index !== draggingIndex.value
|
||||
|
||||
let finalPos = pos
|
||||
if (isShifted) {
|
||||
if (draggingIndex.value < hoverIndex.value && index > draggingIndex.value && index <= hoverIndex.value) {
|
||||
finalPos = getPosition(index - 1)
|
||||
} else if (draggingIndex.value > hoverIndex.value && index < draggingIndex.value && index >= hoverIndex.value) {
|
||||
finalPos = getPosition(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (isBeingDragged) {
|
||||
finalPos = currentPos.value
|
||||
}
|
||||
|
||||
return {
|
||||
width: `${props.itemSize}px`,
|
||||
height: `${props.itemSize}px`,
|
||||
transform: `translate(${finalPos.x}px, ${finalPos.y}px)`,
|
||||
zIndex: isBeingDragged ? 10 : 1,
|
||||
opacity: isBeingDragged ? 0.8 : 1,
|
||||
transition: isBeingDragged ? 'none' : 'transform 0.2s ease, opacity 0.2s ease',
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="drag-sort-container"
|
||||
:style="{ width: containerWidth + 'px', height: containerHeight + 'px', position: 'relative' }"
|
||||
@touchmove.stop.prevent="handleMove"
|
||||
@touchend.stop="handleEnd"
|
||||
@touchcancel.stop="handleEnd"
|
||||
@mousemove.stop="handleMove"
|
||||
@mouseup.stop="handleEnd"
|
||||
@mouseleave.stop="handleEnd"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="drag-item"
|
||||
:style="getItemStyle(index)"
|
||||
@touchstart.stop="handleStart($event, index)"
|
||||
@mousedown.stop="handleStart($event, index)"
|
||||
>
|
||||
<slot name="item" :item="item" :index="index">
|
||||
{{ item }}
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drag-sort-container {
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drag-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -49,6 +49,15 @@ function getStatusTagType(status?: string): 'primary' | 'success' | 'warning' |
|
|||
|
||||
// 进入阅卷
|
||||
function handleEnterMarking(subject: SubjectInfo) {
|
||||
// 已统分的考试不能进入
|
||||
if (props.task.is_scored) {
|
||||
uni.showToast({
|
||||
title: '已完成,不能再进入',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.task.exam_id || !subject.subject_id) {
|
||||
uni.showToast({
|
||||
title: '参数错误',
|
||||
|
|
@ -84,22 +93,24 @@ function navigateToQuality(subjectId: number) {
|
|||
|
||||
<template>
|
||||
<view class="border border-slate-100 rounded-2xl bg-white p-4 shadow-md">
|
||||
<!-- 第一行:名称 + 时间 -->
|
||||
<!-- 第一行:名称 + 已完成标签 -->
|
||||
<view class="mb-4">
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-lg font-semibold">{{ task.exam_name }}</text>
|
||||
<wd-tag v-if="task.is_scored" type="success" size="small">已完成</wd-tag>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:类型 + ID + 时间 + 年级 -->
|
||||
<view class="flex items-center space-x-2">
|
||||
<view class="flex items-center rounded-md bg-blue-100 px-2">
|
||||
<text class="text-xs text-blue-600">{{ task.exam_type }}</text>
|
||||
</view>
|
||||
<text class="text-xs text-green-600">ID: {{ task.exam_id }}</text>
|
||||
<view class="flex items-center">
|
||||
<view class="i-mingcute:calendar-line mr-1 text-xs text-slate-500" />
|
||||
<text class="text-xs text-gray-500">{{ formatDate(task.exam_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:类型 + ID -->
|
||||
<view class="flex items-center space-x-2">
|
||||
<view class="flex items-center rounded-md bg-blue-100 px-2">
|
||||
<text class="text-xs text-blue-600"> {{ task.exam_type }}</text>
|
||||
</view>
|
||||
<text class="text-xs text-green-600">考试ID: {{ task.exam_id }}</text>
|
||||
<text v-if="task.grade" class="text-xs text-purple-600">{{ task.grade }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { useSessionStorage } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useMarkingData } from '@/components/marking/composables/useMarkingData'
|
||||
import { getSelectedOnekeyScores, useMarkingSettings } from '@/components/marking/composables/useMarkingSettings'
|
||||
import { getOnekeyScores, useMarkingSettings } from '@/components/marking/composables/useMarkingSettings'
|
||||
|
||||
interface Props {
|
||||
isLandscape?: boolean
|
||||
|
|
@ -37,8 +37,21 @@ const isCollapsed = ref(true)
|
|||
|
||||
// 悬浮按钮的位置 (使用 SessionStorage 持久化)
|
||||
const floatingButtonPosition = useSessionStorage('quick-score-submit-button-position', {
|
||||
x: 16,
|
||||
y: 16,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
// 初始化悬浮按钮位置到右下角
|
||||
onMounted(() => {
|
||||
if (floatingButtonPosition.value.x === 0 && floatingButtonPosition.value.y === 0) {
|
||||
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
|
||||
const buttonSize = 48 // 按钮大小
|
||||
const margin = 16 // 边距
|
||||
floatingButtonPosition.value = {
|
||||
x: windowWidth - buttonSize - margin,
|
||||
y: windowHeight - buttonSize - margin,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 拖动状态
|
||||
|
|
@ -91,14 +104,8 @@ function handleDragEnd() {
|
|||
// 显示的项目列表
|
||||
const displayItems = computed(() => {
|
||||
if (scoreMode.value === 'onekey') {
|
||||
// 一键打分:根据当前满分和用户选择动态获取分数
|
||||
const selectedScores = getSelectedOnekeyScores(
|
||||
settings.value.stepSize,
|
||||
props.fullScore,
|
||||
settings.value.onekeyScoreSelections,
|
||||
)
|
||||
|
||||
return selectedScores.map(score => ({
|
||||
const scores = getOnekeyScores(settings.value.stepSize, props.fullScore)
|
||||
return scores.map(score => ({
|
||||
type: 'score' as const,
|
||||
value: score,
|
||||
label: score.toString(),
|
||||
|
|
@ -107,7 +114,6 @@ const displayItems = computed(() => {
|
|||
}))
|
||||
}
|
||||
else {
|
||||
// 快捷打分:返回加减分按钮
|
||||
const isAdd = currentMode.value === 'add'
|
||||
return settings.value.quickScoreScores.map((step) => {
|
||||
const isActive = settings.value.quickScoreClickMode && settings.value.quickScoreClickValue === step
|
||||
|
|
@ -281,7 +287,7 @@ async function submitCurrentScore() {
|
|||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="sticky bottom-0 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"
|
||||
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
|
||||
|
|
@ -354,7 +360,7 @@ async function submitCurrentScore() {
|
|||
<!-- 收起/展开按钮 - 仅在一键打分模式显示 -->
|
||||
<div
|
||||
v-if="scoreMode === 'onekey'"
|
||||
class="sticky bottom-0 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"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import DragSort from '@/components/common/DragSort.vue'
|
||||
import {
|
||||
generateOnekeyScores,
|
||||
getSelectedOnekeyScores,
|
||||
updateOnekeyScoreSelection,
|
||||
getOnekeyScores,
|
||||
resetOnekeyScores,
|
||||
setOnekeyScores,
|
||||
useMarkingSettings,
|
||||
} from '@/components/marking/composables/useMarkingSettings'
|
||||
import LDrag from '@/uni_modules/lime-drag/components/l-drag/l-drag.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
|
|
@ -24,6 +24,41 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 屏幕和布局计算
|
||||
const screenInfo = ref({ width: 375, height: 667, isPortrait: true })
|
||||
const dragLayout = computed(() => {
|
||||
const { width, height, isPortrait } = screenInfo.value
|
||||
const dialogWidth = isPortrait ? width * 0.9 : Math.min(width * 0.5, 600)
|
||||
const itemSize = 32 // 2em = 32px (假设 1em = 16px)
|
||||
const gap = 8 // 0.5em = 8px
|
||||
const buttonWidth = 80 // 重新排序按钮宽度
|
||||
const containerPadding = 16 // 1em padding
|
||||
const availableWidth = dialogWidth - containerPadding * 2 - buttonWidth - gap
|
||||
const columns = Math.floor((availableWidth + gap) / (itemSize + gap))
|
||||
|
||||
return {
|
||||
columns: Math.max(columns, 3),
|
||||
itemSize,
|
||||
containerWidth: availableWidth,
|
||||
}
|
||||
})
|
||||
|
||||
// 获取屏幕信息
|
||||
function updateScreenInfo() {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
screenInfo.value = {
|
||||
width: res.windowWidth,
|
||||
height: res.windowHeight,
|
||||
isPortrait: res.windowHeight > res.windowWidth,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化屏幕信息
|
||||
updateScreenInfo()
|
||||
|
||||
// 弹窗显示状态
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
|
|
@ -38,18 +73,18 @@ const initialSettings = ref({
|
|||
quickScoreMode: '',
|
||||
stepSize: 0,
|
||||
quickScoreScores: [] as number[],
|
||||
onekeyScoreSelections: {} as Record<string, boolean>,
|
||||
onekeyScores: [] as number[],
|
||||
})
|
||||
|
||||
// 监听弹窗打开,保存初始值
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal) {
|
||||
// 保存当前值作为初始值
|
||||
updateScreenInfo()
|
||||
initialSettings.value = {
|
||||
quickScoreMode: settings.value.quickScoreMode,
|
||||
stepSize: settings.value.stepSize,
|
||||
quickScoreScores: [...settings.value.quickScoreScores],
|
||||
onekeyScoreSelections: { ...settings.value.onekeyScoreSelections },
|
||||
onekeyScores: [...getOnekeyScores(settings.value.stepSize, props.fullScore)],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -65,14 +100,21 @@ const activeTab = computed({
|
|||
// 步长选项
|
||||
const stepOptions = [0.5, 1, 2, 3, 4, 5]
|
||||
|
||||
// 生成所有可能的分数(用于选择)
|
||||
function generateAllScores(stepSize: number, fullScore: number): number[] {
|
||||
const scores: number[] = []
|
||||
for (let i = 0; i <= fullScore; i += stepSize) {
|
||||
scores.push(i)
|
||||
}
|
||||
return scores
|
||||
}
|
||||
|
||||
// 常用给分选项 - 根据当前选项卡动态计算
|
||||
const commonScores = computed(() => {
|
||||
if (activeTab.value === 'onekey') {
|
||||
// 一键打分模式:根据步长和满分动态生成
|
||||
return generateOnekeyScores(settings.value.stepSize, props.fullScore)
|
||||
return generateAllScores(settings.value.stepSize, props.fullScore)
|
||||
}
|
||||
else {
|
||||
// 快捷打分模式:固定的步长选项
|
||||
return [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5]
|
||||
}
|
||||
})
|
||||
|
|
@ -80,7 +122,6 @@ const commonScores = computed(() => {
|
|||
// 选择步长
|
||||
function selectStepSize(step: number) {
|
||||
settings.value.stepSize = step
|
||||
// 步长变化时不需要特殊处理,分数会自动重新计算
|
||||
}
|
||||
|
||||
// 自定义步长输入
|
||||
|
|
@ -90,12 +131,7 @@ const customStepValue = ref('')
|
|||
// 已选择的常用给分
|
||||
const selectedCommonScores = computed(() => {
|
||||
if (activeTab.value === 'onekey') {
|
||||
// 一键打分:根据用户选择状态和当前满分动态计算
|
||||
return getSelectedOnekeyScores(
|
||||
settings.value.stepSize,
|
||||
props.fullScore,
|
||||
settings.value.onekeyScoreSelections,
|
||||
)
|
||||
return getOnekeyScores(settings.value.stepSize, props.fullScore)
|
||||
}
|
||||
else {
|
||||
return settings.value.quickScoreScores
|
||||
|
|
@ -112,38 +148,18 @@ watch(selectedCommonScores, (newScores) => {
|
|||
|
||||
// 更新拖拽排序
|
||||
function updateDraggableOrder(newOrder: number[]) {
|
||||
if (activeTab.value === 'onekey') {
|
||||
draggableScores.value = newOrder
|
||||
|
||||
// 一键打分:更新选择状态以反映新的排序
|
||||
// 先清除当前步长和满分的所有选择
|
||||
const currentKey = `${settings.value.stepSize}_${props.fullScore}`
|
||||
Object.keys(settings.value.onekeyScoreSelections).forEach((key) => {
|
||||
if (key.startsWith(currentKey)) {
|
||||
delete settings.value.onekeyScoreSelections[key]
|
||||
}
|
||||
})
|
||||
|
||||
// 重新设置选中的分数
|
||||
newOrder.forEach((score) => {
|
||||
updateOnekeyScoreSelection(
|
||||
settings.value.stepSize,
|
||||
props.fullScore,
|
||||
score,
|
||||
true,
|
||||
settings.value.onekeyScoreSelections,
|
||||
)
|
||||
})
|
||||
draggableScores.value = newOrder
|
||||
if (activeTab.value === 'quick') {
|
||||
settings.value.quickScoreScores = newOrder
|
||||
}
|
||||
else {
|
||||
settings.value.quickScoreScores = newOrder
|
||||
setOnekeyScores(settings.value.stepSize, props.fullScore, newOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理拖拽变化
|
||||
function handleDragChange(newList) {
|
||||
console.log('newList', newList)
|
||||
updateDraggableOrder(newList.map(i => i.content))
|
||||
function handleDragChange(newList: number[]) {
|
||||
updateDraggableOrder(newList)
|
||||
}
|
||||
|
||||
// 快捷打分步长限制检查
|
||||
|
|
@ -157,15 +173,15 @@ const canAddMoreSteps = computed(() => {
|
|||
// 切换常用给分选择
|
||||
function toggleCommonScore(score: number) {
|
||||
if (activeTab.value === 'onekey') {
|
||||
// 一键打分:更新选择状态
|
||||
const isCurrentlySelected = selectedCommonScores.value.includes(score)
|
||||
updateOnekeyScoreSelection(
|
||||
settings.value.stepSize,
|
||||
props.fullScore,
|
||||
score,
|
||||
!isCurrentlySelected,
|
||||
settings.value.onekeyScoreSelections,
|
||||
)
|
||||
const current = getOnekeyScores(settings.value.stepSize, props.fullScore)
|
||||
const index = current.indexOf(score)
|
||||
if (index > -1) {
|
||||
current.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
current.push(score)
|
||||
}
|
||||
setOnekeyScores(settings.value.stepSize, props.fullScore, current)
|
||||
}
|
||||
else {
|
||||
const currentScores = [...settings.value.quickScoreScores]
|
||||
|
|
@ -174,7 +190,6 @@ function toggleCommonScore(score: number) {
|
|||
currentScores.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
// 检查快捷打分步长限制
|
||||
if (currentScores.length >= 5) {
|
||||
uni.showToast({
|
||||
title: '快捷打分最多5个步长',
|
||||
|
|
@ -257,13 +272,11 @@ function cancelCustomInput() {
|
|||
|
||||
// 重新排序(从小到大)
|
||||
function resetSortOrder() {
|
||||
const sortedScores = [...selectedCommonScores.value].sort((a, b) => a - b)
|
||||
if (activeTab.value === 'onekey') {
|
||||
// 一键打分:重新排序不需要特殊处理,因为draggableScores会自动处理
|
||||
const sortedScores = [...selectedCommonScores.value].sort((a, b) => a - b)
|
||||
updateDraggableOrder(sortedScores)
|
||||
setOnekeyScores(settings.value.stepSize, props.fullScore, sortedScores)
|
||||
}
|
||||
else {
|
||||
const sortedScores = [...settings.value.quickScoreScores].sort((a, b) => a - b)
|
||||
settings.value.quickScoreScores = sortedScores
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +291,7 @@ function cancel() {
|
|||
settings.value.quickScoreMode = initialSettings.value.quickScoreMode
|
||||
settings.value.stepSize = initialSettings.value.stepSize
|
||||
settings.value.quickScoreScores = [...initialSettings.value.quickScoreScores]
|
||||
settings.value.onekeyScoreSelections = { ...initialSettings.value.onekeyScoreSelections }
|
||||
setOnekeyScores(settings.value.stepSize, props.fullScore, initialSettings.value.onekeyScores)
|
||||
close()
|
||||
}
|
||||
|
||||
|
|
@ -292,17 +305,13 @@ function confirm() {
|
|||
}
|
||||
|
||||
function resetOnekeyScoreScores() {
|
||||
// 清除当前步长和满分的所有选择状态,恢复默认(全选)
|
||||
const currentKey = `${settings.value.stepSize}_${props.fullScore}`
|
||||
Object.keys(settings.value.onekeyScoreSelections).forEach((key) => {
|
||||
if (key.startsWith(currentKey)) {
|
||||
delete settings.value.onekeyScoreSelections[key]
|
||||
}
|
||||
})
|
||||
resetOnekeyScores(settings.value.stepSize, props.fullScore)
|
||||
draggableScores.value = [...getOnekeyScores(settings.value.stepSize, props.fullScore)]
|
||||
}
|
||||
|
||||
function resetQuickScoreScores() {
|
||||
settings.value.quickScoreScores = [0.5, 1, 2, 3, 4, 5]
|
||||
settings.value.quickScoreScores = [0.5, 1, 2, 3, 4]
|
||||
draggableScores.value = [0.5, 1, 2, 3, 4]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -313,7 +322,7 @@ function resetQuickScoreScores() {
|
|||
:close-on-click-modal="false"
|
||||
custom-class="score-settings-popup"
|
||||
>
|
||||
<view class="root-container">
|
||||
<view class="max-h-90vh overflow-auto root-container">
|
||||
<view class="w-full rounded-2 bg-white p-1em">
|
||||
<!-- 标题栏 -->
|
||||
<view class="mb-1em flex items-center justify-between">
|
||||
|
|
@ -413,21 +422,8 @@ function resetQuickScoreScores() {
|
|||
|
||||
<!-- 给分排序 -->
|
||||
<view class="mb-1em">
|
||||
<text class="text-0.875em text-gray-700">给分排序</text>
|
||||
<view class="mt-0.5em flex items-center justify-between gap-0.5em">
|
||||
<l-drag
|
||||
:list="draggableScores"
|
||||
class="flex-1"
|
||||
:column="8"
|
||||
grid-height="2em"
|
||||
@change="handleDragChange"
|
||||
>
|
||||
<template #grid="{ content }">
|
||||
<view class="h-2em w-2em flex flex-shrink-0 cursor-move items-center justify-center rounded-full bg-blue-500 text-0.75em text-white font-medium transition-all active:(scale-110 bg-blue-600)">
|
||||
{{ content }}
|
||||
</view>
|
||||
</template>
|
||||
</l-drag>
|
||||
<view class="mb-0.5em flex items-center justify-between">
|
||||
<text class="text-0.875em text-gray-700">给分排序</text>
|
||||
<wd-button
|
||||
type="error"
|
||||
size="small"
|
||||
|
|
@ -438,6 +434,19 @@ function resetQuickScoreScores() {
|
|||
重新排序
|
||||
</wd-button>
|
||||
</view>
|
||||
<DragSort
|
||||
:list="draggableScores"
|
||||
:columns="dragLayout.columns"
|
||||
:item-size="dragLayout.itemSize"
|
||||
:gap="8"
|
||||
@change="handleDragChange"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<view class="h-full w-full flex items-center justify-center rounded-full bg-blue-500 text-0.75em text-white font-medium">
|
||||
{{ item }}
|
||||
</view>
|
||||
</template>
|
||||
</DragSort>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -492,21 +501,8 @@ function resetQuickScoreScores() {
|
|||
|
||||
<!-- 给分排序 -->
|
||||
<view class="mb-1em">
|
||||
<text class="text-0.875em text-gray-700">给分排序</text>
|
||||
<view class="mt-0.5em flex items-center justify-between gap-0.5em">
|
||||
<LDrag
|
||||
:list="draggableScores"
|
||||
class="flex-1"
|
||||
:column="8"
|
||||
grid-height="2em"
|
||||
@change="handleDragChange"
|
||||
>
|
||||
<template #grid="{ content }">
|
||||
<view class="h-2em w-2em flex flex-shrink-0 cursor-move items-center justify-center rounded-full bg-blue-500 text-0.75em text-white font-medium transition-all active:(scale-110 bg-blue-600)">
|
||||
{{ content }}
|
||||
</view>
|
||||
</template>
|
||||
</LDrag>
|
||||
<view class="mb-0.5em flex items-center justify-between">
|
||||
<text class="text-0.875em text-gray-700">给分排序</text>
|
||||
<wd-button
|
||||
type="error"
|
||||
size="small"
|
||||
|
|
@ -517,6 +513,19 @@ function resetQuickScoreScores() {
|
|||
重新排序
|
||||
</wd-button>
|
||||
</view>
|
||||
<DragSort
|
||||
:list="draggableScores"
|
||||
:columns="dragLayout.columns"
|
||||
:item-size="dragLayout.itemSize"
|
||||
:gap="8"
|
||||
@change="handleDragChange"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<view class="h-full w-full flex items-center justify-center rounded-full bg-blue-500 text-0.75em text-white font-medium">
|
||||
{{ item }}
|
||||
</view>
|
||||
</template>
|
||||
</DragSort>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -539,7 +548,13 @@ function resetQuickScoreScores() {
|
|||
|
||||
<style lang="scss">
|
||||
.score-settings-popup {
|
||||
width: max(90vw, 400px);
|
||||
/* 竖屏(高>宽)占90%,横屏占50%或最大600px */
|
||||
@media (orientation: portrait) {
|
||||
width: 90vw;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
width: min(50vw, 600px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 根容器设置 font-size,1rem 最大 16px */
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import QuestionRenderer from './QuestionRenderer.vue'
|
|||
interface Props {
|
||||
imageSize: number
|
||||
questionData: ExamStudentMarkingQuestionResponse[]
|
||||
nextQuestionImages?: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
|
@ -15,13 +16,55 @@ const props = defineProps<Props>()
|
|||
const emit = defineEmits<{
|
||||
'marking-change': [questionIndex: number, imageIndex: number, data: DomMarkingData]
|
||||
'ready': [imageInfo: { width: number, height: number }]
|
||||
'scroll-edge': [atLeftEdge: boolean, atRightEdge: boolean]
|
||||
}>()
|
||||
|
||||
const scale = defineModel<number>('scale', { default: 1.0 })
|
||||
|
||||
// 滚动边缘状态
|
||||
const atLeftEdge = ref(true)
|
||||
const atRightEdge = ref(true)
|
||||
|
||||
// 第一张图片的尺寸信息
|
||||
const firstImageSize = ref({ width: 0, height: 0 })
|
||||
|
||||
// 滚动容器ID(用于 uni.createSelectorQuery)
|
||||
const scrollContainerId = `scroll-container-${Math.random().toString(36).slice(2)}`
|
||||
|
||||
/**
|
||||
* 检查滚动边缘状态(使用 uni.createSelectorQuery)
|
||||
*/
|
||||
function checkScrollEdge() {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.select(`#${scrollContainerId}`).scrollOffset()
|
||||
query.select(`#${scrollContainerId}`).boundingClientRect()
|
||||
query.exec((res) => {
|
||||
if (!res || res.length < 2) return
|
||||
|
||||
const scrollInfo = res[0]
|
||||
const rectInfo = res[1]
|
||||
|
||||
if (!scrollInfo || !rectInfo) return
|
||||
|
||||
const scrollLeft = scrollInfo.scrollLeft || 0
|
||||
const scrollWidth = scrollInfo.scrollWidth || 0
|
||||
const clientWidth = rectInfo.width || 0
|
||||
|
||||
// 允许1px的误差
|
||||
atLeftEdge.value = scrollLeft <= 1
|
||||
atRightEdge.value = scrollLeft + clientWidth >= scrollWidth - 1
|
||||
|
||||
emit('scroll-edge', atLeftEdge.value, atRightEdge.value)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听滚动事件
|
||||
*/
|
||||
function handleScroll() {
|
||||
checkScrollEdge()
|
||||
}
|
||||
|
||||
// 转换为渲染数据
|
||||
const questionDataList = computed(() => {
|
||||
if (!props.questionData?.length)
|
||||
|
|
@ -83,22 +126,37 @@ watch(
|
|||
() => props.questionData,
|
||||
async () => {
|
||||
await loadFirstImageSize()
|
||||
// 数据变化后重新检查滚动边缘
|
||||
nextTick(() => {
|
||||
checkScrollEdge()
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听scale变化,重新检查滚动边缘
|
||||
watch(
|
||||
() => scale.value,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
checkScrollEdge()
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// 暴露方法和数据给父组件
|
||||
defineExpose({
|
||||
firstImageSize,
|
||||
atLeftEdge,
|
||||
atRightEdge,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<scroll-view
|
||||
ref="containerRef"
|
||||
scroll-x
|
||||
scroll-y
|
||||
class="h-full w-full"
|
||||
<div
|
||||
:id="scrollContainerId"
|
||||
class="h-full w-full overflow-auto"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<!-- 题目列表 -->
|
||||
<view
|
||||
|
|
@ -126,5 +184,15 @@ defineExpose({
|
|||
@marking-change="(imageIndex, data) => handleMarkingChange(questionIndex, imageIndex, data)"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 预加载下一题图片 -->
|
||||
<view v-if="nextQuestionImages?.length" class="hidden">
|
||||
<image
|
||||
v-for="(url, index) in nextQuestionImages"
|
||||
:key="`preload_${index}`"
|
||||
:src="url"
|
||||
class="w-0 h-0"
|
||||
/>
|
||||
</view>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ defineExpose({
|
|||
class="absolute left-2 top-2 z-10 flex items-baseline"
|
||||
:style="{ gap: '2px', textShadow: '1px 1px 2px rgba(0, 0, 0, 0.3)' }"
|
||||
>
|
||||
<text class="text-3xl text-red-600 font-bold">{{ currentMarkingData.score || question.finalScore }}</text>
|
||||
<text class="text-3xl text-red-600 font-bold">{{ question.finalScore || currentMarkingData.score }}</text>
|
||||
<text class="text-lg text-red-500 font-semibold">/{{ question.fullScore }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Ref } from 'vue'
|
||||
import type { ExamBatchCreateMarkingTaskRecordRequest, ExamCreateMarkingTaskRecordRequest, ExamQuestionWithTasksResponse, ExamSetProblemRecordRequest, ExamStudentMarkingQuestionResponse } from '@/api'
|
||||
import type { MarkingNavigation } from '@/composables/marking/useMarkingNavigation'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, inject, provide, readonly, ref, watch } from 'vue'
|
||||
import { examMarkingTaskApi } from '@/api'
|
||||
|
|
@ -26,10 +27,11 @@ export interface UseMarkingDataOptions {
|
|||
subjectId?: Ref<number>
|
||||
isLandscape?: Ref<boolean>
|
||||
taskType?: Ref<string>
|
||||
markingNavigation: MarkingNavigation
|
||||
}
|
||||
|
||||
function createMarkingData(options: UseMarkingDataOptions) {
|
||||
const { taskId, questionId, examId, subjectId, isLandscape, taskType } = options
|
||||
const { taskId, questionId, examId, subjectId, isLandscape, taskType, markingNavigation } = options
|
||||
|
||||
// 获取阅卷上下文和历史管理
|
||||
const markingContext = getMarkingContext()
|
||||
|
|
@ -178,44 +180,18 @@ function createMarkingData(options: UseMarkingDataOptions) {
|
|||
// 提交单个记录
|
||||
const submitSingleRecord = async (data?: MarkingSubmitData, index: number = 0) => {
|
||||
try {
|
||||
const question = questionData.value[index]
|
||||
if (!question)
|
||||
throw new Error('题目数据不存在')
|
||||
|
||||
const currentData = data || currentMarkingSubmitData.value[index]
|
||||
const isHistoryMode = markingHistory.isViewingHistory.value
|
||||
|
||||
// 如果是问题卷,只提交问题卷记录,不提交正常阅卷记录
|
||||
if (currentData.isProblem) {
|
||||
const problemRequest: ExamSetProblemRecordRequest = {
|
||||
id: question.id!,
|
||||
task_id: taskId.value,
|
||||
problem_type: currentData.problemType! as any,
|
||||
problem_addition: currentData.problemRemark,
|
||||
}
|
||||
// 根据模式选择数据源
|
||||
const question = isHistoryMode
|
||||
? markingHistory.currentHistoryRecord.value!
|
||||
: questionData.value[index]
|
||||
|
||||
if (markingContext.dataProvider.createProblemRecord) {
|
||||
await markingContext.dataProvider.createProblemRecord(problemRequest as any)
|
||||
}
|
||||
if (!question)
|
||||
throw new Error('题目数据不存在')
|
||||
|
||||
isSubmitted.value = true
|
||||
|
||||
// 如果是历史模式,刷新历史记录
|
||||
if (isHistoryMode) {
|
||||
await markingHistory.forceRefreshHistory()
|
||||
}
|
||||
else {
|
||||
currentTaskInfo.value.marked_quantity += 1
|
||||
// 重新获取下一题
|
||||
questionData.value = []
|
||||
await refetchQuestion()
|
||||
processQuestionData()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 正常阅卷记录提交
|
||||
// 构建提交数据
|
||||
const submitData: ExamCreateMarkingTaskRecordRequest = {
|
||||
id: question.id,
|
||||
scan_info_id: question.scan_info_id!,
|
||||
|
|
@ -228,42 +204,65 @@ function createMarkingData(options: UseMarkingDataOptions) {
|
|||
page_mode: 'single',
|
||||
is_excellent: currentData.isExcellent ? 1 : 0,
|
||||
is_model: currentData.isTypical ? 1 : 0,
|
||||
is_problem: 0, // 问题卷已经在上面处理了,这里必须是0
|
||||
is_problem: currentData.isProblem ? 1 : 0,
|
||||
}
|
||||
|
||||
const batchRequest: ExamBatchCreateMarkingTaskRecordRequest = {
|
||||
batch_data: [submitData],
|
||||
}
|
||||
// 如果是问题卷,不提交正常记录,只提交问题卷记录
|
||||
const response = !currentData.isProblem
|
||||
? await markingContext.dataProvider.submitRecords({
|
||||
is_review: isHistoryMode,
|
||||
batch_data: [submitData],
|
||||
})
|
||||
: null
|
||||
|
||||
// 使用上下文提供者提交
|
||||
const response = await markingContext.dataProvider.submitRecords(batchRequest)
|
||||
// 处理问题卷
|
||||
if (currentData.isProblem) {
|
||||
const problemRequest: ExamSetProblemRecordRequest = {
|
||||
id: question.id!,
|
||||
task_id: taskId.value,
|
||||
problem_type: currentData.problemType! as any,
|
||||
problem_addition: currentData.problemRemark,
|
||||
}
|
||||
|
||||
if (markingContext.dataProvider.createProblemRecord) {
|
||||
await markingContext.dataProvider.createProblemRecord(problemRequest as any)
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
isSubmitted.value = true
|
||||
if (response.success_count === 0) {
|
||||
const errorMessage = response.fail_data?.map(item => item.message).join('、') || '提交失败'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// 更新历史记录总数
|
||||
if (!isHistoryMode && response.success_count > 0) {
|
||||
markingHistory.incrementHistoryCount(response.success_count)
|
||||
}
|
||||
}
|
||||
|
||||
isSubmitted.value = true
|
||||
|
||||
// 如果是历史模式,刷新历史记录
|
||||
if (!isHistoryMode) {
|
||||
// 更新已阅数量
|
||||
if (currentTaskInfo.value) {
|
||||
currentTaskInfo.value.marked_quantity = (currentTaskInfo.value.marked_quantity || 0) + 1
|
||||
}
|
||||
|
||||
// 如果是历史模式,刷新历史记录
|
||||
if (isHistoryMode) {
|
||||
await markingHistory.forceRefreshHistory()
|
||||
}
|
||||
else {
|
||||
// 重新获取下一题
|
||||
questionData.value = []
|
||||
await refetchQuestion()
|
||||
processQuestionData()
|
||||
}
|
||||
|
||||
return response
|
||||
// 重新获取下一题
|
||||
questionData.value = []
|
||||
currentMarkingSubmitData.value = []
|
||||
markingStartTime.value = 0
|
||||
await refetchQuestion()
|
||||
processQuestionData()
|
||||
}
|
||||
else {
|
||||
(question as any).final_score = currentData.score
|
||||
markingNavigation.goToNextQuestion()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('提交阅卷记录失败:', error)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const SETTINGS_VERSION = 2 // 当前版本号
|
||||
|
||||
export interface MarkingSettings {
|
||||
version?: number
|
||||
// 图片位置
|
||||
imagePosition: 'left' | 'center' | 'right'
|
||||
// 背景颜色
|
||||
|
|
@ -42,8 +45,8 @@ export interface MarkingSettings {
|
|||
quickScoreMode: 'onekey' | 'quick'
|
||||
quickScoreLayout: 'single' | 'double'
|
||||
|
||||
// 一键打分相关设置 - 保存用户自定义的选择状态(哪些分数被选中)
|
||||
onekeyScoreSelections: Record<string, boolean> // key: "stepSize_fullScore", value: 是否选中该分数
|
||||
// 一键打分配置:key 为 "stepSize_fullScore",value 为排序后的分数数组
|
||||
onekeyScoreConfigs: Record<string, number[]>
|
||||
|
||||
// 缩放控制面板相关设置
|
||||
showScalePanel: boolean
|
||||
|
|
@ -52,6 +55,7 @@ export interface MarkingSettings {
|
|||
|
||||
// 默认设置
|
||||
const defaultSettings: MarkingSettings = {
|
||||
version: SETTINGS_VERSION,
|
||||
imagePosition: 'left',
|
||||
backgroundColor: '#F0F0F0',
|
||||
keyboardSize: 'normal',
|
||||
|
|
@ -87,78 +91,65 @@ const defaultSettings: MarkingSettings = {
|
|||
quickScoreScores: [1, 2],
|
||||
quickScoreLayout: 'single',
|
||||
|
||||
// 一键打分默认值 - 用户选择状态
|
||||
onekeyScoreSelections: {},
|
||||
// 一键打分默认值
|
||||
onekeyScoreConfigs: {},
|
||||
|
||||
// 缩放控制面板默认值
|
||||
showScalePanel: true,
|
||||
scalePanelCollapsed: false,
|
||||
}
|
||||
|
||||
export const markingSettings = useStorage<MarkingSettings>('marking_settings', defaultSettings)
|
||||
const rawSettings = useStorage<MarkingSettings>('marking_settings', defaultSettings)
|
||||
|
||||
// 版本检查和迁移
|
||||
if (!rawSettings.value.version || rawSettings.value.version < SETTINGS_VERSION) {
|
||||
console.log(`Settings version mismatch (${rawSettings.value.version} < ${SETTINGS_VERSION}), resetting to defaults`)
|
||||
rawSettings.value = { ...defaultSettings }
|
||||
}
|
||||
|
||||
export const markingSettings = rawSettings
|
||||
|
||||
/**
|
||||
* 生成一键打分的分数选项
|
||||
* @param stepSize 步长
|
||||
* @param fullScore 满分
|
||||
* @returns 分数数组
|
||||
* 生成默认的一键打分分数数组(按步长从0到满分)
|
||||
*/
|
||||
export function generateOnekeyScores(stepSize: number, fullScore: number): number[] {
|
||||
function generateDefaultScores(stepSize: number, fullScore: number): number[] {
|
||||
const scores: number[] = []
|
||||
const max = Math.max(fullScore, 2)
|
||||
|
||||
for (let i = 0; i <= max; i += stepSize) {
|
||||
for (let i = 0; i <= fullScore; i += stepSize) {
|
||||
scores.push(i)
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一键打分的选中分数
|
||||
* @param stepSize 步长
|
||||
* @param fullScore 满分
|
||||
* @param selections 用户选择状态
|
||||
* @returns 选中的分数数组
|
||||
* 获取一键打分配置的 key
|
||||
*/
|
||||
export function getSelectedOnekeyScores(
|
||||
stepSize: number,
|
||||
fullScore: number,
|
||||
selections: Record<string, boolean> = {},
|
||||
): number[] {
|
||||
const allScores = generateOnekeyScores(stepSize, fullScore)
|
||||
const selectionKey = `${stepSize}_${fullScore}`
|
||||
|
||||
// 如果没有保存的选择状态,默认全选
|
||||
const hasSelections = Object.keys(selections).some(key => key.startsWith(`${stepSize}_`))
|
||||
if (!hasSelections) {
|
||||
return allScores
|
||||
}
|
||||
|
||||
// 根据保存的选择状态过滤
|
||||
return allScores.filter((score) => {
|
||||
const scoreKey = `${selectionKey}_${score}`
|
||||
return selections[scoreKey] !== false // 默认选中,除非明确设置为false
|
||||
})
|
||||
function getConfigKey(stepSize: number, fullScore: number): string {
|
||||
return `${stepSize}_${fullScore}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一键打分分数的选择状态
|
||||
* @param stepSize 步长
|
||||
* @param fullScore 满分
|
||||
* @param score 分数
|
||||
* @param selected 是否选中
|
||||
* @param selections 选择状态对象
|
||||
* 获取一键打分的分数列表(已排序、已过滤)
|
||||
*/
|
||||
export function updateOnekeyScoreSelection(
|
||||
stepSize: number,
|
||||
fullScore: number,
|
||||
score: number,
|
||||
selected: boolean,
|
||||
selections: Record<string, boolean>,
|
||||
): void {
|
||||
const selectionKey = `${stepSize}_${fullScore}_${score}`
|
||||
selections[selectionKey] = selected
|
||||
export function getOnekeyScores(stepSize: number, fullScore: number): number[] {
|
||||
const key = getConfigKey(stepSize, fullScore)
|
||||
const config = markingSettings.value.onekeyScoreConfigs[key]
|
||||
return config || generateDefaultScores(stepSize, fullScore)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一键打分的分数列表
|
||||
*/
|
||||
export function setOnekeyScores(stepSize: number, fullScore: number, scores: number[]): void {
|
||||
const key = getConfigKey(stepSize, fullScore)
|
||||
markingSettings.value.onekeyScoreConfigs[key] = scores
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置一键打分配置为默认值
|
||||
*/
|
||||
export function resetOnekeyScores(stepSize: number, fullScore: number): void {
|
||||
const key = getConfigKey(stepSize, fullScore)
|
||||
delete markingSettings.value.onekeyScoreConfigs[key]
|
||||
}
|
||||
|
||||
export function useMarkingSettings() {
|
||||
|
|
|
|||
|
|
@ -104,28 +104,6 @@ export interface MarkingContext {
|
|||
defaultPosition?: 'first' | 'last' // 默认定位:first-第一个,last-最后一个
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载图片资源(提升用户体验)
|
||||
*/
|
||||
async function preloadImages(urls: string[]): Promise<void> {
|
||||
if (!urls || urls.length === 0)
|
||||
return
|
||||
|
||||
// 移动端使用 uni.getImageInfo 预加载
|
||||
const imagePromises = urls.map((url) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: () => resolve(),
|
||||
fail: () => resolve(), // 预加载失败不影响主流程
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.all(imagePromises)
|
||||
console.log(`🖼️ 已预加载 ${urls.length} 张图片`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认的阅卷数据提供者(使用原有的API)
|
||||
* 带滑动窗口缓存优化
|
||||
|
|
@ -137,6 +115,8 @@ 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) {
|
||||
|
|
@ -196,7 +176,7 @@ export class DefaultMarkingDataProvider implements MarkingDataProvider {
|
|||
|
||||
// 预加载图片资源
|
||||
if (this.cachedQuestion.questions?.[0]?.image_urls) {
|
||||
preloadImages(this.cachedQuestion.questions[0].image_urls)
|
||||
this.cachedImages.value = this.cachedQuestion.questions?.[0]?.image_urls || []
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -121,8 +121,8 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
|||
* 手势配置
|
||||
*/
|
||||
const swipeConfig = {
|
||||
threshold: 50, // 最小滑动距离(px)
|
||||
timeout: 100, // 最大滑动时间(ms)
|
||||
threshold: 40, // 最小滑动距离(px)
|
||||
timeout: 200, // 最大滑动时间(ms)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -261,7 +261,7 @@ function createMarkingNavigation(_props: MarkingNavigationProps) {
|
|||
}
|
||||
}
|
||||
|
||||
type MarkingNavigation = ReturnType<typeof createMarkingNavigation>
|
||||
export type MarkingNavigation = ReturnType<typeof createMarkingNavigation>
|
||||
const MarkingNavigationSymbol = Symbol('MarkingNavigation')
|
||||
|
||||
export function provideMarkingNavigation(props: MarkingNavigationProps) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CustomRequestOptions } from '@/http/types'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
export interface Response<T> {
|
||||
code?: number
|
||||
|
|
@ -17,7 +18,7 @@ export function http<T = unknown>(options: CustomRequestOptions): Promise<T exte
|
|||
// #endif
|
||||
// 响应成功
|
||||
success(res) {
|
||||
// 状态码 2xx,参考 axios 的设计
|
||||
console.log('res===>', res)
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
const responseData = res.data as Response<T>
|
||||
|
||||
|
|
@ -43,9 +44,10 @@ export function http<T = unknown>(options: CustomRequestOptions): Promise<T exte
|
|||
}
|
||||
}
|
||||
else if (res.statusCode === 401) {
|
||||
const userStore = useUserStore()
|
||||
// 401错误 -> 清理用户信息,跳转到登录页
|
||||
// userStore.clearUserInfo()
|
||||
// uni.navigateTo({ url: '/pages/login/login' })
|
||||
userStore.logOut(true)
|
||||
uni.navigateTo({ url: '/pages/login/login' })
|
||||
reject(res)
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -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 } from '@/composables/marking/MarkingContext'
|
||||
import { 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'
|
||||
|
|
@ -50,6 +50,11 @@ const imageViewerRef = ref<InstanceType<typeof MarkingImageViewerNew>>()
|
|||
|
||||
// 容器尺寸信息
|
||||
const containerSize = ref({ width: 0, height: 0 })
|
||||
const hasInitializedScale = ref(false)
|
||||
|
||||
// 滚动边缘状态
|
||||
const atLeftEdge = ref(true)
|
||||
const atRightEdge = ref(true)
|
||||
|
||||
/**
|
||||
* 获取容器尺寸
|
||||
|
|
@ -100,8 +105,11 @@ function calculateInitialScale(imageWidth: number, imageHeight: number): number
|
|||
* 处理图片就绪事件
|
||||
*/
|
||||
function handleImageReady(imageInfo: { width: number, height: number }) {
|
||||
if (hasInitializedScale.value)
|
||||
return
|
||||
const initialScale = calculateInitialScale(imageInfo.width, imageInfo.height)
|
||||
imageScale.value = initialScale
|
||||
hasInitializedScale.value = true
|
||||
console.log('Initial scale calculated:', initialScale, 'for image:', imageInfo)
|
||||
}
|
||||
|
||||
|
|
@ -131,8 +139,9 @@ async function recalculateScale() {
|
|||
}
|
||||
|
||||
// 提供阅卷上下文(使用默认实现)
|
||||
const markingDataProvider = new DefaultMarkingDataProvider()
|
||||
provideMarkingContext({
|
||||
dataProvider: new DefaultMarkingDataProvider(),
|
||||
dataProvider: markingDataProvider,
|
||||
historyProvider: new DefaultMarkingHistoryProvider(),
|
||||
isHistory: false,
|
||||
defaultPosition: 'last',
|
||||
|
|
@ -163,6 +172,7 @@ const markingData = provideMarkingData({
|
|||
subjectId,
|
||||
isLandscape,
|
||||
taskType,
|
||||
markingNavigation,
|
||||
})
|
||||
const { questionData: questions, questionsList, totalQuestions: totalQuestionsCount } = markingData
|
||||
|
||||
|
|
@ -396,6 +406,14 @@ async function handleNextQuestion() {
|
|||
await goToNextQuestion()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚动边缘变化
|
||||
*/
|
||||
function handleScrollEdge(left: boolean, right: boolean) {
|
||||
atLeftEdge.value = left
|
||||
atRightEdge.value = right
|
||||
}
|
||||
|
||||
// 创建手势处理器
|
||||
const swipeHandler = createSwipeHandler()
|
||||
|
||||
|
|
@ -422,7 +440,8 @@ function handleGlobalTouchStart(e: TouchEvent) {
|
|||
initialDistance.value = getDistance(e.touches[0], e.touches[1])
|
||||
initialScale.value = imageScale.value
|
||||
}
|
||||
else {
|
||||
else if (atLeftEdge.value || atRightEdge.value) {
|
||||
// 只有在边缘时才允许手势切换
|
||||
swipeHandler.onTouchStart(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -445,10 +464,17 @@ function handleGlobalTouchMove(e: TouchEvent) {
|
|||
*/
|
||||
function handleGlobalTouchEnd(e: TouchEvent) {
|
||||
if (e.touches.length < 2) {
|
||||
swipeHandler.onTouchEnd(e)
|
||||
if (atLeftEdge.value || atRightEdge.value) {
|
||||
swipeHandler.onTouchEnd(e)
|
||||
}
|
||||
isGesturing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下一题的图片列表(用于预加载)
|
||||
const nextQuestionImages = computed(() => {
|
||||
return markingDataProvider.cachedImages.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -504,7 +530,9 @@ function handleGlobalTouchEnd(e: TouchEvent) {
|
|||
v-model:scale="imageScale"
|
||||
:question-data="[currentQuestion]"
|
||||
:image-size="100"
|
||||
:next-question-images="nextQuestionImages"
|
||||
@ready="handleImageReady"
|
||||
@scroll-edge="handleScrollEdge"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -512,7 +540,7 @@ function handleGlobalTouchEnd(e: TouchEvent) {
|
|||
<!-- 打分区域(横屏和竖屏共用) -->
|
||||
<template #scoring>
|
||||
<QuickScorePanel
|
||||
v-if="currentQuestion && !isViewingHistory"
|
||||
v-if="currentQuestion"
|
||||
:is-landscape="isLandscape"
|
||||
:full-score="currentQuestion.full_score"
|
||||
@score-selected="handleQuickScoreSelect"
|
||||
|
|
|
|||
|
|
@ -630,6 +630,10 @@ export type ExamMarkingTaskResponse = {
|
|||
exam_name?: string;
|
||||
/** 考试类型 */
|
||||
exam_type?: string;
|
||||
/** 年级 */
|
||||
grade?: string;
|
||||
/** 是否已统分 */
|
||||
is_scored?: boolean;
|
||||
/** 科目列表 */
|
||||
subjects?: SubjectInfo[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export const useUserStore = defineStore(
|
|||
* 退出登录
|
||||
* 清空所有用户相关状态
|
||||
*/
|
||||
const logOut = () => {
|
||||
const logOut = (fromRequest = false) => {
|
||||
// 清空用户信息
|
||||
info.value = {}
|
||||
// 重置登录状态
|
||||
|
|
@ -132,7 +132,9 @@ export const useUserStore = defineStore(
|
|||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('token')
|
||||
|
||||
toast.success('退出成功')
|
||||
if (!fromRequest) {
|
||||
toast.success('退出成功')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue