style(结果页签): 分段、说明、对比改成和题面同一套样子;做对后「接着做」直接给下一题
Deploy / deploy (push) Successful in 1m0s

- 提交结果 / 运行例子 / 自己输入改成浅灰底的分段按钮;状态大字和测试点方块放大一档
- 哪里不对一律是黄色小节标题(例子 1 没对上、错在没给你看的测试点、第 2 行出错),说明用正文大小
- 答错的对比竖着排成灰 / 绿 / 红三格,和题面的例子一个样子;运行时错误把出错那一行放进红格
- 做对了去掉浅绿卡片,改成「接着做」一句话加写着下一题题号题名的大按钮
- 提交详情页的答错对比不变

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-04 22:09:31 +08:00
1 parent 472ec69328
commit 566599614a
12 files changed
+356 -181

No files matched your search

@@ -71,8 +71,8 @@ const label = computed(() => {
} }
.block { .block {
width: 20px; width: 22px;
height: 20px; height: 22px;
border-radius: 4px; border-radius: 4px;
box-sizing: border-box; box-sizing: border-box;
display: inline-flex; display: inline-flex;
@@ -6,6 +6,7 @@ import { applyFixes, clearErrorMark, currentCode, showErrorMark, showPunctuation
import { explainCCompileError } from 'oj/problem/utils/cError' import { explainCCompileError } from 'oj/problem/utils/cError'
import { explainPythonCompileError } from 'utils/pythonError' import { explainPythonCompileError } from 'utils/pythonError'
import { stripJudgePath } from 'utils/judge' import { stripJudgePath } from 'utils/judge'
import SectionHead from './SectionHead.vue'
/** /**
* 编译错误的中文说明卡片:Python 的语法错误走 pythonError.ts,C / C++ 的 gcc 报错走 * 编译错误的中文说明卡片:Python 的语法错误走 pythonError.ts,C / C++ 的 gcc 报错走
@@ -80,17 +81,17 @@ onUnmounted(clearErrorMark)
</script> </script>
<template> <template>
<div class="explain-card"> <!-- 设计稿「题目页观感」结果 X:黄色小节标题说第几行,正文大小的中文说明;那一行编辑器里已经标红了 -->
<div class="explain-section">
<template v-if="explanation"> <template v-if="explanation">
<p class="explain"> <SectionHead icon="problem" tone="amber">{{ explanation.line ? `第 ${explanation.line} 行` : '代码的格式写错了' }}</SectionHead>
<b v-if="explanation.line">第 {{ explanation.line }} 行:</b>{{ explanation.message }} <p class="explain">{{ explanation.message }}</p>
</p>
<p v-if="otherPunctuationLines.length" class="explain other">第 {{ otherPunctuationLines.join('、') }} 行也有中文标点,编辑器里都标出来了。</p> <p v-if="otherPunctuationLines.length" class="explain other">第 {{ otherPunctuationLines.join('、') }} 行也有中文标点,编辑器里都标出来了。</p>
<div v-if="explanation.punctuation"> <div v-if="explanation.punctuation" class="fix">
<n-button v-if="punctuationFixCount" type="primary" @click="fixPunctuation"> 把中文标点都换成英文({{ punctuationFixCount }} 处) </n-button> <n-button v-if="punctuationFixCount" type="primary" @click="fixPunctuation"> 把中文标点都换成英文({{ punctuationFixCount }} 处) </n-button>
<n-text v-else-if="fixedCount" type="success"> 换好了 {{ fixedCount }} 处,再提交一次试试。 </n-text> <n-text v-else-if="fixedCount" type="success"> 换好了 {{ fixedCount }} 处,再提交一次试试。 </n-text>
</div> </div>
<n-collapse> <n-collapse class="raw-collapse">
<n-collapse-item title="原始报错(英文)" name="raw"> <n-collapse-item title="原始报错(英文)" name="raw">
<div class="raw">{{ rawError }}</div> <div class="raw">{{ rawError }}</div>
</n-collapse-item> </n-collapse-item>
@@ -98,6 +99,7 @@ onUnmounted(clearErrorMark)
</template> </template>
<!-- 翻译表没覆盖到的句式(生产数据里约 0.3%):照旧给原文 --> <!-- 翻译表没覆盖到的句式(生产数据里约 0.3%):照旧给原文 -->
<template v-else> <template v-else>
<SectionHead icon="problem" tone="amber">代码的格式写错了</SectionHead>
<p class="explain">请仔细检查,看看代码的格式是不是写错了!</p> <p class="explain">请仔细检查,看看代码的格式是不是写错了!</p>
<div class="raw">{{ rawError }}</div> <div class="raw">{{ rawError }}</div>
</template> </template>
@@ -105,20 +107,15 @@ onUnmounted(clearErrorMark)
</template> </template>
<style scoped> <style scoped>
/* 和答错的说明同一种浅灰卡片(设计稿「语法没过」) */ .explain-section {
.explain-card {
padding: 12px 14px;
border-radius: 6px;
background-color: rgba(128, 128, 128, 0.06);
border: 1px solid v-bind('theme.dividerColor');
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px;
} }
.explain { .explain {
margin: 0; margin: 0 0 10px;
line-height: 1.7; font-size: 16px;
line-height: 1.75;
} }
.explain.other { .explain.other {
@@ -126,6 +123,10 @@ onUnmounted(clearErrorMark)
color: v-bind('theme.textColor2'); color: v-bind('theme.textColor2');
} }
.fix {
margin-bottom: 10px;
}
.raw { .raw {
white-space: pre; white-space: pre;
overflow-x: auto; overflow-x: auto;
@@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue'
import SectionHead from './SectionHead.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCodeStore } from 'oj/store/code' import { useCodeStore } from 'oj/store/code'
@@ -29,7 +31,7 @@ function fillSample(input: string) {
<template> <template>
<!-- 设计稿「自己输入:顶替原来的『自测』模式」;「这里不判对错」在分段那一行右边(ResultPane) --> <!-- 设计稿「自己输入:顶替原来的『自测』模式」;「这里不判对错」在分段那一行右边(ResultPane) -->
<div class="custom"> <div class="custom">
<span class="label">输入</span> <SectionHead icon="input">输入</SectionHead>
<n-input <n-input
v-model:value="customInput" v-model:value="customInput"
type="textarea" type="textarea"
@@ -38,15 +40,16 @@ function fillSample(input: string) {
class="mono" class="mono"
/> />
<div class="actions"> <div class="actions">
<n-button type="primary" ghost :loading="customRunning" :disabled="!codeStore.code.value.trim()" @click="submissionStore.runCustom()"> <n-button type="primary" secondary :loading="customRunning" :disabled="!codeStore.code.value.trim()" @click="submissionStore.runCustom()">
<template #icon><Icon icon="ph:play-fill" /></template>
用这组数据运行 用这组数据运行
</n-button> </n-button>
<n-button v-for="(sample, index) in problem?.samples ?? []" :key="index" text class="fill" @click="fillSample(sample.input)"> <n-button v-for="(sample, index) in problem?.samples ?? []" :key="index" text type="primary" class="fill" @click="fillSample(sample.input)">
填入例子 {{ index + 1 }} 填入例子 {{ index + 1 }}
</n-button> </n-button>
</div> </div>
<span class="label">输出</span> <SectionHead icon="output" class="output-head">输出</SectionHead>
<TrialErrorNote <TrialErrorNote
v-if="customResult !== null && customResult !== SubmissionStatus.accepted" v-if="customResult !== null && customResult !== SubmissionStatus.accepted"
:result="customResult" :result="customResult"
@@ -68,13 +71,14 @@ function fillSample(input: string) {
gap: 8px; gap: 8px;
} }
.label { .output-head {
font-size: 13px; margin-top: 14px;
font-weight: 600;
} }
.mono :deep(textarea) { .mono :deep(textarea) {
font-family: Consolas, Monaco, monospace; font-family: Consolas, Monaco, monospace;
font-size: 16px;
line-height: 24px;
} }
.actions { .actions {
@@ -85,13 +89,12 @@ function fillSample(input: string) {
} }
.fill { .fill {
font-size: 13px; font-size: 14px;
color: v-bind('theme.textColor2');
} }
.output { .output {
margin: 0; margin: 0;
min-height: 88px; min-height: 60px;
box-sizing: border-box; box-sizing: border-box;
padding: 10px 10px; padding: 10px 10px;
border-radius: 6px; border-radius: 6px;
@@ -99,7 +102,9 @@ function fillSample(input: string) {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
font-family: Consolas, Monaco, monospace; font-family: Consolas, Monaco, monospace;
font-size: 14px; font-size: 16px;
line-height: 24px;
border: 1px solid rgba(128, 128, 128, 0.14);
} }
.output.empty { .output.empty {
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import SectionHead from './SectionHead.vue'
import { useThemeVars } from 'naive-ui' import { useThemeVars } from 'naive-ui'
import { useLessonStore } from 'shared/store/lesson' import { useLessonStore } from 'shared/store/lesson'
import ChallengeList from './ChallengeList.vue' import ChallengeList from './ChallengeList.vue'
@@ -44,16 +45,17 @@ const lessonProblemIds = computed(() => lessonStore.activity?.problems.map((item
</script> </script>
<template> <template>
<!-- 设计稿「答案正确:下一题在结果页签里」:浅绿卡片,一个 44px 的大按钮 --> <!--
<div v-if="lesson" class="lesson-next"> 设计稿「题目页观感」结果 X:「接着做」一节,一句话说还剩几道,一个 44px 的大按钮写下一题。
原来是一张浅绿卡片:左边竖栏已经有这节课的进度,不用再框一块
-->
<section v-if="lesson" class="lesson-next">
<SectionHead icon="next">接着做</SectionHead>
<template v-if="lesson.next"> <template v-if="lesson.next">
<div class="text"> <p class="text">
<span class="headline">{{ label }}还剩 {{ lesson.remaining }} 道</span> {{ label }}还剩 {{ lesson.remaining }} 道,下一道 {{ lesson.next.problemDisplayId }}
<span class="detail"> {{ lesson.next.myStatus === 'tried' ? '你做过,还没对' : '你还没做过' }}。
{{ lesson.next.problemDisplayId }} </p>
{{ lesson.next.myStatus === 'tried' ? '你做过,还没对' : '你还没做过' }}
</span>
</div>
<router-link :to="`/problem/${lesson.next.problemDisplayId}`" class="next"> <router-link :to="`/problem/${lesson.next.problemDisplayId}`" class="next">
<span class="next-label">下一题:{{ lesson.next.problemDisplayId }} {{ lesson.next.title }}</span> <span class="next-label">下一题:{{ lesson.next.problemDisplayId }} {{ lesson.next.title }}</span>
<svg <svg
@@ -72,15 +74,12 @@ const lessonProblemIds = computed(() => lessonStore.activity?.problems.map((item
</router-link> </router-link>
</template> </template>
<template v-else> <template v-else>
<div class="text"> <p class="text">{{ label }}都做完了,一共 {{ lesson.total }} 道。<template v-if="!challenges">想接着练,可以去专题里挑一个。</template></p>
<span class="headline">{{ label }}都做完了,一共 {{ lesson.total }} 道</span>
<span v-if="!challenges" class="detail">想接着练,可以去专题里挑一个</span>
</div>
<router-link v-if="!challenges" to="/problemset" class="next secondary"> <router-link v-if="!challenges" to="/problemset" class="next secondary">
<span class="next-label">去专题继续练</span> <span class="next-label">去专题继续练</span>
</router-link> </router-link>
</template> </template>
</div> </section>
<!-- 做完了:先给这节课里还能挑战的,再给专题(设计稿 StkDone)。挑战一条都没有时只剩专题那句 --> <!-- 做完了:先给这节课里还能挑战的,再给专题(设计稿 StkDone)。挑战一条都没有时只剩专题那句 -->
<template v-if="lesson && !lesson.next && challenges"> <template v-if="lesson && !lesson.next && challenges">
<ChallengeList :problem-ids="lessonProblemIds" :language="language" :refresh-key="refreshKey" /> <ChallengeList :problem-ids="lessonProblemIds" :language="language" :refresh-key="refreshKey" />
@@ -90,30 +89,15 @@ const lessonProblemIds = computed(() => lessonStore.activity?.problems.map((item
<style scoped> <style scoped>
.lesson-next { .lesson-next {
margin-top: 12px; margin-top: 4px;
padding: 16px 18px;
border-radius: 8px;
background-color: rgba(24, 160, 88, 0.07);
border: 1px solid rgba(24, 160, 88, 0.25);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
} }
.text { .text {
display: flex; margin: 0 0 12px;
flex-direction: column; font-size: 16px;
gap: 4px; line-height: 1.75;
}
.headline {
font-size: 15px;
font-weight: 600;
}
.detail {
font-size: 13px;
color: v-bind('theme.textColor2');
} }
.next { .next {
@@ -125,10 +109,10 @@ const lessonProblemIds = computed(() => lessonStore.activity?.problems.map((item
gap: 8px; gap: 8px;
height: 44px; height: 44px;
padding: 0 20px; padding: 0 20px;
border-radius: 6px; border-radius: 8px;
background-color: v-bind('theme.primaryColor'); background-color: v-bind('theme.primaryColor');
color: v-bind('buttonText'); color: v-bind('buttonText');
font-size: 15px; font-size: 16px;
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
} }
@@ -101,8 +101,8 @@ const color = computed(() => {
.result-header { .result-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 8px;
margin-bottom: 12px; margin-bottom: 18px;
} }
.line { .line {
@@ -114,8 +114,8 @@ const color = computed(() => {
.badge { .badge {
flex: none; flex: none;
width: 24px; width: 26px;
height: 24px; height: 26px;
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -125,12 +125,14 @@ const color = computed(() => {
} }
.title { .title {
font-size: 18px; font-size: 22px;
line-height: 1.3;
font-weight: 700; font-weight: 700;
color: v-bind('color.text'); color: v-bind('color.text');
} }
.sub { .sub {
font-size: 15px;
color: v-bind('theme.textColor3'); color: v-bind('theme.textColor3');
} }
@@ -74,20 +74,22 @@ function pick(segment: Segment) {
<template> <template>
<FlowchartResult v-if="drawing" /> <FlowchartResult v-if="drawing" />
<div v-show="!drawing" class="result-pane"> <div v-show="!drawing" class="result-pane">
<!-- 设计稿:三个胶囊,选中的深色实心,带上那次结果的时间 --> <!-- 三个分段,带上那次结果的时间 -->
<div v-if="showSegments" class="segments" role="tablist" aria-label="结果"> <div v-if="showSegments" class="segments">
<button <div class="segment-track" role="tablist" aria-label="结果">
v-for="segment in segments" <button
:key="segment.value" v-for="segment in segments"
type="button" :key="segment.value"
role="tab" type="button"
class="segment" role="tab"
:class="{ active: isActive(segment.value) }" class="segment"
:aria-selected="isActive(segment.value)" :class="{ active: isActive(segment.value) }"
@click="pick(segment.value)" :aria-selected="isActive(segment.value)"
> @click="pick(segment.value)"
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template> >
</button> {{ segment.label }}<span v-if="segment.time" class="segment-time">{{ segment.time }}</span>
</button>
</div>
<span v-if="resultSegment === 'custom'" class="segment-note">自己编数据试试,这里不判对错</span> <span v-if="resultSegment === 'custom'" class="segment-note">自己编数据试试,这里不判对错</span>
</div> </div>
@@ -116,35 +118,53 @@ function pick(segment: Segment) {
padding-bottom: 16px; padding-bottom: 16px;
} }
/* 设计稿「题目页观感」结果 X:浅灰底的分段按钮,选中的白底加粗(和工具栏「写代码 / 画流程图」一个样子) */
.segments { .segments {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; align-items: center;
margin-bottom: 14px; gap: 8px 12px;
margin-bottom: 16px;
}
.segment-track {
display: inline-flex;
gap: 2px;
padding: 3px;
border-radius: 8px;
background-color: rgba(128, 128, 128, 0.12);
} }
.segment { .segment {
height: 28px; height: 28px;
padding: 0 12px; padding: 0 12px;
border: 0; border: 0;
border-radius: 15px; border-radius: 6px;
background-color: rgba(128, 128, 128, 0.12); background: transparent;
color: v-bind('theme.textColor2'); color: v-bind('theme.textColor2');
font: inherit; font: inherit;
font-size: 13px; font-size: 14px;
white-space: nowrap;
cursor: pointer; cursor: pointer;
} }
.segment-time {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: v-bind('theme.textColor3');
}
.segment-note { .segment-note {
margin-left: auto;
align-self: center;
font-size: 13px; font-size: 13px;
color: v-bind('theme.textColor3'); color: v-bind('theme.textColor3');
} }
.segment.active { .segment.active {
background-color: v-bind('theme.textColor1'); background-color: v-bind('theme.cardColor');
color: v-bind('theme.cardColor'); color: v-bind('theme.textColor1');
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
} }
.formatted { .formatted {
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { clearErrorMark, showErrorMark } from 'oj/problem/utils/errorMark' import { clearErrorMark, showErrorMark } from 'oj/problem/utils/errorMark'
import { explainRuntimeError, type RuntimeErrorInfo } from 'oj/problem/utils/runtimeError' import { explainRuntimeError, type RuntimeErrorInfo } from 'oj/problem/utils/runtimeError'
import SectionHead from './SectionHead.vue'
/** /**
* 运行时错误的中文说明卡片(诊断由后端 judge/runtime-diagnosis.ts 给)。 * 运行时错误的中文说明(诊断由后端 judge/runtime-diagnosis.ts 给)。
* 知道是第几行的,就把那一行代码摆出来,并在编辑器里标红;卸掉时清掉标记。 * 知道是第几行的,就把那一行代码摆出来,并在编辑器里标红;卸掉时清掉标记。
*/ */
const props = defineProps<{ const props = defineProps<{
@@ -34,34 +35,50 @@ onUnmounted(clearErrorMark)
</script> </script>
<template> <template>
<n-card embedded class="explain-card"> <!-- 设计稿「题目页观感」结果 X:黄色小节标题说第几行,正文大小的中文说明,再把那一行代码摆出来 -->
<n-flex vertical :size="12"> <div class="explain-section">
<div class="explain"> <SectionHead icon="problem" tone="amber">{{ info.line ? `第 ${info.line} 行出错` : '程序跑到一半出错了' }}</SectionHead>
<b v-if="info.line">第 {{ info.line }} 行运行时出错:</b>{{ message }} <p class="explain">{{ message }}</p>
</div> <div v-if="sourceLine" class="explain-code">
<pre v-if="sourceLine" class="explain-code">{{ sourceLine }}</pre> <span class="line-no">{{ info.line }}</span>
</n-flex> <pre>{{ sourceLine }}</pre>
</n-card> </div>
</div>
</template> </template>
<style scoped> <style scoped>
/* 结果弹窗不限宽,长句子不折行会把弹窗撑出屏幕 */
.explain-card {
max-width: 560px;
}
.explain { .explain {
margin: 0 0 10px;
font-size: 16px; font-size: 16px;
line-height: 1.7; line-height: 1.75;
} }
/* 出错的那一行:红底,前面写行号,和编辑器里标红的那一行对得上 */
.explain-code { .explain-code {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 5px 10px;
border-radius: 6px;
background-color: rgba(208, 48, 80, 0.06);
border: 1px solid rgba(208, 48, 80, 0.24);
font-family: Consolas, Monaco, monospace;
font-size: 16px;
line-height: 24px;
}
.line-no {
flex: none;
font-weight: 700;
color: #d03050;
}
.explain-code pre {
flex: 1 1 auto;
min-width: 0;
margin: 0; margin: 0;
padding: 8px 12px; font: inherit;
border-radius: 4px;
font-size: 15px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
background-color: rgba(208, 48, 80, 0.12);
} }
</style> </style>
@@ -103,22 +103,14 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
</ResultHeader> </ResultHeader>
<template v-if="current"> <template v-if="current">
<!-- 对上了:三栏摆出来就行 --> <!-- 对上了:输入、正确输出、你的输出竖着摆出来就行,你的输出也是绿的 -->
<div v-if="current.result === SubmissionStatus.accepted" class="passed-card"> <div v-if="current.result === SubmissionStatus.accepted" class="passed-card">
<div class="blocks"> <span class="label">输入</span>
<div class="block"> <pre class="pane in">{{ current.input }}</pre>
<div class="label">输入</div> <span class="label">正确输出</span>
<pre>{{ current.input }}</pre> <pre class="pane ok">{{ current.expected }}</pre>
</div> <span class="label">你的输出</span>
<div class="block"> <pre class="pane ok">{{ current.output }}</pre>
<div class="label">正确输出</div>
<pre>{{ current.expected }}</pre>
</div>
<div class="block">
<div class="label">你的输出</div>
<pre>{{ current.output }}</pre>
</div>
</div>
</div> </div>
<!-- 跑完了、输出对不上:和判完之后同一套说明 --> <!-- 跑完了、输出对不上:和判完之后同一套说明 -->
<WrongAnswerExplain <WrongAnswerExplain
@@ -134,6 +126,7 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
:code="samplesCode?.value ?? ''" :code="samplesCode?.value ?? ''"
:language="samplesCode?.language ?? ''" :language="samplesCode?.language ?? ''"
:plain="inContest" :plain="inContest"
section
/> />
<!-- 没跑完:编译不过、运行出错、超时 --> <!-- 没跑完:编译不过、运行出错、超时 -->
<TrialErrorNote <TrialErrorNote
@@ -165,15 +158,16 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
} }
.run { .run {
height: 26px; height: 28px;
box-sizing: border-box; box-sizing: border-box;
padding: 0 10px; padding: 0 10px;
border-radius: 13px; border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font: inherit; font: inherit;
font-size: 13px; font-size: 13px;
font-weight: 600;
cursor: pointer; cursor: pointer;
} }
@@ -189,56 +183,58 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
color: v-bind('theme.errorColorPressed'); color: v-bind('theme.errorColorPressed');
} }
.run.active { /* 选中的那个涂实心:设计稿「运行例子」 */
font-weight: 600;
border-width: 1.5px;
}
.run.ok.active { .run.ok.active {
border-color: v-bind('theme.successColor'); border-color: v-bind('theme.successColor');
background-color: v-bind('theme.successColor');
color: #fff;
} }
.run.bad.active { .run.bad.active {
border-color: v-bind('theme.errorColor'); border-color: v-bind('theme.errorColor');
background-color: v-bind('theme.errorColor');
color: #fff;
} }
/* 对上了:和答错时同一张两列的网格(标签 | 格子),只是你的输出也是绿的 */
.passed-card { .passed-card {
padding: 12px 14px;
border-radius: 6px;
background-color: rgba(128, 128, 128, 0.06);
border: 1px solid v-bind('theme.dividerColor');
}
.blocks {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: 64px minmax(0, 1fr);
gap: 8px; gap: 6px 8px;
} align-items: start;
.block {
min-width: 0;
padding: 6px 10px 8px;
border-radius: 5px;
background-color: v-bind('theme.cardColor');
border: 1px solid v-bind('theme.dividerColor');
} }
.label { .label {
font-size: 12px; font-size: 13px;
opacity: 0.7; line-height: 36px;
margin-bottom: 4px; white-space: nowrap;
color: v-bind('theme.textColor3');
} }
.block pre { .pane {
min-width: 0;
margin: 0; margin: 0;
padding: 5px 10px;
border-radius: 6px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
font-family: Monaco, Consolas, monospace; font-family: Consolas, Monaco, monospace;
font-size: 14px; font-size: 16px;
line-height: 24px;
}
.pane.in {
background-color: rgba(128, 128, 128, 0.07);
border: 1px solid rgba(128, 128, 128, 0.14);
}
.pane.ok {
background-color: rgba(24, 160, 88, 0.08);
border: 1px solid rgba(24, 160, 88, 0.26);
} }
.note { .note {
margin: 12px 0 0; margin: 16px 0 0;
font-size: 13px; font-size: 13px;
color: v-bind('theme.textColor3'); color: v-bind('theme.textColor3');
} }
@@ -5,9 +5,23 @@ import { Icon } from '@iconify/vue'
* (「自己输入数据试试 ›」「放大看 ›」)。设计稿「题目页观感」⑦,2026-11-04 定。 * (「自己输入数据试试 ›」「放大看 ›」)。设计稿「题目页观感」⑦,2026-11-04 定。
* *
* 语法要求、提示用黄色:一个是硬性条件、一个是帮忙的话,扫一眼要能和别的节分开。 * 语法要求、提示用黄色:一个是硬性条件、一个是帮忙的话,扫一眼要能和别的节分开。
* 结果页签里「例子 1 没对上」「第 2 行出错」这类说哪里不对的也用黄色(设计稿结果 X)。
* 明暗两套色都在 CSS 里(html.dark),别在这里调 useDark() —— 理由见 ProblemTypeTag。 * 明暗两套色都在 CSS 里(html.dark),别在这里调 useDark() —— 理由见 ProblemTypeTag。
*/ */
export type SectionIcon = 'description' | 'input' | 'output' | 'samples' | 'hint' | 'rules' | 'table' | 'expected' | 'flowchart' | 'similar' export type SectionIcon =
| 'description'
| 'input'
| 'output'
| 'samples'
| 'hint'
| 'rules'
| 'table'
| 'expected'
| 'flowchart'
| 'similar'
| 'problem'
| 'next'
| 'challenge'
const ICON: Record<SectionIcon, string> = { const ICON: Record<SectionIcon, string> = {
description: 'ph:book-open', description: 'ph:book-open',
@@ -20,6 +34,10 @@ const ICON: Record<SectionIcon, string> = {
expected: 'ph:check-circle', expected: 'ph:check-circle',
flowchart: 'ph:flow-arrow', flowchart: 'ph:flow-arrow',
similar: 'ph:shuffle-angular', similar: 'ph:shuffle-angular',
// 结果页签:哪里不对、接着做、还能挑战
problem: 'ph:warning-circle',
next: 'ph:arrow-right',
challenge: 'ph:target',
} }
const { icon, tone = 'green' } = defineProps<{ const { icon, tone = 'green' } = defineProps<{
@@ -16,6 +16,7 @@ import RuntimeErrorExplain from './RuntimeErrorExplain.vue'
import WrongAnswerExplain from './WrongAnswerExplain.vue' import WrongAnswerExplain from './WrongAnswerExplain.vue'
import LessonNext from './LessonNext.vue' import LessonNext from './LessonNext.vue'
import ResultHeader from './ResultHeader.vue' import ResultHeader from './ResultHeader.vue'
import SectionHead from './SectionHead.vue'
import SimilarProblems from './SimilarProblems.vue' import SimilarProblems from './SimilarProblems.vue'
import ChallengeList from './ChallengeList.vue' import ChallengeList from './ChallengeList.vue'
import ChallengeOutcome from './ChallengeOutcome.vue' import ChallengeOutcome from './ChallengeOutcome.vue'
@@ -361,22 +362,30 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
</div> </div>
</template> </template>
<div v-show="!waitingReveal"> <div v-show="!waitingReveal">
<n-flex vertical v-if="compileError || runtimeError || sampleCheck || msg || infoTable.length || submission.statisticInfo?.ast_results?.length"> <n-flex
vertical
:size="20"
v-if="compileError || runtimeError || sampleCheck || msg || infoTable.length || submission.statisticInfo?.ast_results?.length"
>
<CompileErrorExplain v-if="compileError" :err-info="compileError" :language="submission.language" /> <CompileErrorExplain v-if="compileError" :err-info="compileError" :language="submission.language" />
<RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" /> <RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" />
<WrongAnswerExplain v-if="sampleCheck" full :check="sampleCheck" :code="submission.code" :language="submission.language" /> <WrongAnswerExplain v-if="sampleCheck" full section :check="sampleCheck" :code="submission.code" :language="submission.language" />
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded> <!-- 语法要求逐条对没对:和题面的「语法要求」同一个黄色小节标题 -->
<n-flex vertical :size="8"> <section v-if="submission.statisticInfo?.ast_results?.length">
<n-flex v-for="(rule, i) in submission.statisticInfo.ast_results" :key="i" align="center" :size="6"> <SectionHead icon="rules" tone="amber">语法要求</SectionHead>
<n-icon :color="rule.passed ? theme.successColor : theme.errorColor"> <ul class="ast-rules">
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" /> <li v-for="(rule, i) in submission.statisticInfo.ast_results" :key="i">
</n-icon> <Icon
:icon="rule.passed ? 'ph:check-circle-fill' : 'ph:x-circle-fill'"
:width="17"
:color="rule.passed ? theme.successColor : theme.errorColor"
/>
<span>{{ rule.description }}</span> <span>{{ rule.description }}</span>
<!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 --> <!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 -->
<span v-if="!rule.passed && rule.actual !== undefined" :style="{ color: theme.errorColor }"> 当前 {{ rule.actual }} 次 </span> <span v-if="!rule.passed && rule.actual !== undefined" :style="{ color: theme.errorColor }"> 当前 {{ rule.actual }} 次 </span>
</n-flex> </li>
</n-flex> </ul>
</n-card> </section>
<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>
@@ -386,7 +395,10 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
问过之后,AI 的回答才放进卡片 问过之后,AI 的回答才放进卡片
--> -->
<div v-if="(showAIHint && !hintTarget) || customRunHint" class="actions"> <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="showAIHint && !hintTarget" type="primary" secondary :loading="hintLoading" @click="fetchHint(submission.id)">
<template #icon><Icon icon="ph:sparkle-fill" /></template>
让 AI 分析我的代码
</n-button>
<n-button v-if="customRunHint" text type="primary" @click="submissionStore.revealResult('custom')"> <n-button v-if="customRunHint" text type="primary" @click="submissionStore.revealResult('custom')">
{{ customRunHint }} {{ customRunHint }}
</n-button> </n-button>
@@ -425,7 +437,24 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 16px; gap: 8px 16px;
margin-top: 12px; margin-top: 16px;
}
.ast-rules {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 16px;
line-height: 1.6;
}
.ast-rules li {
display: flex;
align-items: center;
gap: 8px;
} }
/* 手机上「让 AI 分析我的代码」占满一行、44px 高,好按 */ /* 手机上「让 AI 分析我的代码」占满一行、44px 高,好按 */
@@ -6,6 +6,7 @@ import { useProblemStore } from 'oj/store/problem'
import { markdownToText } from 'oj/problem/utils/plainText' import { markdownToText } from 'oj/problem/utils/plainText'
import { JUDGE_STATUS, SubmissionStatus } from 'utils/constants' import { JUDGE_STATUS, SubmissionStatus } from 'utils/constants'
import { classifyDiff, explainDiff, firstDifferentLine, type SampleCheck } from 'utils/wrongAnswer' import { classifyDiff, explainDiff, firstDifferentLine, type SampleCheck } from 'utils/wrongAnswer'
import SectionHead from './SectionHead.vue'
/** /**
* 答案错误时的样例对比(数据见后端 judge/run.ts 的 checkSamples)。 * 答案错误时的样例对比(数据见后端 judge/run.ts 的 checkSamples)。
@@ -31,6 +32,12 @@ const props = defineProps<{
* 提示句全是对学生说的第二人称,整句换掉人称就读得通 * 提示句全是对学生说的第二人称,整句换掉人称就读得通
*/ */
peer?: boolean peer?: boolean
/**
* 题目页结果页签的样子(设计稿「题目页观感」结果 X):黄色小节标题说哪里不对,说明用正文大小,
* 输入 / 正确输出 / 你的输出竖着排成灰、绿、红三格,和题面的例子一个样子。
* 不给的话是原来那张浅灰卡片(提交详情页还在用)
*/
section?: boolean
}>() }>()
const theme = useThemeVars() const theme = useThemeVars()
@@ -114,21 +121,30 @@ function lines(text: string) {
</script> </script>
<template> <template>
<div class="explain-card"> <div :class="section ? 'explain-section' : 'explain-card'">
<p v-if="check.passed" class="explain">题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。</p> <template v-if="section">
<template v-else> <SectionHead icon="problem" tone="amber">
<p class="explain"> {{ check.passed ? '例子都对了,错在没给你看的测试点' : `例子 ${(check.index ?? 0) + 1} 没对上` }}
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? '。' : ':' }}</b </SectionHead>
><template v-if="!plain">{{ hint }}</template> <p v-if="check.passed" class="explain">想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。</p>
</p> <p v-else-if="!plain" class="explain">{{ hint }}</p>
</template>
<p v-else-if="check.passed" class="explain">
题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。
</p>
<p v-else class="explain">
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? '。' : ':' }}</b
><template v-if="!plain">{{ hint }}</template>
</p>
<template v-if="!check.passed">
<div class="blocks"> <div class="blocks">
<div class="block"> <div class="block in">
<div class="label">输入</div> <div class="label">输入</div>
<div class="text"> <div class="text">
<div v-for="(line, i) in (check.input ?? '').split('\n')" :key="i" class="line" v-text="line" /> <div v-for="(line, i) in (check.input ?? '').split('\n')" :key="i" class="line" v-text="line" />
</div> </div>
</div> </div>
<div class="block"> <div class="block ok">
<div class="label">正确输出</div> <div class="label">正确输出</div>
<div class="text"> <div class="text">
<template v-for="(line, i) in lines(check.expected ?? '')" :key="i"> <template v-for="(line, i) in lines(check.expected ?? '')" :key="i">
@@ -139,7 +155,7 @@ function lines(text: string) {
</template> </template>
</div> </div>
</div> </div>
<div class="block"> <div class="block bad">
<div class="label">{{ peer ? '学生' : '你' }}的输出</div> <div class="label">{{ peer ? '学生' : '你' }}的输出</div>
<div v-if="check.output" class="text"> <div v-if="check.output" class="text">
<template v-for="(line, i) in lines(check.output)" :key="i"> <template v-for="(line, i) in lines(check.output)" :key="i">
@@ -241,4 +257,83 @@ function lines(text: string) {
color: v-bind('theme.textColor2'); color: v-bind('theme.textColor2');
line-height: 1.6; line-height: 1.6;
} }
/*
* 题目页结果页签(section):标签一列、格子一列,输入灰、正确输出绿、你的输出红。
* .block 用 display: contents 把「标签 + 格子」直接摊进这张两列的网格
*/
.explain-section {
display: flex;
flex-direction: column;
}
.explain-section .explain {
margin: 0 0 10px;
font-size: 16px;
line-height: 1.75;
}
.explain-section .blocks {
grid-template-columns: 64px minmax(0, 1fr);
gap: 6px 8px;
align-items: start;
}
.explain-section .block {
display: contents;
}
.explain-section .label {
margin: 0;
font-size: 13px;
line-height: 36px;
white-space: nowrap;
}
.explain-section .bad .label {
font-weight: 600;
color: v-bind('theme.errorColor');
}
.explain-section .text {
min-width: 0;
padding: 5px 10px;
border-radius: 6px;
font-size: 16px;
line-height: 24px;
max-height: 260px;
}
.explain-section .text.empty {
font-size: 14px;
}
.explain-section .in .text {
background-color: rgba(128, 128, 128, 0.07);
border: 1px solid rgba(128, 128, 128, 0.14);
}
.explain-section .ok .text {
background-color: rgba(24, 160, 88, 0.08);
border: 1px solid rgba(24, 160, 88, 0.26);
}
.explain-section .bad .text {
background-color: rgba(208, 48, 80, 0.06);
border: 1px solid rgba(208, 48, 80, 0.24);
}
.explain-section .line {
min-height: 24px;
}
.explain-section .cut {
grid-column: 2;
}
.explain-section .requirement {
margin-top: 10px;
font-size: 14px;
color: v-bind('theme.textColor3');
}
</style> </style>
@@ -162,7 +162,15 @@ B 看得到。
### 5.3 「结果」页签 ### 5.3 「结果」页签
三个小分段:**提交结果 / 运行例子 / 自己输入**(胶囊高 30),各自保留最近一次的内容。 三个小分段:**提交结果 / 运行例子 / 自己输入**,各自保留最近一次的内容。
**样子(2026-11-04,画布「题目页观感」结果 X)**:分段是浅灰底的分段按钮(选中白底加粗,时间灰色小字);
状态行是图标 + 22px 大字 + 灰色小字,下面测试点方块(22px)。说哪里不对的一律是黄色小节标题
(「例子 1 没对上」「例子都对了,错在没给你看的测试点」「第 2 行出错」「第 1 行」),说明用正文 16px;
例子对比竖着排成灰(输入)/ 绿(正确输出)/ 红(你的输出)三格,和题面的例子一个样子;
运行时错误把出错那一行代码放进红格。「让 AI 分析我的代码」「用这组数据运行」是浅绿底按钮。
做对了是「接着做」一节 + 写着下一题题号题名的大按钮(原来的浅绿卡片去掉了,竖栏里已经有进度)。
提交详情页的答错对比(`WrongAnswerExplain` 不带 `section`)还是原来那张浅灰卡片。
- **自动切换**:点「运行例子」「提交」或流程图「交给 AI 点评」后,自动切到「结果」对应的分段。 - **自动切换**:点「运行例子」「提交」或流程图「交给 AI 点评」后,自动切到「结果」对应的分段。
- **页签上的状态**:✕ 错了 / ✓ 对了 / ! 没交上(语法没过);用图标,不用 7px 的彩点。 - **页签上的状态**:✕ 错了 / ✓ 对了 / ! 没交上(语法没过);用图标,不用 7px 的彩点。