借小丑牌结算时一张张牌亮起来:结果里每组测试点一个方块,从左往右依次亮, 对的绿、错的红并写组号,不到 1 秒亮完,提示和按钮亮完才出来,全对时亮完再放烟花。 大字和「通过几个」判完马上就有;只播刚交的那条,刷新、看旧提交、系统减少动画都直接全亮。 提交详情多给每组对没对(只有对错,不带数据),比赛照旧不给。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a173f75d7
commit
36b3cd6b21
8 files changed
+375
-144
No files matched your search
@@ -372,8 +372,12 @@ function caseSummary(submission: typeof schema.submission.$inferSelect) {
|
|||||||
if (submission.contestId !== null || submission.language === "SQL") return null
|
if (submission.contestId !== null || submission.language === "SQL") return null
|
||||||
const data = asRecord(submission.info).data
|
const data = asRecord(submission.info).data
|
||||||
if (!Array.isArray(data) || data.length === 0) return null
|
if (!Array.isArray(data) || data.length === 0) return null
|
||||||
const passed = data.filter((item) => asRecord(item).result === JudgeStatus.ACCEPTED).length
|
// 判题时已经按组号排过(judge/run.ts),这里再排一次:老数据不一定是排好的
|
||||||
return { passed, total: data.length }
|
const results = data
|
||||||
|
.map((item) => asRecord(item))
|
||||||
|
.sort((a, b) => Number(a.test_case) - Number(b.test_case))
|
||||||
|
.map((item) => item.result === JudgeStatus.ACCEPTED)
|
||||||
|
return { passed: results.filter(Boolean).length, total: results.length, results }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表行的 caseSummary:SQL 里数好的两个数,为 null 的情形和详情一致(SQL 题、没有逐点结果) */
|
/** 列表行的 caseSummary:SQL 里数好的两个数,为 null 的情形和详情一致(SQL 题、没有逐点结果) */
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import { caseRevealStep } from "../utils/caseReveal"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每组测试点一个方块:对的绿、错的红并写上第几组(设计稿「测试点亮起来」)。
|
||||||
|
* `animate` 时从左往右一格一格亮,亮完发 done;不播就一下全亮、马上发 done。
|
||||||
|
* 只在挂上的那一刻看 animate —— 播到一半它变了也不打断。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
results: boolean[]
|
||||||
|
animate: boolean
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{ (e: "done"): void }>()
|
||||||
|
|
||||||
|
const theme = useThemeVars()
|
||||||
|
const shown = ref(props.animate ? 0 : props.results.length)
|
||||||
|
const playing = ref(props.animate)
|
||||||
|
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.animate) {
|
||||||
|
emit("done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timer = setInterval(() => {
|
||||||
|
shown.value++
|
||||||
|
if (shown.value >= props.results.length) {
|
||||||
|
stop()
|
||||||
|
emit("done")
|
||||||
|
}
|
||||||
|
}, caseRevealStep(props.results.length))
|
||||||
|
})
|
||||||
|
function stop() {
|
||||||
|
if (timer) clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
playing.value = false
|
||||||
|
}
|
||||||
|
onBeforeUnmount(stop)
|
||||||
|
|
||||||
|
const failed = computed(() => props.results.flatMap((passed, i) => (passed ? [] : [i + 1])))
|
||||||
|
const label = computed(() => {
|
||||||
|
const passed = props.results.length - failed.value.length
|
||||||
|
const wrong = failed.value.length ? `,第 ${failed.value.join("、")} 组错了` : ""
|
||||||
|
return `${props.results.length} 组测试点通过 ${passed} 组${wrong}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="blocks" role="img" :aria-label="label">
|
||||||
|
<span
|
||||||
|
v-for="(passed, i) in results"
|
||||||
|
:key="i"
|
||||||
|
class="block"
|
||||||
|
:class="{
|
||||||
|
wait: i >= shown,
|
||||||
|
ok: i < shown && passed,
|
||||||
|
bad: i < shown && !passed,
|
||||||
|
now: playing && i === shown - 1,
|
||||||
|
}"
|
||||||
|
>{{ i < shown && !passed ? i + 1 : "" }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.blocks {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.wait {
|
||||||
|
background-color: v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.ok {
|
||||||
|
background-color: v-bind("theme.successColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.bad {
|
||||||
|
background-color: v-bind("theme.errorColor");
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 刚亮的那格外面一圈,看得出「亮到哪了」 */
|
||||||
|
.block.now {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px v-bind("theme.cardColor"),
|
||||||
|
0 0 0 4px v-bind("theme.successColorSuppl");
|
||||||
|
}
|
||||||
|
|
||||||
|
.block.now.bad {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px v-bind("theme.cardColor"),
|
||||||
|
0 0 0 4px v-bind("theme.errorColorSuppl");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import CaseBlocks from "./CaseBlocks.vue"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结果页签里每一块结果的标题:圆形状态图标 + 大字 + 灰色小字,下面可以挂一条测试点进度。
|
* 结果页签里每一块结果的标题:圆形状态图标 + 大字 + 灰色小字,下面可以挂一条测试点进度。
|
||||||
@@ -12,7 +13,15 @@ const props = defineProps<{
|
|||||||
sub?: string
|
sub?: string
|
||||||
/** 测试点进度:通过几个、一共几个 */
|
/** 测试点进度:通过几个、一共几个 */
|
||||||
progress?: { passed: number; total: number } | null
|
progress?: { passed: number; total: number } | null
|
||||||
|
/**
|
||||||
|
* 每组对没对:有就画一排方块代替上面那根进度条(设计稿「测试点亮起来」),
|
||||||
|
* 没有(老接口、SQL 题)才退回进度条
|
||||||
|
*/
|
||||||
|
cases?: boolean[] | null
|
||||||
|
/** 方块一格一格亮;不亮就一下全亮 */
|
||||||
|
animate?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
const emit = defineEmits<{ (e: "revealed"): void }>()
|
||||||
|
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
|
|
||||||
@@ -90,8 +99,15 @@ const color = computed(() => {
|
|||||||
<span v-if="sub" class="sub">{{ sub }}</span>
|
<span v-if="sub" class="sub">{{ sub }}</span>
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
</div>
|
</div>
|
||||||
|
<CaseBlocks
|
||||||
|
v-if="cases && cases.length"
|
||||||
|
:results="cases"
|
||||||
|
:animate="!!animate"
|
||||||
|
class="cases"
|
||||||
|
@done="emit('revealed')"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="progress && progress.total > 0"
|
v-else-if="progress && progress.total > 0"
|
||||||
class="progress"
|
class="progress"
|
||||||
role="img"
|
role="img"
|
||||||
:aria-label="`通过 ${progress.passed}/${progress.total} 个测试点`"
|
:aria-label="`通过 ${progress.passed}/${progress.total} 个测试点`"
|
||||||
@@ -141,6 +157,10 @@ const color = computed(() => {
|
|||||||
color: v-bind("theme.textColor3");
|
color: v-bind("theme.textColor3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cases {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useLessonStore } from "shared/store/lesson"
|
|||||||
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
||||||
import { SubmissionStatus } from "utils/constants"
|
import { SubmissionStatus } from "utils/constants"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||||
|
import { caseRevealDuration } from "../utils/caseReveal"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判完之后「该发生什么」:失败计数、标成已解决、烟花、点评、回题单。
|
* 判完之后「该发生什么」:失败计数、标成已解决、烟花、点评、回题单。
|
||||||
@@ -222,6 +223,20 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这一条要播「测试点一格一格亮」的话,等它亮完。判完那一刻只有 WebSocket 推来的结果,
|
||||||
|
* 方块要等详情(带每组对错)回来才开始亮,所以先等详情,最多 3 秒
|
||||||
|
*/
|
||||||
|
async function afterCasesRevealed(id: string | undefined) {
|
||||||
|
if (!submissionStore.shouldRevealCases(id)) return
|
||||||
|
await until(() => submission.value?.id === id && submission.value?.code !== undefined).toBe(
|
||||||
|
true,
|
||||||
|
{ timeout: 3000 },
|
||||||
|
)
|
||||||
|
const count = submission.value?.caseSummary?.results?.length ?? 0
|
||||||
|
if (count) await new Promise((resolve) => setTimeout(resolve, caseRevealDuration(count)))
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== AC庆祝效果 ====================
|
// ==================== AC庆祝效果 ====================
|
||||||
watch(
|
watch(
|
||||||
() => submission.value?.result,
|
() => submission.value?.result,
|
||||||
@@ -238,14 +253,17 @@ watch(
|
|||||||
|
|
||||||
if (result !== SubmissionStatus.accepted) return
|
if (result !== SubmissionStatus.accepted) return
|
||||||
|
|
||||||
// 2. 放烟花
|
|
||||||
const fireworks = celebrate()
|
|
||||||
|
|
||||||
// 3. 烟花放完弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
// 3. 烟花放完弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
||||||
if (ctx.value.reviewAfterAccepted) {
|
// (评价是欠着的这件事马上记下:方块还在亮的时候离开这道题,也要先评)
|
||||||
reviewOwed.value = true
|
if (ctx.value.reviewAfterAccepted) reviewOwed.value = true
|
||||||
showCommentPanelAfter(fireworks)
|
|
||||||
}
|
// 2. 放烟花:等结果页签里的方块一格一格亮完(不到 1 秒)再放
|
||||||
|
const id = submission.value?.id
|
||||||
|
void afterCasesRevealed(id).then(() => {
|
||||||
|
if (submission.value?.id !== id) return
|
||||||
|
const fireworks = celebrate()
|
||||||
|
if (ctx.value.reviewAfterAccepted) showCommentPanelAfter(fireworks)
|
||||||
|
})
|
||||||
|
|
||||||
if (ctx.value.backToProblemSet) {
|
if (ctx.value.backToProblemSet) {
|
||||||
// 延迟回到题单页面
|
// 延迟回到题单页面
|
||||||
|
|||||||
@@ -214,12 +214,15 @@ const header = computed(() => {
|
|||||||
) {
|
) {
|
||||||
return { kind: "pending" as const, title: status.title, sub: "", progress: null }
|
return { kind: "pending" as const, title: status.title, sub: "", progress: null }
|
||||||
}
|
}
|
||||||
|
// 每组对没对:一排方块代替进度条(设计稿「测试点亮起来」),全对也画
|
||||||
|
const cases = submission.caseSummary?.results ?? null
|
||||||
if (submission.result === SubmissionStatus.accepted) {
|
if (submission.result === SubmissionStatus.accepted) {
|
||||||
return {
|
return {
|
||||||
kind: "success" as const,
|
kind: "success" as const,
|
||||||
title: status.title,
|
title: status.title,
|
||||||
sub: "所有测试点都对了",
|
sub: "所有测试点都对了",
|
||||||
progress: null,
|
progress: null,
|
||||||
|
cases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failedOnSample.value) {
|
if (failedOnSample.value) {
|
||||||
@@ -229,17 +232,42 @@ const header = computed(() => {
|
|||||||
// 和提交列表右栏同一句,「题目里的」是为了和测试点的编号分开
|
// 和提交列表右栏同一句,「题目里的」是为了和测试点的编号分开
|
||||||
sub: `题目里的例子 ${(failedOnSample.value.index ?? 0) + 1} 就错了`,
|
sub: `题目里的例子 ${(failedOnSample.value.index ?? 0) + 1} 就错了`,
|
||||||
progress: null,
|
progress: null,
|
||||||
|
cases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const cases = partialCases.value
|
const partial = partialCases.value
|
||||||
return {
|
return {
|
||||||
kind: status.type,
|
kind: status.type,
|
||||||
title: status.title,
|
title: status.title,
|
||||||
sub: cases ? `通过 ${cases.passed}/${cases.total} 个测试点` : "",
|
sub: partial ? `通过 ${partial.passed}/${partial.total} 个测试点` : "",
|
||||||
progress: cases,
|
progress: partial,
|
||||||
|
cases,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方块一格一格亮的时候,下面的提示、按钮、「下一题」先不出来,亮完一起出来(设计稿「测试点亮起来」)。
|
||||||
|
* 判完那一刻只有 WebSocket 推来的结果,详情(带每组对错)还在路上 —— 这段也先等着,
|
||||||
|
* 不然下面的东西会先闪出来、方块开始亮时又缩回去
|
||||||
|
*/
|
||||||
|
const revealedId = ref<string | null>(null)
|
||||||
|
const waitingReveal = computed(() => {
|
||||||
|
const submission = props.submission
|
||||||
|
if (!submission || props.peer || !header.value || header.value.kind === "pending") return false
|
||||||
|
if (!submissionStore.shouldRevealCases(submission.id)) return false
|
||||||
|
if (submission.code === undefined) return true
|
||||||
|
return !!header.value.cases?.length && revealedId.value !== submission.id
|
||||||
|
})
|
||||||
|
const animateCases = computed(
|
||||||
|
() => !props.peer && submissionStore.shouldRevealCases(props.submission?.id),
|
||||||
|
)
|
||||||
|
function onCasesRevealed() {
|
||||||
|
const id = props.submission?.id
|
||||||
|
if (!id) return
|
||||||
|
revealedId.value = id
|
||||||
|
submissionStore.markCasesRevealed(id)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 例子都对了、错在隐藏测试点:「去『自己输入』拿 90、80 这些数试试」。数取题目描述里出现的、
|
* 例子都对了、错在隐藏测试点:「去『自己输入』拿 90、80 这些数试试」。数取题目描述里出现的、
|
||||||
* 例子里没有的头两个 —— 分段、判断一类题的边界多半就写在描述里(设计稿「错在隐藏测试点」)。
|
* 例子里没有的头两个 —— 分段、判断一类题的边界多半就写在描述里(设计稿「错在隐藏测试点」)。
|
||||||
@@ -352,152 +380,160 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<ResultHeader
|
<ResultHeader
|
||||||
v-if="header"
|
v-if="header"
|
||||||
|
:key="submission.id"
|
||||||
:kind="header.kind"
|
:kind="header.kind"
|
||||||
:title="header.title"
|
:title="header.title"
|
||||||
:sub="header.sub"
|
:sub="header.sub"
|
||||||
:progress="header.progress"
|
:progress="header.progress"
|
||||||
|
:cases="header.cases"
|
||||||
|
:animate="animateCases"
|
||||||
|
@revealed="onCasesRevealed"
|
||||||
/>
|
/>
|
||||||
<LessonNext
|
<div v-show="!waitingReveal">
|
||||||
v-if="showLessonNext && problemStore.problem"
|
<LessonNext
|
||||||
:problem-display-id="problemStore.problem._id"
|
v-if="showLessonNext && problemStore.problem"
|
||||||
:language="submission.language"
|
:problem-display-id="problemStore.problem._id"
|
||||||
:refresh-key="submission.id"
|
:language="submission.language"
|
||||||
:challenges="ctx.challenges"
|
:refresh-key="submission.id"
|
||||||
/>
|
:challenges="ctx.challenges"
|
||||||
<!-- 不在这节课里(题库入口自己做的):做对之后先给这道题的挑战,再给相似题 -->
|
/>
|
||||||
<ChallengeList
|
<!-- 不在这节课里(题库入口自己做的):做对之后先给这道题的挑战,再给相似题 -->
|
||||||
v-if="showSimilar && ctx.challenges && problemStore.problem"
|
<ChallengeList
|
||||||
:problem-ids="[problemStore.problem._id]"
|
v-if="showSimilar && ctx.challenges && problemStore.problem"
|
||||||
:language="submission.language"
|
:problem-ids="[problemStore.problem._id]"
|
||||||
:refresh-key="submission.id"
|
:language="submission.language"
|
||||||
/>
|
:refresh-key="submission.id"
|
||||||
<SimilarProblems v-if="showSimilar" title="再练几道相似的" />
|
/>
|
||||||
|
<SimilarProblems v-if="showSimilar" title="再练几道相似的" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<n-flex
|
<div v-show="!waitingReveal">
|
||||||
vertical
|
<n-flex
|
||||||
v-if="
|
vertical
|
||||||
compileError ||
|
v-if="
|
||||||
runtimeError ||
|
compileError ||
|
||||||
sampleCheck ||
|
runtimeError ||
|
||||||
msg ||
|
sampleCheck ||
|
||||||
infoTable.length ||
|
msg ||
|
||||||
submission.statisticInfo?.ast_results?.length
|
infoTable.length ||
|
||||||
"
|
submission.statisticInfo?.ast_results?.length
|
||||||
>
|
"
|
||||||
<CompileErrorExplain
|
>
|
||||||
v-if="compileError"
|
<CompileErrorExplain
|
||||||
:err-info="compileError"
|
v-if="compileError"
|
||||||
:language="submission.language"
|
:err-info="compileError"
|
||||||
/>
|
:language="submission.language"
|
||||||
<RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" />
|
/>
|
||||||
<WrongAnswerExplain
|
<RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" />
|
||||||
v-if="sampleCheck"
|
<WrongAnswerExplain
|
||||||
full
|
v-if="sampleCheck"
|
||||||
:check="sampleCheck"
|
full
|
||||||
:code="submission.code"
|
:check="sampleCheck"
|
||||||
:language="submission.language"
|
:code="submission.code"
|
||||||
/>
|
:language="submission.language"
|
||||||
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
|
/>
|
||||||
<n-flex vertical :size="8">
|
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
|
||||||
<n-flex
|
<n-flex vertical :size="8">
|
||||||
v-for="(rule, i) in submission.statisticInfo.ast_results"
|
<n-flex
|
||||||
:key="i"
|
v-for="(rule, i) in submission.statisticInfo.ast_results"
|
||||||
align="center"
|
:key="i"
|
||||||
:size="6"
|
align="center"
|
||||||
>
|
:size="6"
|
||||||
<n-icon :color="rule.passed ? theme.successColor : theme.errorColor">
|
|
||||||
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" />
|
|
||||||
</n-icon>
|
|
||||||
<span>{{ rule.description }}</span>
|
|
||||||
<!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 -->
|
|
||||||
<span
|
|
||||||
v-if="!rule.passed && rule.actual !== undefined"
|
|
||||||
:style="{ color: theme.errorColor }"
|
|
||||||
>
|
>
|
||||||
当前 {{ rule.actual }} 次
|
<n-icon :color="rule.passed ? theme.successColor : theme.errorColor">
|
||||||
</span>
|
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" />
|
||||||
|
</n-icon>
|
||||||
|
<span>{{ rule.description }}</span>
|
||||||
|
<!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 -->
|
||||||
|
<span
|
||||||
|
v-if="!rule.passed && rule.actual !== undefined"
|
||||||
|
:style="{ color: theme.errorColor }"
|
||||||
|
>
|
||||||
|
当前 {{ rule.actual }} 次
|
||||||
|
</span>
|
||||||
|
</n-flex>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-flex>
|
</n-card>
|
||||||
</n-card>
|
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
|
||||||
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
|
<n-data-table v-if="infoTable.length" striped :data="infoTable" :columns="columns" />
|
||||||
<n-data-table v-if="infoTable.length" striped :data="infoTable" :columns="columns" />
|
</n-flex>
|
||||||
</n-flex>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
设计稿:还没问过 AI 时只是一个描边按钮;错在隐藏测试点时旁边给「去自己输入试试」。
|
设计稿:还没问过 AI 时只是一个描边按钮;错在隐藏测试点时旁边给「去自己输入试试」。
|
||||||
问过之后,AI 的回答才放进卡片
|
问过之后,AI 的回答才放进卡片
|
||||||
-->
|
-->
|
||||||
<div v-if="(showAIHint && !hintTarget) || customRunHint" class="actions">
|
<div v-if="(showAIHint && !hintTarget) || customRunHint" class="actions">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="showAIHint && !hintTarget"
|
v-if="showAIHint && !hintTarget"
|
||||||
type="primary"
|
type="primary"
|
||||||
ghost
|
ghost
|
||||||
:loading="hintLoading"
|
:loading="hintLoading"
|
||||||
@click="fetchHint(submission.id)"
|
@click="fetchHint(submission.id)"
|
||||||
>
|
>
|
||||||
让 AI 分析我的代码
|
让 AI 分析我的代码
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="customRunHint"
|
v-if="customRunHint"
|
||||||
text
|
text
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="submissionStore.revealResult('custom')"
|
@click="submissionStore.revealResult('custom')"
|
||||||
>
|
>
|
||||||
{{ customRunHint }}
|
{{ customRunHint }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<n-alert v-if="showAIHint && hintError" type="error" :title="hintError" class="hint-error" />
|
<n-alert v-if="showAIHint && hintError" type="error" :title="hintError" class="hint-error" />
|
||||||
<template v-if="showAIHint && hintTarget">
|
<template v-if="showAIHint && hintTarget">
|
||||||
<n-card size="small" class="hint-card">
|
<n-card size="small" class="hint-card">
|
||||||
<n-spin v-if="hintLoading && !hintContent" size="small" />
|
<n-spin v-if="hintLoading && !hintContent" size="small" />
|
||||||
<Markdown v-if="hintContent" :content="hintContent" />
|
<Markdown v-if="hintContent" :content="hintContent" />
|
||||||
<!-- 等级和「再多一点提示」。按钮出不出由后端 done 里的 canEscalate 定,
|
<!-- 等级和「再多一点提示」。按钮出不出由后端 done 里的 canEscalate 定,
|
||||||
前端不自己推阶梯(要再交一次才升得动,规则在 services/hint-level.ts) -->
|
前端不自己推阶梯(要再交一次才升得动,规则在 services/hint-level.ts) -->
|
||||||
<n-flex
|
<n-flex
|
||||||
v-if="hintLevel !== null && !hintLoading && !hintTyping"
|
v-if="hintLevel !== null && !hintLoading && !hintTyping"
|
||||||
align="center"
|
align="center"
|
||||||
size="small"
|
size="small"
|
||||||
style="margin-top: 8px"
|
style="margin-top: 8px"
|
||||||
>
|
|
||||||
<n-tag size="small" :bordered="false">
|
|
||||||
{{ hintLevelLabel(hintLevel) }}
|
|
||||||
</n-tag>
|
|
||||||
<n-button
|
|
||||||
v-if="hintCanEscalate"
|
|
||||||
size="tiny"
|
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
@click="fetchHint(submission.id, true)"
|
|
||||||
>
|
>
|
||||||
再多一点提示
|
<n-tag size="small" :bordered="false">
|
||||||
</n-button>
|
{{ hintLevelLabel(hintLevel) }}
|
||||||
</n-flex>
|
</n-tag>
|
||||||
<n-flex
|
<n-button
|
||||||
v-if="hintId !== null && !hintLoading && !hintTyping"
|
v-if="hintCanEscalate"
|
||||||
align="center"
|
size="tiny"
|
||||||
size="small"
|
type="primary"
|
||||||
style="margin-top: 8px"
|
ghost
|
||||||
>
|
@click="fetchHint(submission.id, true)"
|
||||||
<n-text depth="3">这条提示对你有帮助吗?</n-text>
|
>
|
||||||
<n-button
|
再多一点提示
|
||||||
size="tiny"
|
</n-button>
|
||||||
:type="hintHelpful === true ? 'primary' : 'default'"
|
</n-flex>
|
||||||
:disabled="hintFeedbackSending"
|
<n-flex
|
||||||
@click="sendHintFeedback(true)"
|
v-if="hintId !== null && !hintLoading && !hintTyping"
|
||||||
|
align="center"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 8px"
|
||||||
>
|
>
|
||||||
有帮助
|
<n-text depth="3">这条提示对你有帮助吗?</n-text>
|
||||||
</n-button>
|
<n-button
|
||||||
<n-button
|
size="tiny"
|
||||||
size="tiny"
|
:type="hintHelpful === true ? 'primary' : 'default'"
|
||||||
:type="hintHelpful === false ? 'warning' : 'default'"
|
:disabled="hintFeedbackSending"
|
||||||
:disabled="hintFeedbackSending"
|
@click="sendHintFeedback(true)"
|
||||||
@click="sendHintFeedback(false)"
|
>
|
||||||
>
|
有帮助
|
||||||
没帮助
|
</n-button>
|
||||||
</n-button>
|
<n-button
|
||||||
</n-flex>
|
size="tiny"
|
||||||
</n-card>
|
:type="hintHelpful === false ? 'warning' : 'default'"
|
||||||
</template>
|
:disabled="hintFeedbackSending"
|
||||||
|
@click="sendHintFeedback(false)"
|
||||||
|
>
|
||||||
|
没帮助
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 测试点一格一格亮(设计稿「测试点亮起来」,借小丑牌结算时一张张牌亮起来)的节奏。
|
||||||
|
* 不管几组,全部亮完不超过 0.9 秒:组少的一格 120 毫秒,组多的平分 900 毫秒。
|
||||||
|
* 结果页签里的方块(CaseBlocks)和等方块亮完再放的烟花(SubmissionEffects)用同一份。
|
||||||
|
*/
|
||||||
|
const TOTAL_MS = 900
|
||||||
|
const STEP_MAX_MS = 120
|
||||||
|
|
||||||
|
export function caseRevealStep(count: number) {
|
||||||
|
return Math.min(STEP_MAX_MS, Math.floor(TOTAL_MS / Math.max(count, 1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function caseRevealDuration(count: number) {
|
||||||
|
return caseRevealStep(count) * count
|
||||||
|
}
|
||||||
@@ -65,6 +65,25 @@ export const useSubmissionStore = defineStore("submission", () => {
|
|||||||
revealResult("custom")
|
revealResult("custom")
|
||||||
trial.runCustom({ ...codeStore.code }, { problemId: problem.id, contestId: problem.contestId })
|
trial.runCustom({ ...codeStore.code }, { problemId: problem.id, contestId: problem.contestId })
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 测试点一格一格亮(结果页签里的 CaseBlocks)只播一次,而且只播这一页上刚交、刚判完的那条:
|
||||||
|
* 刷新、切页签回来、看以前的提交都直接全亮。系统设了「减少动画」的不播。
|
||||||
|
* 烟花(SubmissionEffects)按同一个判断决定要不要等方块亮完
|
||||||
|
*/
|
||||||
|
const reducedMotion = usePreferredReducedMotion()
|
||||||
|
const revealedCases = new Set<string>()
|
||||||
|
function shouldRevealCases(id: string | undefined) {
|
||||||
|
return (
|
||||||
|
!!id &&
|
||||||
|
reducedMotion.value !== "reduce" &&
|
||||||
|
id === monitor.submissionId.value &&
|
||||||
|
!revealedCases.has(id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function markCasesRevealed(id: string) {
|
||||||
|
revealedCases.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
const isFormatting = ref(false)
|
const isFormatting = ref(false)
|
||||||
const isSubmittingRequest = ref(false)
|
const isSubmittingRequest = ref(false)
|
||||||
|
|
||||||
@@ -206,6 +225,8 @@ export const useSubmissionStore = defineStore("submission", () => {
|
|||||||
resultSeq,
|
resultSeq,
|
||||||
resultSegment,
|
resultSegment,
|
||||||
revealResult,
|
revealResult,
|
||||||
|
shouldRevealCases,
|
||||||
|
markCasesRevealed,
|
||||||
runSamples,
|
runSamples,
|
||||||
runCustom,
|
runCustom,
|
||||||
isFormatting,
|
isFormatting,
|
||||||
|
|||||||
@@ -218,6 +218,11 @@ export const caseSummarySchema = z
|
|||||||
.object({
|
.object({
|
||||||
passed: z.number().int(),
|
passed: z.number().int(),
|
||||||
total: z.number().int(),
|
total: z.number().int(),
|
||||||
|
/**
|
||||||
|
* 每组对没对,按组号排。只有详情给(结果页签里方块一格一格亮,错的写组号),列表只给
|
||||||
|
* 两个数。只有对错、不带数据和输出,所以学生也给;为 null 的情形同上(比赛照旧不给)
|
||||||
|
*/
|
||||||
|
results: z.array(z.boolean()).optional(),
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user