refactor: 优化阅卷问题
continuous-integration/drone/push Build is passing Details

This commit is contained in:
AfyerCu 2025-11-21 21:05:19 +08:00
parent 785696659a
commit 58df855801
6 changed files with 202 additions and 54 deletions

View File

@ -6,6 +6,7 @@ import { useMarkingSettings } from '@/components/marking/composables/useMarkingS
import { useMarkingHistory } from '@/composables/marking/useMarkingHistory'
interface Props {
mode: string
isLandscape: boolean
currentQuestionIndex: number
totalQuestions: number
@ -135,6 +136,7 @@ const currentHistoryIndexInList = computed(() => {
<!-- 题目选择器 -->
<wd-picker
:model-value="currentQuestionIndex"
:disabled="mode === 'review'"
:columns="questionPickerColumns"
:value="String(currentQuestionIndex)"
:title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
@ -152,7 +154,11 @@ const currentHistoryIndexInList = computed(() => {
<view class="flex items-baseline gap-2">
<text class="text-16px text-slate-600 font-medium leading-none">
{{ isViewingHistory ? historyModeText : `${currentTaskSubmit}/${totalQuestions}` }}
{{
mode === 'review'
? '回评模式'
: (isViewingHistory ? historyModeText : `${Math.min(currentTaskSubmit, totalQuestions)}/${totalQuestions}`)
}}
</text>
</view>
</view>
@ -242,6 +248,7 @@ const currentHistoryIndexInList = computed(() => {
<!-- 题目选择器 -->
<wd-picker
:model-value="currentQuestionIndex"
:disabled="mode === 'review'"
:columns="questionPickerColumns"
:value="String(currentQuestionIndex)"
:title="`${currentQuestion?.question_major}.${currentQuestion?.question_minor}`"
@ -259,7 +266,7 @@ const currentHistoryIndexInList = computed(() => {
<view class="flex items-center gap-2">
<!-- 题目序号 -->
<text class="mr-2 text-14px text-slate-600 font-medium">
{{ currentTaskSubmit }}/{{ totalQuestions }}
{{ mode === 'review' ? '回评模式' : (isViewingHistory ? historyModeText : `${Math.min(currentTaskSubmit, totalQuestions)}/${totalQuestions}`) }}
</text>
<view class="flex gap-8px">
@ -311,12 +318,17 @@ const currentHistoryIndexInList = computed(() => {
<!-- 作答区域和打分区域 - 左右分栏 -->
<view class="relative flex flex-1 overflow-hidden">
<!-- 左侧图片区域 -->
<view class="relative flex-1 overflow-hidden bg-slate-50">
<view
class="relative flex-1 overflow-hidden bg-slate-50"
:class="settings.quickScorePosition === 'left' ? 'order-2' : 'order-1'"
>
<slot name="content" :current-question="currentQuestion" />
</view>
<!-- 右侧打分区域 -->
<slot name="scoring" />
<view :class="settings.quickScorePosition === 'left' ? 'order-1' : 'order-2'">
<slot name="scoring" />
</view>
</view>
</view>
</template>

View File

@ -252,8 +252,6 @@ function createMarkingData(options: UseMarkingDataOptions) {
}
// 重新获取下一题
questionData.value = []
currentMarkingSubmitData.value = []
markingStartTime.value = 0
await refetchQuestion()
processQuestionData()

View File

@ -8,6 +8,7 @@ import type {
ExamStudentMarkingQuestionResponse,
ExamStudentMarkingQuestionsResponse,
} from '@/api'
import { injectLocal, provideLocal } from '@vueuse/core'
import { examMarkingTaskApi } from '@/api'
// 分页响应接口
@ -265,14 +266,16 @@ export const MarkingContextSymbol = Symbol('MarkingContext')
*
*/
export function provideMarkingContext(context: MarkingContext) {
provide(MarkingContextSymbol, context)
provideLocal(MarkingContextSymbol, context)
return context
}
/**
* 使
*/
export function useMarkingContext(): MarkingContext | null {
return inject<MarkingContext>(MarkingContextSymbol) || null
return injectLocal<MarkingContext>(MarkingContextSymbol) || null
}
/**

View File

@ -0,0 +1,99 @@
import type {
MarkingDataProvider,
MarkingHistoryProvider,
MarkingHistoryRecord,
PaginatedResponse,
SubmitResponse,
} from './MarkingContext'
import type {
ExamBatchCreateMarkingTaskRecordRequest,
ExamStudentMarkingQuestionResponse,
} from '@/api'
import { ref } from 'vue'
import { examMarkingTaskApi } from '@/api'
import { provideMarkingContext } from './MarkingContext'
// Store for the question to be reviewed
const currentReviewQuestion = ref<ExamStudentMarkingQuestionResponse | null>(null)
/**
*
*/
export function setReviewQuestion(question: ExamStudentMarkingQuestionResponse) {
currentReviewQuestion.value = question
}
/**
*
*
*/
export class ReviewMarkingDataProvider implements MarkingDataProvider {
async getSingleQuestion(_taskId: number): Promise<ExamStudentMarkingQuestionResponse | null> {
return currentReviewQuestion.value
}
async getMultipleQuestions(
taskId: number,
_options: { count: number },
): Promise<{ questions: ExamStudentMarkingQuestionResponse[] } | null> {
const question = await this.getSingleQuestion(taskId)
return question ? { questions: [question] } : null
}
async submitRecords(data: ExamBatchCreateMarkingTaskRecordRequest): Promise<SubmitResponse | null> {
// 允许提交(重新打分)
const response = await examMarkingTaskApi.batchCreate({ ...data, is_review: true })
if (!response)
return null
return {
success_count: response.success_count || 0,
fail_data: response.fail_data?.map(item => ({ message: item.message || '' })),
success_data: response.success_data?.map(item => ({
id: item.id || 0,
question_id: item.question_id || 0,
score: item.score || 0,
remark: item.remark,
is_excellent: item.is_excellent || 0,
is_model: item.is_model || 0,
is_problem: item.is_problem || 0,
})),
is_end: response.is_end || false,
}
}
async createProblemRecord(data: any): Promise<void> {
await examMarkingTaskApi.problemRecordCreate(data)
}
}
/**
*
*
*/
export class ReviewMarkingHistoryProvider implements MarkingHistoryProvider {
async getHistoryPage(
_taskId: number,
_options: { page: number, page_size: number },
): Promise<PaginatedResponse<MarkingHistoryRecord>> {
return {
list: [],
page: 1,
page_size: 20,
total: 0,
has_more: false,
}
}
}
/**
*
*/
export function provideReviewMarkingContext() {
provideMarkingContext({
dataProvider: new ReviewMarkingDataProvider(),
historyProvider: new ReviewMarkingHistoryProvider(),
isHistory: false,
defaultPosition: 'last',
})
}

View File

@ -8,9 +8,10 @@
</route>
<script lang="ts" setup>
import type { MarkingDataProvider, MarkingHistoryProvider } from '@/composables/marking/MarkingContext'
import { useQueryClient } from '@tanstack/vue-query'
import { watchImmediate, whenever } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import AnswerDialog from '@/components/marking/components/dialog/AnswerDialog.vue'
import AvgScoreDialog from '@/components/marking/components/dialog/AvgScoreDialog.vue'
import FullscreenImageDialog from '@/components/marking/components/dialog/FullscreenImageDialog.vue'
@ -19,7 +20,8 @@ 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 { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext, useMarkingContext } from '@/composables/marking/MarkingContext'
import { cachedImages, DefaultMarkingDataProvider, DefaultMarkingHistoryProvider, provideMarkingContext } from '@/composables/marking/MarkingContext'
import { ReviewMarkingDataProvider, ReviewMarkingHistoryProvider } from '@/composables/marking/ReviewMarkingContext'
import { provideMarkingHistory } from '@/composables/marking/useMarkingHistory'
import { provideMarkingNavigation } from '@/composables/marking/useMarkingNavigation'
import { useSafeArea } from '@/composables/useSafeArea'
@ -29,6 +31,10 @@ defineOptions({
name: 'GradingPage',
})
const props = defineProps<{
mode: string
}>()
//
const { safeAreaInsets } = useSafeArea()
@ -134,11 +140,12 @@ async function recalculateScale() {
}
}
// 使
const markingDataProvider = new DefaultMarkingDataProvider()
provideMarkingContext({
const markingDataProvider = props.mode === 'review' ? new ReviewMarkingDataProvider() : new DefaultMarkingDataProvider()
const markingHistoryProvider = props.mode === 'review' ? new ReviewMarkingHistoryProvider() : new DefaultMarkingHistoryProvider()
const context = provideMarkingContext({
dataProvider: markingDataProvider,
historyProvider: new DefaultMarkingHistoryProvider(),
historyProvider: markingHistoryProvider,
isHistory: false,
defaultPosition: 'last',
})
@ -456,11 +463,15 @@ const historyModeText = computed(() => {
//
async function handlePrevQuestion() {
if (props.mode === 'review')
return
await goToPrevQuestion()
}
//
async function handleNextQuestion() {
if (props.mode === 'review')
return
await goToNextQuestion()
}
@ -571,20 +582,21 @@ const nextQuestionImages = computed(() => {
</view>
<MarkingLayout
:mode
:is-landscape="isLandscape"
:is-fullscreen="false"
:current-question-index="currentQuestionIndex"
:current-task-submit="navCurrentIndex || 0"
:total-questions="totalQuestions"
:questions="questionsList"
:my-score="myScore"
:avg-score="avgScore"
:my-score="Number(myScore)"
:avg-score="Number(avgScore)"
:is-viewing-history="isViewingHistory"
:can-go-prev="canGoPrev"
:can-go-next="canGoNext"
:history-mode-text="historyModeText"
:task-id="taskId"
:current-question-full-score="currentQuestion?.full_score || 0"
:current-question-full-score="fullScore"
:current-question-final-score="currentQuestion?.final_score || 0"
@go-back="goBack"
@select-question="selectQuestion"

View File

@ -11,11 +11,12 @@
import { useInfiniteQuery, useQuery } from '@tanstack/vue-query'
import { whenever } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { examMarkingTaskApi } from '@/api'
import ReviewHeader from '@/components/marking/review/ReviewHeader.vue'
import ReviewImageRenderer from '@/components/marking/review/ReviewImageRenderer.vue'
import ScoreEditDialog from '@/components/marking/review/ScoreEditDialog.vue'
import { setReviewQuestion } from '@/composables/marking/ReviewMarkingContext'
import { useUserId } from '@/composables/useUserId'
defineOptions({
@ -126,46 +127,69 @@ function goBack() {
const scoreEditDialogRef = ref<InstanceType<typeof ScoreEditDialog>>()
//
const containerWidth = ref(0)
// 0
// - padding(32) - padding(32) - (32)
const systemInfo = uni.getSystemInfoSync()
const containerWidth = ref(systemInfo.windowWidth - 96)
//
function updateContainerWidth() {
const query = uni.createSelectorQuery()
query.select('.image-container').boundingClientRect((data) => {
if (data && !Array.isArray(data) && data.width > 0) {
// padding
containerWidth.value = data.width - 32 // 32 = padding 16px * 2
console.log('[Review] Container width:', containerWidth.value)
}
}).exec()
}
whenever(() => historyData.value?.pages?.length, () => {
setTimeout(() => {
const query = uni.createSelectorQuery()
query.select('.image-container').boundingClientRect((data) => {
if (data && !Array.isArray(data)) {
// padding
containerWidth.value = data.width - 32 // 32 = padding 16px * 2
console.log('[Review] Container width:', containerWidth.value)
}
}).exec()
}, 100)
// 使 nextTick DOM
nextTick(() => {
//
setTimeout(updateContainerWidth, 200)
})
}, { immediate: true })
//
async function handleImageClick(record: any) {
try {
const result = await scoreEditDialogRef.value?.open({
currentScore: record.score,
recordTask: record,
})
let lastQuestionIndex = -1
//
function handleImageClick(record: any, index: number) {
//
const questionData = {
...record,
// tasks grading
tasks: {
initial: {
id: record.task_id,
question_id: record.question_id,
task_type: 'initial',
},
},
// standard_answer
standard_answer: record.standard_answer || '',
//
final_score: record.score,
}
if (result) {
//
await refetchHistory()
uni.showToast({
title: '分数修改成功',
icon: 'success',
})
}
}
catch (error) {
if (error !== 'cancel') {
console.error('修改分数失败:', error)
}
}
//
setReviewQuestion(questionData)
lastQuestionIndex = index
//
uni.navigateTo({
url: `/pages/marking/grading?taskId=${record.task_id}&questionId=${record.question_id}&examId=${examId.value}&subjectId=${subjectId.value}&type=single&taskType=initial&mode=review`,
})
}
const refreshKey = ref(0)
onShow(async () => {
if (lastQuestionIndex !== -1) {
await refetchHistory()
refreshKey.value += 1
}
})
//
function loadMore() {
if (hasNextPage.value && !isFetchingNextPage.value) {
@ -210,12 +234,12 @@ function handleRefresh() {
</view>
<!-- 历史记录列表 -->
<view v-else-if="historyList.length > 0" :key="containerWidth" class="history-list">
<view v-else-if="historyList.length > 0" class="history-list">
<view
v-for="record in historyList"
:key="record.id"
v-for="(record, index) in historyList"
:key="`${record.id}_${refreshKey}`"
class="history-item mb-4 rounded-3 bg-white p-4 shadow-sm"
@click="handleImageClick(record)"
@click="handleImageClick(record, index)"
>
<!-- 头部信息 -->
<view class="item-header mb-3 flex items-center justify-between">