style(题目): 题目页照设计稿「机房(方案 B)」重排——顶满的两栏、带底色的上下文条、下划线页签、新的结果和题面样式
Deploy / deploy (push) Canceled after 0s

原来实施时只照了设计文档,没对照设计稿,一路套 Naive 现成的分段页签、提示框、单选组,
功能对了、样子差很多。这次逐块对着设计稿的画板改:

- 两栏顶满内容区,中间一条 1px 分隔线;上下文条、页签行、工具栏下面各一道分隔线
- 上下文条:课堂浅绿、题单浅蓝、比赛浅黄三种底色;‹ › 是不带边框的箭头;比赛的题号是方角、
  做错的是红色 ✕;手机上不摆一排题号,写「这节课 做完 2/5 · 这是第 3 道」配 44px 的箭头
- 「题目 / 结果」是下划线页签,右边「统计 / 点评 / 我的提交」是胶囊按钮
- 抽屉盖住整个左栏(连上下文条、页签行),顶上有自己的「我的提交 / 统计 / 点评」页签,Esc 能关
- 结果页签:三个分段是胶囊,选中的深色实心、带时间(「提交结果 · 10:42」);标题是圆形图标 +
  大字 + 灰字(「答案错误 在例子 1 上就错了」),不再套整块彩色提示框;答错的对比三栏并排、
  只把不一样的那几个字标红;「题目要求的输出」是一行小字;「让 AI 分析我的代码」是描边按钮;
  答对后的「下一题」是浅绿卡片里 44px 的大按钮
- 工具栏:语言、运行例子(描边)、提交(实心,就叫「提交」)靠左,求助、⋯ 推到最右;
  交对之后「提交」变成绿色描边;协作条换成设计稿的深蓝
- 题面:标题下「简单 · 还没交过」,流程图过关的加「流程图 A 级」;代码要求压成一行;
  小标题不带图标;输入、输出左右两栏;参考流程图是一行可以点开的条;例子表带「例子 1」和「复制」
- 去掉「限时 1 秒」:执行时间、内存对中职学生只会添迷惑(和设计稿不同,用户定的)
- 我的提交:一整块带框的列表,结果用彩色文字;展开的代码先露 5 行;题单锁住的几次合成一行
- 错在隐藏测试点时「去自己输入」给的数跳过例子里出现过的

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-28 20:04:20 +08:00
1 parent 83b6b7a81a
commit 1fd9bf67c2
17 files changed
+1800 -706

No files matched your search

