style(题目): 题目页照设计稿「机房(方案 B)」重排——顶满的两栏、带底色的上下文条、下划线页签、新的结果和题面样式
Deploy / deploy (push) Canceled after 0s
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:
1 parent
83b6b7a81a
commit
1fd9bf67c2
17 files changed
+1800
-706
No files matched your search
@@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useDark } from "@vueuse/core"
|
||||
import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { markdownToText } from "oj/problem/utils/plainText"
|
||||
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
|
||||
import {
|
||||
classifyDiff,
|
||||
@@ -34,16 +32,21 @@ const props = defineProps<{
|
||||
full?: boolean
|
||||
}>()
|
||||
|
||||
const isDark = useDark()
|
||||
const theme = useThemeVars()
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const submissionStore = useSubmissionStore()
|
||||
|
||||
/** 题目要求的输出(题面「输出」那一节的原文,Markdown)。SQL 题没有这一节 */
|
||||
const requirement = computed(() =>
|
||||
props.full && !problem.value?.sqlConfig ? (problem.value?.outputDescription ?? "").trim() : "",
|
||||
)
|
||||
/** 自己输入走 Judge0,SQL 跑不了 */
|
||||
const canCustomRun = computed(() => props.full && !problem.value?.sqlConfig)
|
||||
/**
|
||||
* 卡片末尾那行「题目原话」(设计稿「答案错误」「错在隐藏测试点」):
|
||||
* 例子上就错了 —— 摆题面「输出」那一节,格式错、漏了一行这类,对着原话一看就明白;
|
||||
* 例子都对了、错在隐藏测试点 —— 摆「描述」,边界条件多半写在那里。SQL 题两节都不摆
|
||||
*/
|
||||
const requirement = computed(() => {
|
||||
if (!props.full || !problem.value || problem.value.sqlConfig) return null
|
||||
const source = props.check.passed ? problem.value.description : problem.value.outputDescription
|
||||
const text = markdownToText(source ?? "", props.check.passed ? 160 : 120)
|
||||
if (!text) return null
|
||||
return { label: props.check.passed ? "题目原话" : "题目要求的输出", text }
|
||||
})
|
||||
|
||||
/** 后端每段截到 2000 字,到了上限就是被截过 */
|
||||
const TEXT_LIMIT = 2000
|
||||
@@ -64,6 +67,40 @@ const diffLine = computed(() =>
|
||||
firstDifferentLine(props.check.expected ?? "", props.check.output ?? ""),
|
||||
)
|
||||
|
||||
/**
|
||||
* 不一样的那一行,拆成「相同的开头 / 不同的中间 / 相同的结尾」三段,只把中间那段标红
|
||||
* (设计稿:多出来的「等级:」、错成的「E」、多出来的「333333333333」)。
|
||||
* 标在「你的输出」上;你的输出那段是空的(少了东西)就标在正确输出上,告诉他少了什么
|
||||
*/
|
||||
function splitDiff(expected: string, output: string) {
|
||||
const e = [...expected]
|
||||
const o = [...output]
|
||||
let head = 0
|
||||
while (head < e.length && head < o.length && e[head] === o[head]) head++
|
||||
let tail = 0
|
||||
while (
|
||||
tail < e.length - head &&
|
||||
tail < o.length - head &&
|
||||
e[e.length - 1 - tail] === o[o.length - 1 - tail]
|
||||
)
|
||||
tail++
|
||||
const cut = (chars: string[]) => ({
|
||||
before: chars.slice(0, head).join(""),
|
||||
marked: chars.slice(head, chars.length - tail).join(""),
|
||||
after: chars.slice(chars.length - tail).join(""),
|
||||
})
|
||||
const mine = cut(o)
|
||||
const theirs = cut(e)
|
||||
return mine.marked ? { output: mine, expected: null } : { output: null, expected: theirs }
|
||||
}
|
||||
|
||||
const diffParts = computed(() => {
|
||||
if (!onSample.value || diffLine.value < 0) return null
|
||||
const expected = lines(props.check.expected ?? "")[diffLine.value] ?? ""
|
||||
const output = lines(props.check.output ?? "")[diffLine.value] ?? ""
|
||||
return splitDiff(expected, output)
|
||||
})
|
||||
|
||||
/**
|
||||
* 只差空白的时候把空白画出来,不然两边看着一模一样。
|
||||
*
|
||||
@@ -83,28 +120,17 @@ function lines(text: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card embedded class="explain-card">
|
||||
<n-flex v-if="check.passed" vertical :size="10">
|
||||
<div class="explain">
|
||||
题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。
|
||||
</div>
|
||||
<n-button
|
||||
v-if="canCustomRun"
|
||||
text
|
||||
type="primary"
|
||||
class="custom"
|
||||
@click="submissionStore.revealResult('custom')"
|
||||
>
|
||||
去自己输入试试 ›
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-flex v-else vertical :size="12">
|
||||
<div class="explain">
|
||||
<div class="explain-card">
|
||||
<p v-if="check.passed" class="explain">
|
||||
题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。
|
||||
</p>
|
||||
<template v-else>
|
||||
<p class="explain">
|
||||
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? "。" : ":" }}</b
|
||||
><template v-if="!plain">{{ hint }}</template>
|
||||
</div>
|
||||
</p>
|
||||
<div class="blocks">
|
||||
<div class="block input">
|
||||
<div class="block">
|
||||
<div class="label">输入</div>
|
||||
<div class="text">
|
||||
<div
|
||||
@@ -118,111 +144,84 @@ function lines(text: string) {
|
||||
<div class="block">
|
||||
<div class="label">正确输出</div>
|
||||
<div class="text">
|
||||
<div
|
||||
v-for="(line, i) in lines(check.expected ?? '')"
|
||||
:key="i"
|
||||
class="line"
|
||||
:class="{ diff: onSample && i === diffLine }"
|
||||
v-text="line"
|
||||
/>
|
||||
<template v-for="(line, i) in lines(check.expected ?? '')" :key="i">
|
||||
<div v-if="i === diffLine && diffParts?.expected" class="line">
|
||||
<span v-text="diffParts.expected.before" /><span
|
||||
class="diff"
|
||||
v-text="diffParts.expected.marked"
|
||||
/><span v-text="diffParts.expected.after" />
|
||||
</div>
|
||||
<div v-else class="line" v-text="line" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="label">你的输出</div>
|
||||
<div v-if="check.output" class="text">
|
||||
<div
|
||||
v-for="(line, i) in lines(check.output)"
|
||||
:key="i"
|
||||
class="line"
|
||||
:class="{ diff: onSample && i === diffLine }"
|
||||
v-text="line"
|
||||
/>
|
||||
<template v-for="(line, i) in lines(check.output)" :key="i">
|
||||
<div v-if="i === diffLine && diffParts?.output" class="line">
|
||||
<span v-text="diffParts.output.before" /><span
|
||||
class="diff"
|
||||
v-text="diffParts.output.marked"
|
||||
/><span v-text="diffParts.output.after" />
|
||||
</div>
|
||||
<div v-else class="line" v-text="line" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="text empty">(什么都没有输出)</div>
|
||||
<n-text v-if="check.output && check.output.length >= TEXT_LIMIT" depth="3" class="cut">
|
||||
<div v-if="check.output && check.output.length >= TEXT_LIMIT" class="cut">
|
||||
输出太长,只显示了前面一部分
|
||||
</n-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-flex>
|
||||
<div v-if="requirement" class="requirement">
|
||||
<div class="label">题目要求的输出</div>
|
||||
<MdPreview
|
||||
preview-theme="vuepress"
|
||||
:model-value="requirement"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
<p v-if="requirement" class="requirement">{{ requirement.label }}:{{ requirement.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 设计稿「答案错误」:一块浅灰卡片,三栏并排的白底小框 */
|
||||
.explain-card {
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.custom {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.requirement {
|
||||
margin-top: 14px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
|
||||
/* md-editor 的预览自带白底、一圈 padding 和段落外边距,放在灰卡片里像一块补丁 */
|
||||
.requirement :deep(.md-editor) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.requirement :deep(.md-editor-preview-wrapper) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.requirement :deep(.md-editor-preview > :first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.requirement :deep(.md-editor-preview > :last-child) {
|
||||
margin-bottom: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.explain {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.blocks {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.block.input {
|
||||
grid-column: 1 / -1;
|
||||
padding: 6px 10px 8px;
|
||||
border-radius: 5px;
|
||||
background-color: v-bind("theme.cardColor");
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
|
||||
/* 每行用 v-text 填,不写 {{ line }}:模板里标签两边的换行缩进会被 Vue 压成一个空格,
|
||||
@@ -233,14 +232,26 @@ function lines(text: string) {
|
||||
}
|
||||
|
||||
.diff {
|
||||
background-color: rgba(208, 48, 80, 0.2);
|
||||
background-color: rgba(208, 48, 80, 0.18);
|
||||
color: v-bind("theme.errorColorPressed");
|
||||
}
|
||||
|
||||
.empty {
|
||||
opacity: 0.6;
|
||||
color: v-bind("theme.textColor3");
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cut {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.requirement {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user