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

View File

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

View File

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

View File

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