@@ -8,7 +8,6 @@ import {
submissionCaseResults,
submissionMemoryFormat,
submissionPartialCases,
submissionResultTitle,
submissionTimeFormat,
} from "utils/functions"
import type { Submission } from "utils/types"
@@ -19,6 +18,7 @@ import PythonErrorExplain from "./PythonErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
import LessonNext from "./LessonNext.vue"
import ResultHeader from "./ResultHeader.vue"
import SimilarProblems from "./SimilarProblems.vue"
import { useLessonStore } from "oj/store/lesson"
import { useProblemPageContext } from "../composables/problemPageContext"
@@ -151,34 +151,70 @@ const failedOnSample = computed(() =>
)
/**
* 结果标题(设计文档第 6 节)。比「答案错误 · 通过 1/3 个测试点」多走一步:
* 例子上就错了的说「在例子 N 上就错了」,例子都对的才报测试点;做对了说全对了。
* 结果标题(设计文档第 6 节、设计稿「答案错误 / 答案正确 / 错在隐藏测试点」):
* 大字是判题结果,灰色小字说清楚错在哪 —— 例子上就错了的说「在例子 N 上就错了」、不挂进度;
* 其余照旧报「通过 x/y 个测试点」(一个都没过就不报);做对了说全对了。
* 提交详情页还用通用的 submissionResultTitle
*/
const title = computed(() => {
const header = computed(() => {
const submission = props.submission
if (!submission) return ""
if (submission.result === SubmissionStatus.accepted) return "答案正确 · 所有测试点都对了"
if (!submission) return null
const status = JUDGE_STATUS[submission.result]
if (
submission.result === SubmissionStatus.pending ||
submission.result === SubmissionStatus.judging ||
submission.result === SubmissionStatus.submitting
) {
return { kind: "pending" as const, title: status.title, sub: "", progress: null }
}
if (submission.result === SubmissionStatus.accepted) {
return {
kind: "success" as const,
title: status.title,
sub: "所有测试点都对了",
progress: null,
}
}
if (failedOnSample.value) {
return `答案错误 · 在例子 ${(failedOnSample.value.index ?? 0) + 1} 上就错了`
return {
kind: status.type,
title: status.title,
sub: `在例子 ${(failedOnSample.value.index ?? 0) + 1} 上就错了`,
progress: null,
}
}
const cases = partialCases.value
return {
kind: status.type,
title: status.title,
sub: cases ? `通过 ${cases.passed}/${cases.total} 个测试点` : "",
progress: cases,
}
const cases = hiddenCases.value
if (cases) return `答案错误 · 通过 ${cases.passed}/${cases.total} 个测试点`
return submissionResultTitle(submission)
})
/**
* 例子都对了、错在隐藏的测试点上:一个都没过也要报「通过 0/5」—— 那正说明例子之外的全错了。
* 其余情况沿用 submissionPartialCases(一个都没过就不报)
* 例子都对了、错在隐藏测试点:「去『自己输入』拿 90、80 这些数试试」。数取题目描述里出现的、
* 例子里没有的头两个 —— 分段、判断一类题的边界多半就写在描述里(设计稿「错在隐藏测试点」)。
* SQL 题、比赛里没有「自己输入」
*/
const hiddenCases = computed(() => {
const summary = props.submission?.caseSummary
if (!sampleCheck.value?.passed || !summary || summary.passed >= summary.total) return null
return summary
const submissionStore = useSubmissionStore()
const customRunHint = computed(() => {
if (!sampleCheck.value?.passed || props.peer) return ""
const problem = problemStore.problem
if (!problem || problem.sqlConfig) return ""
// 例子里出现过的数不算:例子已经对了,拿它们再试一遍没用
const inSamples = new Set(
problem.samples.flatMap(
(sample) => `${sample.input} ${sample.output}`.match(/-?\d+(?:\.\d+)?/g) ?? [],
),
)
const numbers = [...new Set(problem.description.match(/-?\d+(?:\.\d+)?/g) ?? [])]
.filter((value) => !inSamples.has(value))
.slice(0, 2)
return numbers.length
? `去「自己输入」拿 ${numbers.join("、")} 这些数试试 ›`
: "去「自己输入」拿特殊的数试试 ›"
})
const progressCases = computed(
() => hiddenCases.value ?? (failedOnSample.value ? null : partialCases.value),
)
// 是否显示AI提示区域。
// 阈值和后端 POST /ai/hint 共用契约里的 HINT_MIN_FAILURES,别在这里写死数字;
@@ -249,16 +285,13 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
<template>
<div v-if="submission">
<n-alert :type="JUDGE_STATUS[submission.result]['type']" :title="title" class="mb-3">
<template v-if="progressCases" #default>
<n-progress
type="line"
status="success"
:percentage="(progressCases.passed / progressCases.total) * 100"
:show-indicator="false"
/>
</template>
</n-alert>
<ResultHeader
v-if="header"
:kind="header.kind"
:title="header.title"
:sub="header.sub"
:progress="header.progress"
/>
<LessonNext
v-if="showLessonNext && problemStore.problem"
:problem-display-id="problemStore.problem._id"
@@ -310,18 +343,33 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
<n-data-table v-if="infoTable.length" striped :data="infoTable" :columns="columns" />
</n-flex>
<!-- AI 提示区域 -->
<template v-if="showAIHint">
<n-card size="small" style="margin-top: 12px; max-width: 480px">
<n-alert v-if="hintError" type="error" :title="hintError" class="mb-3" />
<n-button
v-if="!hintTarget && !hintLoading"
type="primary"
@click="fetchHint(submission.id)"
>
让 AI 分析我的代码
</n-button>
<n-spin v-else-if="hintLoading && !hintTarget" size="small" />
<!--
设计稿:还没问过 AI 时只是一个描边按钮;错在隐藏测试点时旁边给「去自己输入试试」。
问过之后,AI 的回答才放进卡片
-->
<div v-if="(showAIHint && !hintTarget) || customRunHint" class="actions">
<n-button
v-if="showAIHint && !hintTarget"
type="primary"
ghost
:loading="hintLoading"
@click="fetchHint(submission.id)"
>
让 AI 分析我的代码
</n-button>
<n-button
v-if="customRunHint"
text
type="primary"
@click="submissionStore.revealResult('custom')"
>
{{ customRunHint }}
</n-button>
</div>
<n-alert v-if="showAIHint && hintError" type="error" :title="hintError" class="hint-error" />
<template v-if="showAIHint && hintTarget">
<n-card size="small" class="hint-card">
<n-spin v-if="hintLoading && !hintContent" size="small" />
<MdPreview
v-if="hintContent"
:model-value="hintContent"
@@ -379,6 +427,23 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
</template>
<style scoped>
.actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 12px;
}
.hint-error {
margin-top: 12px;
}
.hint-card {
margin-top: 12px;
max-width: 560px;
}
.msg {
white-space: pre;
word-break: break-all;