| | |
| | | <!-- 底部按钮区域 --> |
| | | <view class="submit-action-area"> |
| | | <view class="btn cancel-btn" @tap="closeAnswer">稍后可答题</view> |
| | | <view class="btn confirm-btn" :class="{ disabled: quizSubmitting }" @tap="submitAnswer"> |
| | | {{ quizSubmitting ? '提交中...' : '提交' }} |
| | | <view class="btn confirm-btn" :class="{ disabled: submitButtonDisabled }" @tap="submitAnswer"> |
| | | {{ submitButtonText }} |
| | | </view> |
| | | </view> |
| | | <view class="submit-hint" v-if="submitHintText">{{ submitHintText }}</view> |
| | | </template> |
| | | |
| | | <!-- 答题完成结果区域 --> |
| | |
| | | const loadFailed = ref(false) |
| | | const quizLoading = ref(false) |
| | | const quizSubmitting = ref(false) |
| | | const quizNowTimestamp = ref(Date.now()) |
| | | const liveDetail = ref<LiveDetailResponse>(createEmptyLiveDetail()) |
| | | const quizState = ref<QuizStatusResponse>(createEmptyQuizState()) |
| | | const questions = ref<QuizQuestionItem[]>([]) |
| | |
| | | const instance = getCurrentInstance() |
| | | let videoContext: ReturnType<typeof uni.createVideoContext> | null = null |
| | | let playbackStartupTimer: ReturnType<typeof setTimeout> | null = null |
| | | let quizAvailabilityTimer: ReturnType<typeof setInterval> | null = null |
| | | |
| | | /** |
| | | * 生成观看页默认直播详情,避免模板访问空对象。 |
| | |
| | | }) |
| | | const watchCountText = computed(() => formatCount(liveDetail.value.watchCount)) |
| | | const rewardPointsText = computed(() => String(quizState.value.pointsReward || liveDetail.value.quizPoints || 0)) |
| | | const hasQuizQuestions = computed(() => questions.value.length > 0) |
| | | const isQuizTimeReached = computed(() => { |
| | | if (quizState.value.quizEnabled) { |
| | | return true |
| | | } |
| | | return isQuizAvailableByTime(quizState.value.quizAvailableAt) |
| | | }) |
| | | const answerButtonText = computed(() => { |
| | | if (quizState.value.quizPointsAwarded) { |
| | | return '已得分' |
| | |
| | | return '答题' |
| | | }) |
| | | const answerButtonDisabled = computed(() => !activeSessionId.value || quizSubmitting.value) |
| | | const isSubmitEnabled = computed(() => { |
| | | if (!activeSessionId.value || quizState.value.quizSubmitted) { |
| | | return false |
| | | } |
| | | return hasQuizQuestions.value && isQuizTimeReached.value |
| | | }) |
| | | const submitButtonDisabled = computed(() => !isSubmitEnabled.value || quizSubmitting.value) |
| | | const submitButtonText = computed(() => (quizSubmitting.value ? '提交中...' : '提交')) |
| | | const answerViewState = computed<QuizViewState>(() => { |
| | | if (quizState.value.quizSubmitted) { |
| | | if (quizState.value.quizPointsAwarded) { |
| | |
| | | } |
| | | return 'wrong' |
| | | } |
| | | if (quizState.value.quizEnabled) { |
| | | if (hasQuizQuestions.value) { |
| | | return 'answering' |
| | | } |
| | | return 'unopened' |
| | |
| | | return `开放时间:${formatDateLabel(quizState.value.quizAvailableAt)}` |
| | | } |
| | | return '' |
| | | }) |
| | | const submitHintText = computed(() => { |
| | | if (quizState.value.quizSubmitted || !hasQuizQuestions.value) { |
| | | return '' |
| | | } |
| | | if (isQuizTimeReached.value) { |
| | | return '' |
| | | } |
| | | if (quizState.value.quizAvailableAt) { |
| | | return `答题开放时间:${formatDateLabel(quizState.value.quizAvailableAt)}` |
| | | } |
| | | return '答题暂未开放' |
| | | }) |
| | | const videoPlaceholderText = computed(() => { |
| | | if (loading.value) { |
| | |
| | | */ |
| | | onHide(() => { |
| | | clearPlaybackStartupTimer() |
| | | clearQuizAvailabilityTimer() |
| | | submitCurrentWatchSegment() |
| | | }) |
| | | |
| | |
| | | */ |
| | | onUnload(() => { |
| | | clearPlaybackStartupTimer() |
| | | clearQuizAvailabilityTimer() |
| | | submitCurrentWatchSegment() |
| | | }) |
| | | |
| | |
| | | ? response.data.pointsReward |
| | | : Number(liveDetail.value.quizPoints || 0), |
| | | } |
| | | quizNowTimestamp.value = Date.now() |
| | | questions.value = createQuizQuestions(response.data.questions) |
| | | startQuizAvailabilityTimer() |
| | | return true |
| | | } finally { |
| | | quizLoading.value = false |
| | |
| | | * 重置答题状态,避免切换场次时沿用上一场直播数据。 |
| | | */ |
| | | function resetQuizState() { |
| | | clearQuizAvailabilityTimer() |
| | | quizLoading.value = false |
| | | quizSubmitting.value = false |
| | | quizNowTimestamp.value = Date.now() |
| | | quizState.value = createEmptyQuizState() |
| | | questions.value = [] |
| | | } |
| | | |
| | | /** |
| | | * 清理答题开放时间轮询,避免页面切换后重复计时。 |
| | | */ |
| | | function clearQuizAvailabilityTimer() { |
| | | if (quizAvailabilityTimer) { |
| | | clearInterval(quizAvailabilityTimer) |
| | | quizAvailabilityTimer = null |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 在答题开放前启动本地时间轮询,到点后自动启用提交按钮。 |
| | | */ |
| | | function startQuizAvailabilityTimer() { |
| | | clearQuizAvailabilityTimer() |
| | | if (!quizState.value.quizAvailableAt || quizState.value.quizSubmitted || isQuizTimeReached.value) { |
| | | return |
| | | } |
| | | quizAvailabilityTimer = setInterval(() => { |
| | | quizNowTimestamp.value = Date.now() |
| | | if (isQuizTimeReached.value) { |
| | | clearQuizAvailabilityTimer() |
| | | } |
| | | }, 1000) |
| | | } |
| | | |
| | | /** |
| | |
| | | const normalizedValue = value.includes('T') ? value : value.replace(' ', 'T') |
| | | const parsedTime = new Date(normalizedValue).getTime() |
| | | return Number.isNaN(parsedTime) ? 0 : parsedTime |
| | | } |
| | | |
| | | /** |
| | | * 根据本地当前时间判断是否已达到答题开放时间。 |
| | | */ |
| | | function isQuizAvailableByTime(value?: string): boolean { |
| | | if (!value) { |
| | | return false |
| | | } |
| | | const availableTimestamp = parsePlaybackStartTime(value) |
| | | if (!availableTimestamp) { |
| | | return false |
| | | } |
| | | return quizNowTimestamp.value >= availableTimestamp |
| | | } |
| | | |
| | | /** |
| | |
| | | * 记录当前题目的选项结果。 |
| | | */ |
| | | function selectOption(qIndex: number, oIndex: number) { |
| | | if (answerViewState.value !== 'answering') { |
| | | if (quizState.value.quizSubmitted || !hasQuizQuestions.value) { |
| | | return |
| | | } |
| | | const question = questions.value[qIndex] |
| | |
| | | * 提交观看页答题结果,并根据返回结果切换答题状态展示。 |
| | | */ |
| | | async function submitAnswer() { |
| | | if (answerViewState.value !== 'answering' || quizSubmitting.value) { |
| | | if (!isSubmitEnabled.value || quizSubmitting.value) { |
| | | return |
| | | } |
| | | const hasUnanswered = questions.value.some((item) => !item.selectedOption) |
| | |
| | | if (response?.code !== 0 || !response.data) { |
| | | return |
| | | } |
| | | clearQuizAvailabilityTimer() |
| | | applyQuizSubmitResult(response.data) |
| | | } finally { |
| | | quizSubmitting.value = false |
| | |
| | | |
| | | .submit-action-area { |
| | | padding: 24rpx 32rpx; |
| | | padding-bottom: 68rpx; |
| | | padding-bottom: 24rpx; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .submit-hint { |
| | | padding: 0 40rpx 68rpx; |
| | | font-size: 24rpx; |
| | | line-height: 36rpx; |
| | | color: #999999; |
| | | text-align: right; |
| | | } |
| | | |
| | | .result-container { |
| | | padding: 40rpx; |
| | | display: flex; |