refactor: 优化阅卷问题
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
785696659a
commit
58df855801
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -252,8 +252,6 @@ function createMarkingData(options: UseMarkingDataOptions) {
|
|||
}
|
||||
|
||||
// 重新获取下一题
|
||||
questionData.value = []
|
||||
currentMarkingSubmitData.value = []
|
||||
markingStartTime.value = 0
|
||||
await refetchQuestion()
|
||||
processQuestionData()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue