72% 的提交一次就通过,每次通过之后学生都要回首页或再问一遍题号去找下一道。 - 结果面板在通过时显示 LessonNext:取 /me/class-activity(老师布置的优先,否则推断), 给出排在当前题后面、还没通过的第一道,后面都做完了再从头找;全做完了显示「都做完了」 并指向题单。当前题不在列表里、比赛、题单入口都不显示 - 评价弹窗关掉后重新打开结果面板:点弹窗时面板会被当成点了外面收起来,「下一题」就在面板里 - 换题(同一组件只换路由参数,有了题号直达和下一题之后成了常规操作)时收掉上一道题的 结果面板、语法错误、还在跟的提交(新增 useSubmissionMonitor.reset),并取消通过后 那两个 1.5 秒的定时器——否则会给还没做的新题弹评价,上一道判完时还会把新题标成已解决 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6bf845e06
commit
a63bf6ea07
4 files changed
+141
-3
No files matched your search
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getClassActivity } from "oj/api"
|
||||||
|
import type { ClassActivity } from "utils/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过之后告诉学生「这节课的下一题是哪道」。
|
||||||
|
*
|
||||||
|
* 72% 的提交一次就通过,每次通过之后学生都得回首页、或者再问一遍题号去找下一道。
|
||||||
|
* 这节课的题就是首页「班里在做」那几道(老师在课堂看板布置的,或者从同班提交推断的),
|
||||||
|
* 这里直接拿同一个接口,给出下一道没做完的。
|
||||||
|
*
|
||||||
|
* 「下一道」是按列表顺序排在这道**后面**、还没通过的第一道,后面都做完了再从头找 ——
|
||||||
|
* 学生常常跳着做,先做完了第 3 道再回头做第 1 道。当前这道本身不看它的 myStatus:
|
||||||
|
* 接口是通过之前那一刻的状态,这时候它刚刚才通过。
|
||||||
|
*
|
||||||
|
* 这道题不在这节课的列表里(学生在做别的)就什么都不显示。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 当前这道题的展示题号 */
|
||||||
|
problemDisplayId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activity = ref<ClassActivity | null>(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
activity.value = await getClassActivity().catch(() => null)
|
||||||
|
})
|
||||||
|
|
||||||
|
const lesson = computed(() => {
|
||||||
|
const list = activity.value?.problems ?? []
|
||||||
|
const current = props.problemDisplayId.toLowerCase()
|
||||||
|
const index = list.findIndex((item) => item.problemDisplayId.toLowerCase() === current)
|
||||||
|
if (index < 0) return null
|
||||||
|
const rest = [...list.slice(index + 1), ...list.slice(0, index)].filter(
|
||||||
|
(item) => item.myStatus !== "accepted",
|
||||||
|
)
|
||||||
|
return { total: list.length, next: rest[0] ?? null, remaining: rest.length }
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = computed(() => (activity.value?.source === "teacher" ? "老师布置的题" : "这节课的题"))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card v-if="lesson" size="small" embedded class="lesson-next">
|
||||||
|
<n-flex v-if="lesson.next" align="center" justify="space-between" :wrap="false">
|
||||||
|
<n-text depth="3">{{ label }}还剩 {{ lesson.remaining }} 道</n-text>
|
||||||
|
<router-link :to="`/problem/${lesson.next.problemDisplayId}`">
|
||||||
|
<n-button type="primary">
|
||||||
|
下一题:{{ lesson.next.problemDisplayId }} {{ lesson.next.title }}
|
||||||
|
</n-button>
|
||||||
|
</router-link>
|
||||||
|
</n-flex>
|
||||||
|
<n-flex v-else align="center" justify="space-between">
|
||||||
|
<n-text>{{ label }}都做完了,一共 {{ lesson.total }} 道 🎉</n-text>
|
||||||
|
<router-link to="/problemset">
|
||||||
|
<n-button secondary>去题单继续练</n-button>
|
||||||
|
</router-link>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lesson-next {
|
||||||
|
margin-top: 12px;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 题目名可能很长(带 emoji 的、二十几个字的),按钮别把结果面板撑宽 */
|
||||||
|
.lesson-next :deep(.n-button__content) {
|
||||||
|
display: block;
|
||||||
|
max-width: 320px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,6 +17,7 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import PythonErrorExplain from "./PythonErrorExplain.vue"
|
import PythonErrorExplain from "./PythonErrorExplain.vue"
|
||||||
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
|
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
|
||||||
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
|
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
|
||||||
|
import LessonNext from "./LessonNext.vue"
|
||||||
import { useAIStream } from "shared/composables/aiStream"
|
import { useAIStream } from "shared/composables/aiStream"
|
||||||
import { submitHintFeedback } from "oj/api"
|
import { submitHintFeedback } from "oj/api"
|
||||||
import { MdPreview } from "md-editor-v3"
|
import { MdPreview } from "md-editor-v3"
|
||||||
@@ -121,6 +122,18 @@ const sampleCheck = computed(() => {
|
|||||||
return submission.statisticInfo?.sample_check ?? null
|
return submission.statisticInfo?.sample_check ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过之后给「这节课的下一题」。比赛有自己的题目列表;从题单入口进来的,通过后
|
||||||
|
* 1.5 秒会自动跳回题单(SubmitCode 的 goToProblemSetDelayed),这两种都不给
|
||||||
|
*/
|
||||||
|
const route = useRoute()
|
||||||
|
const showLessonNext = computed(
|
||||||
|
() =>
|
||||||
|
props.submission?.result === SubmissionStatus.accepted &&
|
||||||
|
problemStore.problem?.contestId == null &&
|
||||||
|
!route.params.problemSetId,
|
||||||
|
)
|
||||||
|
|
||||||
const msg = computed(() => {
|
const msg = computed(() => {
|
||||||
if (!props.submission) return ""
|
if (!props.submission) return ""
|
||||||
// 走 PythonErrorExplain / RuntimeErrorExplain 那两张中文卡片
|
// 走 PythonErrorExplain / RuntimeErrorExplain 那两张中文卡片
|
||||||
@@ -288,6 +301,12 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
<!-- key 用提交 id:同一道题再交一次、又通过了,要重新拉一遍「还剩几道」 -->
|
||||||
|
<LessonNext
|
||||||
|
v-if="showLessonNext && problemStore.problem"
|
||||||
|
:key="submission.id"
|
||||||
|
:problem-display-id="problemStore.problem._id"
|
||||||
|
/>
|
||||||
<n-flex
|
<n-flex
|
||||||
vertical
|
vertical
|
||||||
v-if="
|
v-if="
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ const [commentPanel] = useToggle()
|
|||||||
|
|
||||||
function closeCommentPanel() {
|
function closeCommentPanel() {
|
||||||
commentPanel.value = false
|
commentPanel.value = false
|
||||||
|
// 点评价弹窗时,结果面板会当成「点了外面」收起来 —— 而「下一题」就在面板里
|
||||||
|
// (LessonNext),评价完得把它重新打开
|
||||||
|
showResult.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
@@ -45,7 +48,14 @@ const { isDesktop } = useBreakpoints()
|
|||||||
const { celebrate } = useFireworks()
|
const { celebrate } = useFireworks()
|
||||||
|
|
||||||
// ==================== 判题监控 ====================
|
// ==================== 判题监控 ====================
|
||||||
const { submission, judging, pending, submitting, startMonitoring } = useSubmissionMonitor()
|
const {
|
||||||
|
submission,
|
||||||
|
judging,
|
||||||
|
pending,
|
||||||
|
submitting,
|
||||||
|
startMonitoring,
|
||||||
|
reset: resetSubmission,
|
||||||
|
} = useSubmissionMonitor()
|
||||||
|
|
||||||
const showResult = ref(false)
|
const showResult = ref(false)
|
||||||
const isFormatting = ref(false)
|
const isFormatting = ref(false)
|
||||||
@@ -69,7 +79,7 @@ const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
|||||||
// ==================== AC 后弹出点评轮盘 ====================
|
// ==================== AC 后弹出点评轮盘 ====================
|
||||||
// 只对已经能评价、且还没评过的人弹:后端要求先有 AC 才收评价,这里刚 AC 完正好;
|
// 只对已经能评价、且还没评过的人弹:后端要求先有 AC 才收评价,这里刚 AC 完正好;
|
||||||
// mine 非 null 说明早就评过了,别再打扰。
|
// mine 非 null 说明早就评过了,别再打扰。
|
||||||
const { start: showCommentPanelDelayed } = useTimeoutFn(
|
const { start: showCommentPanelDelayed, stop: cancelCommentPanel } = useTimeoutFn(
|
||||||
async () => {
|
async () => {
|
||||||
const res = await getReaction(problem.value!.id)
|
const res = await getReaction(problem.value!.id)
|
||||||
if (res.mine === null) {
|
if (res.mine === null) {
|
||||||
@@ -80,7 +90,7 @@ const { start: showCommentPanelDelayed } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
const { start: goToProblemSetDelayed } = useTimeoutFn(
|
const { start: goToProblemSetDelayed, stop: cancelGoToProblemSet } = useTimeoutFn(
|
||||||
() => {
|
() => {
|
||||||
router.push({
|
router.push({
|
||||||
name: "problemset",
|
name: "problemset",
|
||||||
@@ -93,6 +103,27 @@ const { start: goToProblemSetDelayed } = useTimeoutFn(
|
|||||||
{ immediate: false },
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换题时把上一道题的东西全收掉:结果面板、提交前的语法错误、还在跟的那条提交,以及
|
||||||
|
* 通过之后那两个 1.5 秒的定时器 —— 评价弹窗是按**当前**题目去查、去弹的,学生通过后
|
||||||
|
* 马上点了「下一题」,不取消的话就会给一道还没做的题弹评价。
|
||||||
|
*
|
||||||
|
* 题目页换题是同一个组件复用(只换路由参数),以前几乎只有退回列表再点进来这一条路,
|
||||||
|
* 有了顶栏题号直达和结果面板里的「下一题」之后,这成了常规操作。
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => problem.value?._id,
|
||||||
|
(next, previous) => {
|
||||||
|
if (!previous || next === previous) return
|
||||||
|
showResult.value = false
|
||||||
|
syntaxErrorInfo.value = ""
|
||||||
|
commentPanel.value = false
|
||||||
|
cancelCommentPanel()
|
||||||
|
cancelGoToProblemSet()
|
||||||
|
resetSubmission()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
const buttonState = computed(() =>
|
const buttonState = computed(() =>
|
||||||
getSubmitButtonState({
|
getSubmitButtonState({
|
||||||
|
|||||||
@@ -129,6 +129,18 @@ export function useSubmissionMonitor() {
|
|||||||
startPollingFallback()
|
startPollingFallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 换题的时候丢掉当前这条提交。题目页换题(顶栏题号直达、「下一题」)是同一个组件
|
||||||
|
* 复用、只换路由参数,不清的话上一道题的结果面板原样挂在新题上;而且上一道还在判的话,
|
||||||
|
* 判完触发的「通过」处理会把**新题**标成已解决。提交本身照常判完落库,只是这边不再跟。
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
pausePolling()
|
||||||
|
unsubscribe()
|
||||||
|
submissionId.value = ""
|
||||||
|
submission.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
const judging = computed(() => submission.value?.result === SubmissionStatus.judging)
|
const judging = computed(() => submission.value?.result === SubmissionStatus.judging)
|
||||||
|
|
||||||
@@ -159,5 +171,6 @@ export function useSubmissionMonitor() {
|
|||||||
// 方法
|
// 方法
|
||||||
startMonitoring,
|
startMonitoring,
|
||||||
pausePolling,
|
pausePolling,
|
||||||
|
reset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user