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
@@ -31,47 +31,85 @@ function openHisSubmissions() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="collabStore.room" class="collab-bar" role="status">
|
<div v-if="collabStore.room" class="collab-bar" role="status">
|
||||||
|
<span class="dot" aria-hidden="true" />
|
||||||
<span class="who">
|
<span class="who">
|
||||||
正在帮 <b>{{ collabStore.room.peerName }}</b> ·
|
正在帮 {{ collabStore.room.peerName }} ·
|
||||||
{{ LANGUAGE_SHOW_VALUE[collabStore.room.language] }}
|
{{ LANGUAGE_SHOW_VALUE[collabStore.room.language] }}
|
||||||
</span>
|
</span>
|
||||||
<n-button text class="link" @click="openHisSubmissions">看他交过什么 ↗</n-button>
|
<span class="note">你改的他马上能看到</span>
|
||||||
|
<span class="spacer" />
|
||||||
|
<button type="button" class="ghost" @click="openHisSubmissions">看他交过什么</button>
|
||||||
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
|
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
|
||||||
那边只是退回排队 —— 见 store 里 leave 的注释 -->
|
那边只是退回排队 —— 见 store 里 leave 的注释 -->
|
||||||
<n-button size="small" class="end" @click="collabStore.leave('done')">结束协作</n-button>
|
<button type="button" class="end" @click="collabStore.leave('done')">结束协作</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 设计稿「上下文条与协作状态」:深蓝一条,顶在工具栏上面 */
|
||||||
.collab-bar {
|
.collab-bar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 10px 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
padding: 0 12px;
|
background-color: #1d4f9c;
|
||||||
border-radius: 6px;
|
|
||||||
background-color: #2080f0;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
flex: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #7fe0a8;
|
||||||
|
}
|
||||||
|
|
||||||
.who {
|
.who {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.note {
|
||||||
--n-text-color: #fff !important;
|
color: #d5e2f6;
|
||||||
--n-text-color-hover: rgba(255, 255, 255, 0.85) !important;
|
}
|
||||||
--n-text-color-pressed: rgba(255, 255, 255, 0.7) !important;
|
|
||||||
--n-text-color-focus: #fff !important;
|
.spacer {
|
||||||
text-decoration: underline;
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost,
|
||||||
|
.end {
|
||||||
|
flex: none;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #8fb0e0;
|
||||||
|
background: transparent;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.end {
|
.end {
|
||||||
margin-left: auto;
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #1d4f9c;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -7,6 +7,8 @@ import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
|||||||
import { useLessonStore } from "oj/store/lesson"
|
import { useLessonStore } from "oj/store/lesson"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
import type { ProblemSet, ProblemSetProblem } from "utils/types"
|
import type { ProblemSet, ProblemSetProblem } from "utils/types"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||||
import ProblemChips, { type ChipItem } from "./ProblemChips.vue"
|
import ProblemChips, { type ChipItem } from "./ProblemChips.vue"
|
||||||
@@ -27,6 +29,20 @@ import ProblemChips, { type ChipItem } from "./ProblemChips.vue"
|
|||||||
const CHIPS_FIT = 5
|
const CHIPS_FIT = 5
|
||||||
|
|
||||||
const ctx = useProblemPageContext()
|
const ctx = useProblemPageContext()
|
||||||
|
const { isMobile } = useBreakpoints()
|
||||||
|
const theme = useThemeVars()
|
||||||
|
const isDark = useDark()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 三种入口各一种底色(设计稿「上下文条与协作状态」):课堂浅绿、题单浅蓝、比赛浅黄。
|
||||||
|
* 字色在暗色下换成浅一档的,深绿、深蓝、深黄放在暗底上看不清
|
||||||
|
*/
|
||||||
|
const tone = computed(() => ({
|
||||||
|
lessonText: isDark.value ? theme.value.primaryColor : theme.value.primaryColorPressed,
|
||||||
|
setText: isDark.value ? "#8fb4ee" : "#1d4f9c",
|
||||||
|
setBar: isDark.value ? "#6f9be0" : "#2f6bc4",
|
||||||
|
contestText: isDark.value ? "#f2c97d" : "#8a5200",
|
||||||
|
}))
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const currentId = computed(() => problem.value?._id ?? "")
|
const currentId = computed(() => problem.value?._id ?? "")
|
||||||
@@ -85,6 +101,7 @@ const lesson = computed(() => {
|
|||||||
chips,
|
chips,
|
||||||
done: list.filter((item) => item.myStatus === "accepted").length,
|
done: list.filter((item) => item.myStatus === "accepted").length,
|
||||||
total: list.length,
|
total: list.length,
|
||||||
|
position: index + 1,
|
||||||
previous: chips[index - 1]?.to ?? null,
|
previous: chips[index - 1]?.to ?? null,
|
||||||
next: chips[index + 1]?.to ?? null,
|
next: chips[index + 1]?.to ?? null,
|
||||||
}
|
}
|
||||||
@@ -211,115 +228,274 @@ onSolvedHere(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 课堂 -->
|
<!-- 课堂 -->
|
||||||
<div v-if="lesson" class="context-bar">
|
<div v-if="lesson" class="context-bar lesson">
|
||||||
<span class="lead">
|
<span class="lead lesson-lead">这节课 做完 {{ lesson.done }}/{{ lesson.total }}</span>
|
||||||
这节课 <b>做完 {{ lesson.done }}/{{ lesson.total }}</b>
|
<!-- 手机上放不下一排题号:只说是第几道,箭头做大(设计稿「手机:题目 / 代码 / 结果」) -->
|
||||||
</span>
|
<span v-if="isMobile" class="meta">这是第 {{ lesson.position }} 道</span>
|
||||||
<ProblemChips :items="lesson.chips" :current="currentId" />
|
<ProblemChips v-else :items="lesson.chips" :current="currentId" variant="lesson" />
|
||||||
<n-button-group v-if="lesson.total <= CHIPS_FIT" size="small" class="steps">
|
<div class="spacer" />
|
||||||
<n-button :disabled="!lesson.previous" title="上一题" @click="$router.push(lesson.previous!)">
|
<template v-if="isMobile || lesson.total <= CHIPS_FIT">
|
||||||
‹
|
<component
|
||||||
</n-button>
|
:is="lesson.previous ? 'router-link' : 'span'"
|
||||||
<n-button :disabled="!lesson.next" title="下一题" @click="$router.push(lesson.next!)">
|
:to="lesson.previous ?? undefined"
|
||||||
›
|
class="step"
|
||||||
</n-button>
|
:class="{ disabled: !lesson.previous, big: isMobile }"
|
||||||
</n-button-group>
|
aria-label="上一题"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M15 6l-6 6 6 6" />
|
||||||
|
</svg>
|
||||||
|
</component>
|
||||||
|
<component
|
||||||
|
:is="lesson.next ? 'router-link' : 'span'"
|
||||||
|
:to="lesson.next ?? undefined"
|
||||||
|
class="step forward"
|
||||||
|
:class="{ disabled: !lesson.next, big: isMobile }"
|
||||||
|
aria-label="下一题"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 6l6 6-6 6" />
|
||||||
|
</svg>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 题单 -->
|
<!-- 题单 -->
|
||||||
<div v-else-if="setBar" class="context-bar">
|
<div v-else-if="setBar" class="context-bar problemset">
|
||||||
<router-link :to="setBar.home" class="back" :title="`回到题单:${setBar.title}`">
|
<router-link :to="setBar.home" class="back set-back" :title="`回到题单:${setBar.title}`">
|
||||||
<span aria-hidden="true">‹</span>
|
‹ {{ setBar.title }}
|
||||||
<span class="name">{{ setBar.title }}</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
<n-progress
|
<div class="set-progress" aria-hidden="true">
|
||||||
class="set-progress"
|
<div class="set-progress-fill" :style="{ width: `${setBar.percentage}%` }" />
|
||||||
type="line"
|
</div>
|
||||||
:percentage="setBar.percentage"
|
<span class="meta">
|
||||||
:show-indicator="false"
|
做完 {{ setBar.done }}/{{ setBar.total }}
|
||||||
:height="6"
|
|
||||||
status="success"
|
|
||||||
/>
|
|
||||||
<span class="lead">
|
|
||||||
<b>做完 {{ setBar.done }}/{{ setBar.total }}</b>
|
|
||||||
<template v-if="setBar.position"> · 第 {{ setBar.position }} 题</template>
|
<template v-if="setBar.position"> · 第 {{ setBar.position }} 题</template>
|
||||||
</span>
|
</span>
|
||||||
<n-button-group size="small" class="steps">
|
<div class="spacer" />
|
||||||
<n-button :disabled="!setBar.previous" title="上一题" @click="$router.push(setBar.previous!)">
|
<component
|
||||||
‹
|
:is="setBar.previous ? 'router-link' : 'span'"
|
||||||
</n-button>
|
:to="setBar.previous ?? undefined"
|
||||||
<n-button :disabled="!setBar.next" title="下一题" @click="$router.push(setBar.next!)">
|
class="step"
|
||||||
›
|
:class="{ disabled: !setBar.previous, big: isMobile }"
|
||||||
</n-button>
|
aria-label="上一题"
|
||||||
</n-button-group>
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M15 6l-6 6 6 6" />
|
||||||
|
</svg>
|
||||||
|
</component>
|
||||||
|
<component
|
||||||
|
:is="setBar.next ? 'router-link' : 'span'"
|
||||||
|
:to="setBar.next ?? undefined"
|
||||||
|
class="step forward"
|
||||||
|
:class="{ disabled: !setBar.next, big: isMobile }"
|
||||||
|
aria-label="下一题"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 6l6 6-6 6" />
|
||||||
|
</svg>
|
||||||
|
</component>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 比赛 -->
|
<!-- 比赛 -->
|
||||||
<div v-else-if="contestBar" class="context-bar">
|
<div v-else-if="contestBar" class="context-bar contest">
|
||||||
<router-link :to="contestBar.home" class="back" :title="`回到比赛:${contestBar.title}`">
|
<router-link
|
||||||
<span aria-hidden="true">‹</span>
|
:to="contestBar.home"
|
||||||
<span class="name">{{ contestBar.title }}</span>
|
class="back contest-back"
|
||||||
</router-link>
|
:title="`回到比赛:${contestBar.title}`"
|
||||||
<ProblemChips :items="contestBar.chips" :current="currentId" />
|
>
|
||||||
<span v-if="contestBar.remaining" class="lead remaining">
|
‹ {{ contestBar.title }}
|
||||||
还剩 <b>{{ contestBar.remaining }}</b>
|
|
||||||
</span>
|
|
||||||
<span v-else class="lead">已结束</span>
|
|
||||||
<router-link :to="contestBar.rank" class="steps">
|
|
||||||
<n-button size="small">排名</n-button>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<ProblemChips :items="contestBar.chips" :current="currentId" variant="contest" />
|
||||||
|
<div class="spacer" />
|
||||||
|
<template v-if="contestBar.remaining">
|
||||||
|
<span class="meta">还剩</span>
|
||||||
|
<span class="countdown">{{ contestBar.remaining }}</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="meta">已结束</span>
|
||||||
|
<router-link :to="contestBar.rank" class="contest-rank">排名</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.context-bar {
|
.context-bar {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
flex: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 12px 0 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
padding: 0 2px;
|
|
||||||
flex: none;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson {
|
||||||
|
background-color: rgba(24, 160, 88, 0.07);
|
||||||
|
border-bottom-color: rgba(24, 160, 88, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problemset {
|
||||||
|
gap: 10px;
|
||||||
|
background-color: rgba(47, 107, 196, 0.06);
|
||||||
|
border-bottom-color: rgba(47, 107, 196, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest {
|
||||||
|
gap: 8px;
|
||||||
|
padding-right: 16px;
|
||||||
|
background-color: rgba(208, 138, 28, 0.08);
|
||||||
|
border-bottom-color: rgba(208, 138, 28, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remaining b {
|
.lesson-lead {
|
||||||
font-variant-numeric: tabular-nums;
|
color: v-bind("tone.lessonText");
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
flex: none;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back {
|
.back {
|
||||||
display: inline-flex;
|
flex: 0 1 auto;
|
||||||
align-items: center;
|
min-width: 0;
|
||||||
gap: 2px;
|
max-width: 200px;
|
||||||
/* 名字不跟题号那段抢地方:题号多了本来就能横向滚,名字挤成两个字就认不出了 */
|
overflow: hidden;
|
||||||
flex: none;
|
text-overflow: ellipsis;
|
||||||
max-width: 160px;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back:hover {
|
.back:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.set-back {
|
||||||
overflow: hidden;
|
color: v-bind("tone.setText");
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
|
|
||||||
|
.contest-back {
|
||||||
|
max-width: 160px;
|
||||||
|
color: v-bind("tone.contestText");
|
||||||
}
|
}
|
||||||
|
|
||||||
.set-progress {
|
.set-progress {
|
||||||
flex: 1 1 60px;
|
flex: none;
|
||||||
min-width: 40px;
|
width: 110px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(47, 107, 196, 0.16);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps {
|
.set-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: v-bind("tone.setBar");
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-left: auto;
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: v-bind("tone.contestText");
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-rank {
|
||||||
|
flex: none;
|
||||||
|
color: v-bind("tone.contestText");
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-rank:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ‹ ›:不带边框的箭头,「下一题」那个上色 */
|
||||||
|
.step {
|
||||||
|
flex: none;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.big {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.forward {
|
||||||
|
color: v-bind("tone.lessonText");
|
||||||
|
}
|
||||||
|
|
||||||
|
.problemset .step.forward {
|
||||||
|
color: v-bind("tone.setText");
|
||||||
|
}
|
||||||
|
|
||||||
|
.step:not(.disabled):hover {
|
||||||
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -10,7 +10,7 @@ import { useUserStore } from "shared/store/user"
|
|||||||
import storage from "utils/storage"
|
import storage from "utils/storage"
|
||||||
import type { LANGUAGE } from "utils/types"
|
import type { LANGUAGE } from "utils/types"
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import { NFlex } from "naive-ui"
|
import { NFlex, useThemeVars } from "naive-ui"
|
||||||
import SubmitCode from "./SubmitCode.vue"
|
import SubmitCode from "./SubmitCode.vue"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||||
import { statisticsOpen, useEditorMenu } from "../composables/editorMenu"
|
import { statisticsOpen, useEditorMenu } from "../composables/editorMenu"
|
||||||
@@ -44,6 +44,7 @@ const mode = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「运行例子」:用题目里的例子试跑(Judge0),结果在左栏「结果」页签。顶替原来题面里
|
* 「运行例子」:用题目里的例子试跑(Judge0),结果在左栏「结果」页签。顶替原来题面里
|
||||||
@@ -119,8 +120,8 @@ const toggleHelp = () => {
|
|||||||
* 所以次要的按钮收进「更多」、「写代码 / 画流程图」缩短一点(设计文档 5.4)。语言下拉不收:
|
* 所以次要的按钮收进「更多」、「写代码 / 画流程图」缩短一点(设计文档 5.4)。语言下拉不收:
|
||||||
* 窄于 120px 时图标和「Python」会被挤成两行
|
* 窄于 120px 时图标和「Python」会被挤成两行
|
||||||
*/
|
*/
|
||||||
const toolbarRef = useTemplateRef<{ $el: HTMLElement }>("toolbarRef")
|
const toolbarRef = useTemplateRef<HTMLElement>("toolbarRef")
|
||||||
const { width: toolbarWidth } = useElementSize(() => toolbarRef.value?.$el)
|
const { width: toolbarWidth } = useElementSize(toolbarRef)
|
||||||
const narrow = computed(() => isDesktop.value && toolbarWidth.value > 0 && toolbarWidth.value < 600)
|
const narrow = computed(() => isDesktop.value && toolbarWidth.value > 0 && toolbarWidth.value < 600)
|
||||||
/** 「课堂统计」常驻在工具栏上:桌面、而且放得下 */
|
/** 「课堂统计」常驻在工具栏上:桌面、而且放得下 */
|
||||||
const statisticsInline = computed(() => isDesktop.value && !narrow.value)
|
const statisticsInline = computed(() => isDesktop.value && !narrow.value)
|
||||||
@@ -152,7 +153,8 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-flex ref="toolbarRef" align="center" :size="narrow ? 8 : 12">
|
<!-- 设计稿:语言、运行例子、提交靠左;求助、课堂统计、⋯ 推到最右 -->
|
||||||
|
<div ref="toolbarRef" class="toolbar" :class="{ narrow }">
|
||||||
<!-- 协作中的老师不会落到画图:求助入口本身就排掉了流程图 -->
|
<!-- 协作中的老师不会落到画图:求助入口本身就排掉了流程图 -->
|
||||||
<n-radio-group v-if="canDraw && !showCollabBar" v-model:value="mode" :size="buttonSize">
|
<n-radio-group v-if="canDraw && !showCollabBar" v-model:value="mode" :size="buttonSize">
|
||||||
<n-radio-button value="code">{{ narrow ? "代码" : "写代码" }}</n-radio-button>
|
<n-radio-button value="code">{{ narrow ? "代码" : "写代码" }}</n-radio-button>
|
||||||
@@ -174,6 +176,8 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
<n-button
|
<n-button
|
||||||
v-if="canRunSamples && isDesktop"
|
v-if="canRunSamples && isDesktop"
|
||||||
:size="buttonSize"
|
:size="buttonSize"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
:loading="samplesRunning"
|
:loading="samplesRunning"
|
||||||
:disabled="!codeStore.code.value.trim()"
|
:disabled="!codeStore.code.value.trim()"
|
||||||
@click="submissionStore.runSamples()"
|
@click="submissionStore.runSamples()"
|
||||||
@@ -185,6 +189,22 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
|
|
||||||
<SubmitCode v-else-if="isDesktop" />
|
<SubmitCode v-else-if="isDesktop" />
|
||||||
|
|
||||||
|
<div class="spacer" />
|
||||||
|
|
||||||
|
<n-button
|
||||||
|
v-if="showHelpButton"
|
||||||
|
:size="buttonSize"
|
||||||
|
:type="helpButtonType"
|
||||||
|
:secondary="collabStore.helpStatus !== 'idle'"
|
||||||
|
:disabled="collabStore.helpStatus === 'active'"
|
||||||
|
:title="helpButtonTitle"
|
||||||
|
class="help"
|
||||||
|
@click="toggleHelp"
|
||||||
|
>
|
||||||
|
<span v-if="collabStore.helpStatus === 'active'" class="dot" aria-hidden="true" />
|
||||||
|
{{ helpButtonText }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
<n-button
|
<n-button
|
||||||
v-if="statisticsInline && userStore.isTeacherOrAbove"
|
v-if="statisticsInline && userStore.isTeacherOrAbove"
|
||||||
:size="buttonSize"
|
:size="buttonSize"
|
||||||
@@ -200,20 +220,44 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
>
|
>
|
||||||
<n-button :size="buttonSize" :title="narrow ? '更多' : undefined">
|
<n-button :size="buttonSize" class="more" aria-label="更多:去自测猫、复制代码、重置代码……">
|
||||||
{{ narrow ? "⋯" : "更多" }}
|
⋯
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
<n-button
|
|
||||||
v-if="showHelpButton"
|
|
||||||
:size="buttonSize"
|
|
||||||
:type="helpButtonType"
|
|
||||||
:disabled="collabStore.helpStatus === 'active'"
|
|
||||||
:title="helpButtonTitle"
|
|
||||||
@click="toggleHelp"
|
|
||||||
>
|
|
||||||
{{ helpButtonText }}
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
height: 48px;
|
||||||
|
flex: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar.narrow {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
width: 34px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: v-bind("theme.successColor");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
import { useLessonStore } from "oj/store/lesson"
|
import { useLessonStore } from "oj/store/lesson"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,45 +19,127 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const lessonStore = useLessonStore()
|
const lessonStore = useLessonStore()
|
||||||
|
const theme = useThemeVars()
|
||||||
|
// 暗色主题的主色是浅薄荷绿,按钮上的白字看不清,换深色字(和 Naive 自己的主按钮一样)
|
||||||
|
const isDark = useDark()
|
||||||
|
const buttonText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
|
||||||
|
|
||||||
// 一般课堂条早就拉过了,这里只是兜底(旧了才拉)
|
// 一般课堂条早就拉过了,这里只是兜底(旧了才拉)
|
||||||
onMounted(() => lessonStore.load())
|
onMounted(() => lessonStore.load())
|
||||||
|
|
||||||
const lesson = computed(() => lessonStore.nextAfter(props.problemDisplayId))
|
const lesson = computed(() => lessonStore.nextAfter(props.problemDisplayId))
|
||||||
|
|
||||||
const label = computed(() => lessonStore.label)
|
// 设计稿和上下文条都叫「这节课」,不区分是老师布置的还是从同班提交推断的
|
||||||
|
const label = "这节课"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card v-if="lesson" size="small" embedded class="lesson-next">
|
<!-- 设计稿「答案正确:下一题在结果页签里」:浅绿卡片,一个 44px 的大按钮 -->
|
||||||
<n-flex v-if="lesson.next" align="center" justify="space-between" :wrap="false">
|
<div v-if="lesson" class="lesson-next">
|
||||||
<n-text depth="3">{{ label }}还剩 {{ lesson.remaining }} 道</n-text>
|
<template v-if="lesson.next">
|
||||||
<router-link :to="`/problem/${lesson.next.problemDisplayId}`">
|
<div class="text">
|
||||||
<n-button type="primary">
|
<span class="headline">{{ label }}还剩 {{ lesson.remaining }} 道</span>
|
||||||
下一题:{{ lesson.next.problemDisplayId }} {{ lesson.next.title }}
|
<span class="detail">
|
||||||
</n-button>
|
{{ lesson.next.problemDisplayId }}
|
||||||
|
{{ lesson.next.myStatus === "tried" ? "你做过,还没对" : "你还没做过" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<router-link :to="`/problem/${lesson.next.problemDisplayId}`" class="next">
|
||||||
|
<span class="next-label"
|
||||||
|
>下一题:{{ lesson.next.problemDisplayId }} {{ lesson.next.title }}</span
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||||
|
</svg>
|
||||||
</router-link>
|
</router-link>
|
||||||
</n-flex>
|
</template>
|
||||||
<n-flex v-else align="center" justify="space-between">
|
<template v-else>
|
||||||
<n-text>{{ label }}都做完了,一共 {{ lesson.total }} 道 🎉</n-text>
|
<div class="text">
|
||||||
<router-link to="/problemset">
|
<span class="headline">{{ label }}都做完了,一共 {{ lesson.total }} 道</span>
|
||||||
<n-button secondary>去题单继续练</n-button>
|
<span class="detail">想接着练,可以去题单里挑一个</span>
|
||||||
|
</div>
|
||||||
|
<router-link to="/problemset" class="next secondary">
|
||||||
|
<span class="next-label">去题单继续练</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</n-flex>
|
</template>
|
||||||
</n-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.lesson-next {
|
.lesson-next {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
max-width: 560px;
|
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;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
align-self: flex-start;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: v-bind("theme.primaryColor");
|
||||||
|
color: v-bind("buttonText");
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next:hover {
|
||||||
|
background-color: v-bind("theme.primaryColorHover");
|
||||||
|
}
|
||||||
|
|
||||||
|
.next:focus-visible {
|
||||||
|
outline: 2px solid v-bind("theme.primaryColorPressed");
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next.secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid v-bind("theme.primaryColor");
|
||||||
|
color: v-bind("theme.primaryColorPressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 题目名可能很长(带 emoji 的、二十几个字的),按钮别把结果面板撑宽 */
|
/* 题目名可能很长(带 emoji 的、二十几个字的),按钮别把结果面板撑宽 */
|
||||||
.lesson-next :deep(.n-button__content) {
|
.next-label {
|
||||||
display: block;
|
min-width: 0;
|
||||||
max-width: 320px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -5,8 +5,9 @@ import type { RouteLocationRaw } from "vue-router"
|
|||||||
/**
|
/**
|
||||||
* 上下文条里的一排题号(课堂、比赛共用)。只放题号,题目名在鼠标停上去时给。
|
* 上下文条里的一排题号(课堂、比赛共用)。只放题号,题目名在鼠标停上去时给。
|
||||||
*
|
*
|
||||||
* 状态用图标区分,不只靠颜色:✓ 做完(实心绿)/ ! 做过没对(橙)/ 灰底 没做;当前题描边。
|
* 状态用图标区分,不只靠颜色:✓ 做完(实心绿)/ 做过没对(课堂是橙色 !,比赛是红色 ✕)/
|
||||||
* 一节课一般 5 道以内,一行放得下;多了(比赛、老师一次布置了一长串)就横向滚动,
|
* 灰底 没做;当前题描边,做完了的当前题实心绿再套一圈。课堂是圆角胶囊、比赛是方角
|
||||||
|
* (设计稿「上下文条与协作状态」)。一节课一般 5 道以内,一行放得下;多了就横向滚动,
|
||||||
* 当前题保持在视野里。
|
* 当前题保持在视野里。
|
||||||
*/
|
*/
|
||||||
export interface ChipItem {
|
export interface ChipItem {
|
||||||
@@ -19,17 +20,22 @@ export interface ChipItem {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: ChipItem[]
|
items: ChipItem[]
|
||||||
current: string
|
current: string
|
||||||
|
variant: "lesson" | "contest"
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
/**
|
/** 「做完」的实心绿上用什么字色:暗色主题的成功色是浅薄荷绿,白字几乎看不见 */
|
||||||
* 「做完」的实心绿上用什么字色。暗色主题的成功色是浅薄荷绿,白字几乎看不见 ——
|
|
||||||
* 和 Naive 自己的主按钮一样,暗色下换深色字
|
|
||||||
*/
|
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
const doneText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
|
const doneText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
|
||||||
|
|
||||||
const STATUS_TEXT = { done: "做完了", tried: "做过,还没对", none: "还没做" } as const
|
const STATUS_TEXT = computed(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
done: "做完了",
|
||||||
|
tried: props.variant === "contest" ? "交过,还没对" : "做过,还没对",
|
||||||
|
none: "还没做",
|
||||||
|
}) as const,
|
||||||
|
)
|
||||||
|
|
||||||
const isCurrent = (item: ChipItem) => item.displayId.toLowerCase() === props.current.toLowerCase()
|
const isCurrent = (item: ChipItem) => item.displayId.toLowerCase() === props.current.toLowerCase()
|
||||||
|
|
||||||
@@ -49,7 +55,8 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="scroller" class="chips">
|
<!-- 类名带前缀:父组件(ContextBar)的 .lesson / .contest 会作用到子组件根元素上 -->
|
||||||
|
<div ref="scroller" class="chips" :class="`chips-${variant}`">
|
||||||
<n-tooltip v-for="item in items" :key="item.displayId" :delay="300">
|
<n-tooltip v-for="item in items" :key="item.displayId" :delay="300">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -59,9 +66,48 @@ watch(
|
|||||||
:aria-label="`${item.displayId} ${item.title}(${STATUS_TEXT[item.status]})`"
|
:aria-label="`${item.displayId} ${item.title}(${STATUS_TEXT[item.status]})`"
|
||||||
:aria-current="isCurrent(item) ? 'page' : undefined"
|
:aria-current="isCurrent(item) ? 'page' : undefined"
|
||||||
>
|
>
|
||||||
<!-- 用字符不用图标:图标是从 Iconify 源现拉的,拉不到时状态就只剩颜色 -->
|
<!-- 内嵌 SVG,不走 Iconify:图标源拉不到时状态就只剩颜色了 -->
|
||||||
<span v-if="item.status === 'done'" aria-hidden="true">✓</span>
|
<svg
|
||||||
<span v-else-if="item.status === 'tried'" aria-hidden="true">!</span>
|
v-if="item.status === 'done'"
|
||||||
|
width="11"
|
||||||
|
height="11"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M5 12l5 5 9-10" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="item.status === 'tried' && variant === 'contest'"
|
||||||
|
width="11"
|
||||||
|
height="11"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M6 6l12 12M18 6L6 18" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="item.status === 'tried'"
|
||||||
|
width="11"
|
||||||
|
height="11"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 7v6" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
<span>{{ item.displayId }}</span>
|
<span>{{ item.displayId }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
@@ -78,9 +124,13 @@ watch(
|
|||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: none;
|
||||||
/* 描边的外圈别被裁掉 */
|
/* 当前题外面那一圈别被裁掉 */
|
||||||
padding: 3px 2px;
|
padding: 4px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
@@ -89,14 +139,19 @@ watch(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
padding: 0 9px;
|
box-sizing: border-box;
|
||||||
|
padding: 0 8px;
|
||||||
border-radius: 13px;
|
border-radius: 13px;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: v-bind("theme.textColor2");
|
color: v-bind("theme.textColor2");
|
||||||
background-color: rgba(128, 128, 128, 0.12);
|
background-color: rgba(128, 128, 128, 0.14);
|
||||||
border: 1px solid transparent;
|
}
|
||||||
|
|
||||||
|
.chips-contest .chip {
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.done {
|
.chip.done {
|
||||||
@@ -105,23 +160,36 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chip.tried {
|
.chip.tried {
|
||||||
color: v-bind("theme.warningColor");
|
color: v-bind("theme.warningColorPressed");
|
||||||
background-color: rgba(240, 160, 32, 0.12);
|
background-color: rgba(240, 160, 32, 0.12);
|
||||||
border-color: rgba(240, 160, 32, 0.5);
|
border: 1px solid rgba(240, 160, 32, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.current {
|
.chips-contest .chip.tried {
|
||||||
border-color: v-bind("theme.primaryColor");
|
color: v-bind("theme.errorColorPressed");
|
||||||
box-shadow: 0 0 0 1px v-bind("theme.primaryColor");
|
background-color: rgba(208, 48, 80, 0.12);
|
||||||
font-weight: 600;
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip.current:not(.done) {
|
||||||
|
color: v-bind("theme.primaryColorPressed");
|
||||||
|
background-color: v-bind("theme.cardColor");
|
||||||
|
border: 1.5px solid v-bind("theme.primaryColor");
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chips-contest .chip.current:not(.done) {
|
||||||
|
color: v-bind("theme.warningColorPressed");
|
||||||
|
border-color: v-bind("theme.warningColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当前题做完了:实心绿,外面再套一圈(中间隔一道底色) */
|
/* 当前题做完了:实心绿,外面再套一圈(中间隔一道底色) */
|
||||||
.chip.done.current {
|
.chip.done.current {
|
||||||
border-color: v-bind("theme.successColor");
|
font-weight: 700;
|
||||||
|
border-color: v-bind("theme.primaryColorPressed");
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px v-bind("theme.bodyColor"),
|
0 0 0 2px v-bind("theme.cardColor"),
|
||||||
0 0 0 3px v-bind("theme.successColor");
|
0 0 0 3.5px v-bind("theme.primaryColorPressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip:hover {
|
.chip:hover {
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { DIFFICULTY } from "utils/constants"
|
import { DIFFICULTY } from "utils/constants"
|
||||||
import { getTagColor } from "utils/functions"
|
import { copyToClipboard, getTagColor } from "utils/functions"
|
||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import Copy from "shared/components/Copy.vue"
|
|
||||||
import { useDark } from "@vueuse/core"
|
import { useDark } from "@vueuse/core"
|
||||||
import { MdPreview } from "md-editor-v3"
|
import { MdPreview } from "md-editor-v3"
|
||||||
import "md-editor-v3/lib/preview.css"
|
import "md-editor-v3/lib/preview.css"
|
||||||
import SQLDataTable from "./SQLDataTable.vue"
|
import SQLDataTable from "./SQLDataTable.vue"
|
||||||
import SimilarProblems from "./SimilarProblems.vue"
|
import SimilarProblems from "./SimilarProblems.vue"
|
||||||
import { useFlowchartStore } from "oj/store/flowchart"
|
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
||||||
import { useMyFlowchartStore } from "shared/store/myFlowchart"
|
import { useMyFlowchartStore } from "shared/store/myFlowchart"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||||
@@ -20,6 +19,7 @@ const ProblemFlowchart = defineAsyncComponent(() => import("./ProblemFlowchart.v
|
|||||||
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
||||||
|
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
const theme = useThemeVars()
|
||||||
const problemStore = useProblemStore()
|
const problemStore = useProblemStore()
|
||||||
const { problem } = storeToRefs(problemStore)
|
const { problem } = storeToRefs(problemStore)
|
||||||
const ctx = useProblemPageContext()
|
const ctx = useProblemPageContext()
|
||||||
@@ -46,16 +46,14 @@ const showSimilar = computed(
|
|||||||
* 通过」),占掉一屏里最值钱的那几行。题单里不给(入口差异表)。
|
* 通过」),占掉一屏里最值钱的那几行。题单里不给(入口差异表)。
|
||||||
*/
|
*/
|
||||||
const myStatus = computed(() => {
|
const myStatus = computed(() => {
|
||||||
|
if (!ctx.value.statusTag || !userStore.isAuthed) return null
|
||||||
const status = problem.value?.myStatus
|
const status = problem.value?.myStatus
|
||||||
if (!ctx.value.statusTag || status === undefined || status === null) return null
|
// 设计稿:没交过也写一个「还没交过」,状态这一格始终在
|
||||||
|
if (status === undefined || status === null) return "none"
|
||||||
return status === 0 ? "solved" : "tried"
|
return status === 0 ? "solved" : "tried"
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 「限时 1 秒」:学生不需要 ms / MB。1000 → 1,1500 → 1.5 */
|
// 不写「限时 1 秒 / 内存 256MB」:中职学生看了只会犯迷糊,超时、超内存判出来时结果页签里自有说明
|
||||||
const timeLimitText = computed(() => {
|
|
||||||
const ms = problem.value?.timeLimit ?? 0
|
|
||||||
return `限时 ${Number((ms / 1000).toFixed(2))} 秒`
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==================== 流程图小节 ====================
|
// ==================== 流程图小节 ====================
|
||||||
// 同一个位置二选一:老师给的参考图(showFlowchart,默认折叠),或者学生自己画到 A/S 的
|
// 同一个位置二选一:老师给的参考图(showFlowchart,默认折叠),或者学生自己画到 A/S 的
|
||||||
@@ -68,6 +66,38 @@ const myFlowchartStore = useMyFlowchartStore()
|
|||||||
const flowchartStore = useFlowchartStore()
|
const flowchartStore = useFlowchartStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const myFlowchartZoom = ref(false)
|
const myFlowchartZoom = ref(false)
|
||||||
|
/** 参考流程图默认折叠;自己画的那张默认展开、可以收起(设计稿「流程图 A 级之后」) */
|
||||||
|
const referenceOpen = ref(false)
|
||||||
|
const myFlowchartOpen = ref(true)
|
||||||
|
watch(
|
||||||
|
() => problem.value?._id,
|
||||||
|
() => {
|
||||||
|
referenceOpen.value = false
|
||||||
|
myFlowchartOpen.value = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 流程图画到 A / S 了:标题下给个「流程图 A 级」 */
|
||||||
|
const flowchartGrade = computed(() => {
|
||||||
|
const passed = flowchartStore.scores.filter((row) => isFlowchartPass(row.grade))
|
||||||
|
if (!passed.length) return ""
|
||||||
|
return passed.some((row) => row.grade === "S") ? "S" : "A"
|
||||||
|
})
|
||||||
|
|
||||||
|
// 例子旁的「复制」:点完变成「已复制」一秒
|
||||||
|
const message = useMessage()
|
||||||
|
const copiedKey = ref("")
|
||||||
|
const { start: resetCopied } = useTimeoutFn(() => (copiedKey.value = ""), 1000, {
|
||||||
|
immediate: false,
|
||||||
|
})
|
||||||
|
async function copySample(key: string, text: string) {
|
||||||
|
if (await copyToClipboard(text)) {
|
||||||
|
copiedKey.value = key
|
||||||
|
resetCopied()
|
||||||
|
} else {
|
||||||
|
message.error("复制失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 这道题能画流程图,就去查画到 A/S 没有 —— 原来要先切到「流程图」这门「语言」才查。
|
// 这道题能画流程图,就去查画到 A/S 没有 —— 原来要先切到「流程图」这门「语言」才查。
|
||||||
// 不看 canDraw:手机上画不了,但以前在电脑上画好的那张照样该摆出来
|
// 不看 canDraw:手机上画不了,但以前在电脑上画好的那张照样该摆出来
|
||||||
@@ -104,11 +134,11 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="problem">
|
<div v-if="problem">
|
||||||
<header class="problem-head">
|
<header class="problem-head">
|
||||||
<n-flex align="center" :size="10" :wrap="false">
|
<div class="title-row">
|
||||||
<n-tag :bordered="false">{{ problem._id }}</n-tag>
|
<span class="pid">{{ problem._id }}</span>
|
||||||
<h2 class="problemTitle">{{ problem.title }}</h2>
|
<h1 class="problem-title">{{ problem.title }}</h1>
|
||||||
</n-flex>
|
</div>
|
||||||
<n-flex align="center" :size="8" class="problem-meta">
|
<div class="meta">
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="problem.difficulty"
|
v-if="problem.difficulty"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -117,88 +147,115 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
>
|
>
|
||||||
{{ DIFFICULTY[problem.difficulty] }}
|
{{ DIFFICULTY[problem.difficulty] }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="myStatus === 'solved'" size="small" type="success" :bordered="false" round>
|
<n-tag v-if="myStatus === 'solved'" size="small" type="success" :bordered="false">
|
||||||
✓ 已解决
|
已解决
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-else-if="myStatus === 'tried'" size="small" type="warning" :bordered="false" round>
|
<n-tag v-else-if="myStatus === 'tried'" size="small" type="warning" :bordered="false">
|
||||||
! 尝试过,还没通过
|
做过,还没对
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-text depth="3" v-if="!isSQL">{{ timeLimitText }}</n-text>
|
<n-tag v-else-if="myStatus === 'none'" size="small" :bordered="false">还没交过</n-tag>
|
||||||
</n-flex>
|
<n-tag v-if="flowchartGrade" size="small" type="success" :bordered="false">
|
||||||
|
流程图 {{ flowchartGrade }} 级
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 代码要求(AST 规则)放在最前面:它是硬性的判定条件,写到一半才看见就晚了 -->
|
<!-- 代码要求(AST 规则)放在最前面:它是硬性的判定条件,写到一半才看见就晚了 -->
|
||||||
<div v-if="astRequirements.length > 0" class="requirements">
|
<div v-if="astRequirements.length > 0" class="requirements">
|
||||||
<n-flex align="center" :size="6" class="requirements-head">
|
<span class="requirements-head">代码要求</span>
|
||||||
<Icon icon="streamline-ultimate-color:check-button" :width="18"></Icon>
|
<template v-for="[lang, rules] in astRequirements" :key="lang">
|
||||||
<span>代码要求</span>
|
|
||||||
<n-text depth="3" class="requirements-note">提交时会逐条检查</n-text>
|
|
||||||
</n-flex>
|
|
||||||
<div v-for="[lang, rules] in astRequirements" :key="lang" class="requirements-lang">
|
|
||||||
<span v-if="astRequirements.length > 1" class="lang-label">{{ lang }}</span>
|
<span v-if="astRequirements.length > 1" class="lang-label">{{ lang }}</span>
|
||||||
<n-flex :size="6">
|
<n-tag
|
||||||
<n-tag v-for="(rule, i) in rules" :key="i" :type="KIND_TAG_TYPE[rule.kind]" size="small">
|
v-for="(rule, i) in rules"
|
||||||
{{ rule.description }}
|
:key="`${lang}-${i}`"
|
||||||
</n-tag>
|
:type="KIND_TAG_TYPE[rule.kind]"
|
||||||
</n-flex>
|
size="small"
|
||||||
</div>
|
:bordered="false"
|
||||||
|
>
|
||||||
|
{{ rule.description }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
<span class="requirements-note">提交时会逐条检查</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="title">
|
<section class="block">
|
||||||
<Icon icon="streamline-ultimate-color:checklist"></Icon>
|
<h3 class="title">描述</h3>
|
||||||
描述
|
|
||||||
</h3>
|
|
||||||
<MdPreview
|
|
||||||
preview-theme="vuepress"
|
|
||||||
:model-value="problem.description"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-if="!isSQL">
|
|
||||||
<h3 class="title">
|
|
||||||
<Icon icon="streamline-ultimate-color:envelope-back-front"></Icon>
|
|
||||||
输入
|
|
||||||
</h3>
|
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="vuepress"
|
preview-theme="vuepress"
|
||||||
:model-value="problem.inputDescription"
|
:model-value="problem.description"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h3 class="title">
|
<!-- 输入、输出一般都只有一两句,左右摆开省半屏 -->
|
||||||
<Icon icon="streamline-ultimate-color:mailbox-post"></Icon>
|
<div v-if="!isSQL" class="io">
|
||||||
输出
|
<section class="block">
|
||||||
</h3>
|
<h3 class="title">输入</h3>
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="vuepress"
|
preview-theme="vuepress"
|
||||||
:model-value="problem.outputDescription"
|
:model-value="problem.inputDescription"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
</template>
|
</section>
|
||||||
|
<section class="block">
|
||||||
|
<h3 class="title">输出</h3>
|
||||||
|
<MdPreview
|
||||||
|
preview-theme="vuepress"
|
||||||
|
:model-value="problem.outputDescription"
|
||||||
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-collapse v-if="canShowReferenceFlowchart" class="flowchart-section">
|
<!-- 老师给的参考流程图:一行可以点开的条,默认折叠 -->
|
||||||
<n-collapse-item name="reference">
|
<section v-if="canShowReferenceFlowchart" class="block">
|
||||||
<template #header>
|
<button
|
||||||
<span class="title collapse-title">
|
type="button"
|
||||||
<Icon icon="vscode-icons:file-type-drawio"></Icon>
|
class="fold"
|
||||||
参考流程图
|
:aria-expanded="referenceOpen"
|
||||||
</span>
|
@click="referenceOpen = !referenceOpen"
|
||||||
</template>
|
>
|
||||||
|
<svg
|
||||||
|
class="fold-caret"
|
||||||
|
:class="{ open: referenceOpen }"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 6l6 6-6 6" />
|
||||||
|
</svg>
|
||||||
|
<span class="fold-title">老师给的参考流程图</span>
|
||||||
|
<span class="fold-note">{{ referenceOpen ? "收起" : "点开看" }}</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="referenceOpen" class="fold-body">
|
||||||
<ProblemFlowchart />
|
<ProblemFlowchart />
|
||||||
</n-collapse-item>
|
</div>
|
||||||
</n-collapse>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="myFlowchartStore.showing" class="flowchart-section">
|
<!-- 自己画到 A/S 的那张:默认展开一小块,可以放大、可以收起 -->
|
||||||
<n-flex align="center" justify="space-between">
|
<section v-else-if="myFlowchartStore.showing" class="block">
|
||||||
<h3 class="title">
|
<div class="block-head">
|
||||||
<Icon icon="vscode-icons:file-type-drawio"></Icon>
|
<h3 class="title">你画的流程图</h3>
|
||||||
你画的流程图
|
|
||||||
</h3>
|
|
||||||
<n-button text type="primary" size="small" @click="myFlowchartZoom = true">
|
<n-button text type="primary" size="small" @click="myFlowchartZoom = true">
|
||||||
放大看 ›
|
放大看
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
<n-button text size="small" @click="myFlowchartOpen = !myFlowchartOpen">
|
||||||
<div class="my-flowchart" role="button" @click="myFlowchartZoom = true">
|
{{ myFlowchartOpen ? "收起" : "展开" }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="myFlowchartOpen"
|
||||||
|
class="my-flowchart"
|
||||||
|
role="button"
|
||||||
|
aria-label="放大看你画的流程图"
|
||||||
|
@click="myFlowchartZoom = true"
|
||||||
|
>
|
||||||
<MyFlowchart compact />
|
<MyFlowchart compact />
|
||||||
</div>
|
</div>
|
||||||
<n-modal
|
<n-modal
|
||||||
@@ -216,47 +273,48 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
例子是一张表:例子 N | 输入 | 输出。原来每个例子各占一个小标题加两个框,三个例子
|
例子是一张表:例子 N | 输入 | 输出。原来每个例子各占一个小标题加两个框,三个例子
|
||||||
就占掉大半屏;Python 的例子大多一两行,摆成表一眼能对上「这个输入 → 这个输出」
|
就占掉大半屏;Python 的例子大多一两行,摆成表一眼能对上「这个输入 → 这个输出」
|
||||||
-->
|
-->
|
||||||
<section v-if="samples.length" class="sample">
|
<section v-if="samples.length" class="block">
|
||||||
<n-flex align="center" justify="space-between" class="sample-head">
|
<div class="block-head">
|
||||||
<h3 class="title">
|
<h3 class="title">例子</h3>
|
||||||
<Icon icon="streamline-emojis:microscope"></Icon>
|
|
||||||
例子
|
|
||||||
</h3>
|
|
||||||
<n-button
|
<n-button
|
||||||
text
|
text
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
|
class="block-link"
|
||||||
@click="submissionStore.revealResult('custom')"
|
@click="submissionStore.revealResult('custom')"
|
||||||
>
|
>
|
||||||
自己输入数据试试 ›
|
自己输入数据试试 ›
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</div>
|
||||||
<div class="sample-table" role="table" aria-label="例子">
|
<div class="sample-table" role="table" aria-label="例子">
|
||||||
<div class="sample-row sample-header" role="row">
|
<span role="columnheader"></span>
|
||||||
<span role="columnheader"></span>
|
<span role="columnheader" class="sample-label">输入</span>
|
||||||
<span role="columnheader">输入</span>
|
<span role="columnheader" class="sample-label">输出</span>
|
||||||
<span role="columnheader">输出</span>
|
<template v-for="(sample, index) of samples" :key="index">
|
||||||
</div>
|
<span class="sample-no" role="rowheader">例子 {{ index + 1 }}</span>
|
||||||
<div v-for="(sample, index) of samples" :key="index" class="sample-row" role="row">
|
<div
|
||||||
<span class="sample-no" role="rowheader">{{ index + 1 }}</span>
|
v-for="side in ['input', 'output'] as const"
|
||||||
<div class="sample-cell" role="cell">
|
:key="side"
|
||||||
<pre class="testcase">{{ sample.input }}</pre>
|
class="sample-cell"
|
||||||
<span class="sample-copy"><Copy :value="sample.input" /></span>
|
role="cell"
|
||||||
|
>
|
||||||
|
<pre class="testcase">{{ sample[side] }}</pre>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sample-copy"
|
||||||
|
:aria-label="`复制例子 ${index + 1} 的${side === 'input' ? '输入' : '输出'}`"
|
||||||
|
@click="copySample(`${index}-${side}`, sample[side])"
|
||||||
|
>
|
||||||
|
{{ copiedKey === `${index}-${side}` ? "已复制" : "复制" }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sample-cell" role="cell">
|
</template>
|
||||||
<pre class="testcase">{{ sample.output }}</pre>
|
|
||||||
<span class="sample-copy"><Copy :value="sample.output" /></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="isSQL && sqlDisplay">
|
<template v-if="isSQL && sqlDisplay">
|
||||||
<h3 class="title">
|
<h3 class="title sql-title">数据表</h3>
|
||||||
<Icon icon="devicon:sqlite"></Icon>
|
|
||||||
数据表
|
|
||||||
</h3>
|
|
||||||
<div v-for="t in sqlDisplay.tables" :key="t.name">
|
<div v-for="t in sqlDisplay.tables" :key="t.name">
|
||||||
<p class="sqlTableName">{{ t.name }}</p>
|
<p class="sqlTableName">{{ t.name }}</p>
|
||||||
<SQLDataTable
|
<SQLDataTable
|
||||||
@@ -267,10 +325,7 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="title">
|
<h3 class="title sql-title">期望结果</h3>
|
||||||
<Icon icon="streamline-ultimate-color:check-button"></Icon>
|
|
||||||
期望结果
|
|
||||||
</h3>
|
|
||||||
<template v-if="sqlExpectedQuery">
|
<template v-if="sqlExpectedQuery">
|
||||||
<SQLDataTable
|
<SQLDataTable
|
||||||
:columns="sqlExpectedQuery.columns"
|
:columns="sqlExpectedQuery.columns"
|
||||||
@@ -294,35 +349,30 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="problem.hint">
|
<section v-if="problem.hint" class="block">
|
||||||
<h3 class="title">
|
<h3 class="title">提示</h3>
|
||||||
<Icon icon="streamline-emojis:man-tipping-hand-1"></Icon>
|
|
||||||
提示
|
|
||||||
</h3>
|
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="preview"
|
preview-theme="preview"
|
||||||
:model-value="problem.hint"
|
:model-value="problem.hint"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div v-if="problem.source">
|
<section v-if="problem.source" class="block">
|
||||||
<h3 class="title">
|
<h3 class="title">来源</h3>
|
||||||
<Icon icon="streamline-ultimate-color:book-open-bookmark"></Icon>
|
|
||||||
来源
|
|
||||||
</h3>
|
|
||||||
<MdPreview
|
<MdPreview
|
||||||
preview-theme="vuepress"
|
preview-theme="vuepress"
|
||||||
:model-value="problem.source"
|
:model-value="problem.source"
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
:theme="isDark ? 'dark' : 'light'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<SimilarProblems v-if="showSimilar" title="相似题目推荐" />
|
<SimilarProblems v-if="showSimilar" title="相似题目推荐" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 设计稿「题目页签:还没运行过」 */
|
||||||
.problem-head {
|
.problem-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -330,26 +380,64 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problemTitle {
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pid {
|
||||||
|
flex: none;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(128, 128, 128, 0.12);
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-meta {
|
.meta {
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 20px 0 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* md-editor 的预览自带一圈 padding 和段落外边距,放在小节标题下面显得很散 */
|
.block {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-head .title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-link {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* md-editor 的预览自带一圈 padding、白底和段落外边距,放在小节标题下面显得很散 */
|
||||||
|
:deep(.md-editor) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.md-editor-preview-wrapper) {
|
:deep(.md-editor-preview-wrapper) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -362,14 +450,23 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.io {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码要求:一行的浅绿框,硬性的判定条件,放在最前面 */
|
||||||
.requirements {
|
.requirements {
|
||||||
padding: 10px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(24, 160, 88, 0.3);
|
border: 1px solid rgba(24, 160, 88, 0.3);
|
||||||
background-color: rgba(24, 160, 88, 0.06);
|
background-color: rgba(24, 160, 88, 0.06);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 8px;
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 8px;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.requirements-head {
|
.requirements-head {
|
||||||
@@ -377,103 +474,55 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
}
|
}
|
||||||
|
|
||||||
.requirements-note {
|
.requirements-note {
|
||||||
font-size: 12px;
|
color: v-bind("theme.textColor3");
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.requirements-lang {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-label {
|
.lang-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sample {
|
/* 参考流程图:一行可以点开的条 */
|
||||||
margin-top: 20px;
|
.fold {
|
||||||
}
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
.sample-head {
|
box-sizing: border-box;
|
||||||
margin-bottom: 8px;
|
padding: 0 12px;
|
||||||
}
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
|
|
||||||
.sample-head .title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-table {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
background-color: rgba(128, 128, 128, 0.04);
|
||||||
}
|
|
||||||
|
|
||||||
.sample-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 2.2em minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-row + .sample-row {
|
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-header {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.7;
|
|
||||||
background-color: rgba(128, 128, 128, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-header > span {
|
|
||||||
padding: 4px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-no {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8px;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold:hover {
|
||||||
|
border-color: v-bind("theme.borderColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-caret {
|
||||||
|
flex: none;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-caret.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fold-note {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.6;
|
color: v-bind("theme.textColor3");
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
background-color: rgba(128, 128, 128, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sample-cell {
|
.fold-body {
|
||||||
position: relative;
|
margin-top: 8px;
|
||||||
min-width: 0;
|
|
||||||
padding: 6px 30px 6px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sample-cell + .sample-cell {
|
|
||||||
border-left: 1px solid rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy 的根是 n-tooltip,class 挂不上去,所以外面包一层 span 来定位 */
|
|
||||||
.sample-copy {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 6px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testcase {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: pre;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: Monaco, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flowchart-section {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-flowchart {
|
.my-flowchart {
|
||||||
@@ -483,6 +532,67 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 例子:例子 N | 输入 | 输出,每格右边一个「复制」 */
|
||||||
|
.sample-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 56px minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 6px 8px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-label,
|
||||||
|
.sample-no {
|
||||||
|
font-size: 12px;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-no {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-cell {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 2px 4px 2px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(128, 128, 128, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testcase {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-copy {
|
||||||
|
flex: none;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
color: v-bind("theme.primaryColorPressed");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sample-copy:hover {
|
||||||
|
background-color: rgba(24, 160, 88, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-title {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.sqlTableName {
|
.sqlTableName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 8px 0 4px;
|
margin: 8px 0 4px;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
@@ -11,7 +12,8 @@ import { useTeacherCollab } from "../composables/teacherCollab"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计 / 点评 / 我的提交。原来是左栏的三个页签,课上几乎没人点,却和「题目」挤在一排;
|
* 统计 / 点评 / 我的提交。原来是左栏的三个页签,课上几乎没人点,却和「题目」挤在一排;
|
||||||
* 现在收进抽屉,盖住左栏、编辑器不动。
|
* 现在收进抽屉:盖住整个左栏(连上下文条、页签行),编辑器不动;抽屉顶上有自己的页签,
|
||||||
|
* 三样之间直接切(设计稿「我的提交 / 统计 / 点评:抽屉盖住左栏」)。
|
||||||
*
|
*
|
||||||
* 抽屉里的内容关上就卸载:再打开时「我的提交」要重新拉,刚交的那次才看得见。
|
* 抽屉里的内容关上就卸载:再打开时「我的提交」要重新拉,刚交的那次才看得见。
|
||||||
*/
|
*/
|
||||||
@@ -23,9 +25,21 @@ const open = defineModel<ProblemDrawer | null>({ required: true })
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 桌面:挂进左栏(它得是 position: relative);手机:不传,从底下拉起来盖住整屏 */
|
/** 桌面:挂进左栏(它得是 position: relative);手机:不传,从底下拉起来盖住整屏 */
|
||||||
to?: string
|
to?: HTMLElement | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
/** 抽屉顶上的页签,顺序照设计稿:最常看的「我的提交」在前 */
|
||||||
|
const tabs = computed(() =>
|
||||||
|
(["submission", "info", "reaction"] as const).filter((key) => ctx.value.drawers.includes(key)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
|
// trap-focus 关着(编辑器还要能点),焦点不在抽屉里时 Naive 自己的 Esc 不生效,这里补一个
|
||||||
|
useEventListener(document, "keydown", (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape" && open.value) open.value = null
|
||||||
|
})
|
||||||
|
|
||||||
const ctx = useProblemPageContext()
|
const ctx = useProblemPageContext()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -54,13 +68,8 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查)
|
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查),页签写「他的提交」
|
||||||
const teacherCollab = useTeacherCollab()
|
const teacherCollab = useTeacherCollab()
|
||||||
const drawerTitle = computed(() =>
|
|
||||||
lastOpened.value === "submission" && teacherCollab.value
|
|
||||||
? "他的提交"
|
|
||||||
: DRAWER_TITLE[lastOpened.value],
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「看这道题所有人的提交」,原来是工具栏上的「本题提交」。可见条件照旧:
|
* 「看这道题所有人的提交」,原来是工具栏上的「本题提交」。可见条件照旧:
|
||||||
@@ -90,34 +99,118 @@ function goAllSubmissions() {
|
|||||||
<template>
|
<template>
|
||||||
<n-drawer
|
<n-drawer
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
:to="props.to"
|
:to="props.to ?? undefined"
|
||||||
:placement="props.to ? 'left' : 'bottom'"
|
:placement="props.to ? 'left' : 'bottom'"
|
||||||
:width="props.to ? '100%' : undefined"
|
:width="props.to ? '100%' : undefined"
|
||||||
:height="props.to ? undefined : '85%'"
|
:height="props.to ? undefined : '85%'"
|
||||||
:trap-focus="false"
|
:trap-focus="false"
|
||||||
:block-scroll="!props.to"
|
:block-scroll="!props.to"
|
||||||
>
|
>
|
||||||
<n-drawer-content :title="drawerTitle" closable :native-scrollbar="false">
|
<n-drawer-content
|
||||||
|
:native-scrollbar="false"
|
||||||
|
:header-style="{ padding: 0 }"
|
||||||
|
:body-content-style="{ padding: '16px 20px' }"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="drawer-head">
|
||||||
|
<div class="drawer-tabs" role="tablist">
|
||||||
|
<button
|
||||||
|
v-for="key in tabs"
|
||||||
|
:key="key"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="drawer-tab"
|
||||||
|
:class="{ active: lastOpened === key }"
|
||||||
|
:aria-selected="lastOpened === key"
|
||||||
|
@click="open = key"
|
||||||
|
>
|
||||||
|
{{ key === "submission" && teacherCollab ? "他的提交" : DRAWER_TITLE[key] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="drawer-close" aria-label="关闭" @click="open = null">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<path d="M6 6l12 12M18 6L6 18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<ProblemInfo v-if="lastOpened === 'info'" />
|
<ProblemInfo v-if="lastOpened === 'info'" />
|
||||||
<ProblemReaction v-else-if="lastOpened === 'reaction'" />
|
<ProblemReaction v-else-if="lastOpened === 'reaction'" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ProblemSubmission />
|
<ProblemSubmission />
|
||||||
<n-button
|
<div v-if="showAllSubmissions" class="all-submissions">
|
||||||
v-if="showAllSubmissions"
|
<n-button text type="primary" @click="goAllSubmissions">看这道题所有人的提交 ›</n-button>
|
||||||
class="all-submissions"
|
</div>
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
@click="goAllSubmissions"
|
|
||||||
>
|
|
||||||
看这道题所有人的提交 ›
|
|
||||||
</n-button>
|
|
||||||
</template>
|
</template>
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.drawer-head {
|
||||||
|
height: 48px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 12px 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-tabs {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-tab {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-tab.active {
|
||||||
|
border-bottom-color: v-bind("theme.primaryColor");
|
||||||
|
color: v-bind("theme.textColor1");
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-close {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-close:hover {
|
||||||
|
background-color: v-bind("theme.hoverColor");
|
||||||
|
}
|
||||||
|
|
||||||
.all-submissions {
|
.all-submissions {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -91,9 +91,11 @@ const contestID = route.params.contestID || null
|
|||||||
// 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法
|
// 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法
|
||||||
const storageKey = useDraftKey()
|
const storageKey = useDraftKey()
|
||||||
|
|
||||||
|
// 桌面:顶栏 60 + 工具栏 48,协作条再占 40(两栏顶满内容区,见 detail.vue 的 problem-split)。
|
||||||
|
// 手机:顶栏两行 + 课堂条 + 页签 + 工具栏约 236px,底部还有「运行例子 / 提交」那条约 64px
|
||||||
const editorHeight = computed(() => {
|
const editorHeight = computed(() => {
|
||||||
const base = isDesktop.value ? 133 : 300
|
const base = isDesktop.value ? 108 : 300
|
||||||
return `calc(100vh - ${base + (teacherCollab.value ? 52 : 0)}px)`
|
return `calc(100vh - ${base + (teacherCollab.value ? 40 : 0)}px)`
|
||||||
})
|
})
|
||||||
|
|
||||||
function loadCode() {
|
function loadCode() {
|
||||||
@@ -160,7 +162,7 @@ provide("flowchartEditorRef", flowchartEditorRef)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-flex vertical>
|
<div class="problem-editor">
|
||||||
<CollabBar v-if="teacherCollab" />
|
<CollabBar v-if="teacherCollab" />
|
||||||
<EditorToolbar @change-language="changeLanguage" />
|
<EditorToolbar @change-language="changeLanguage" />
|
||||||
<!--
|
<!--
|
||||||
@@ -180,5 +182,5 @@ provide("flowchartEditorRef", flowchartEditorRef)
|
|||||||
:extra-extensions="editorExtensions"
|
:extra-extensions="editorExtensions"
|
||||||
@update:model-value="changeCode"
|
@update:model-value="changeCode"
|
||||||
/>
|
/>
|
||||||
</n-flex>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
import { getRankOfProblem, getSubmission, getSubmissions } from "oj/api"
|
import { getRankOfProblem, getSubmission, getSubmissions } from "oj/api"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
|
||||||
import { useCollabStore } from "shared/store/collab"
|
import { useCollabStore } from "shared/store/collab"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||||
import { copyToClipboard, parseTime } from "utils/functions"
|
import { copyToClipboard, parseTime } from "utils/functions"
|
||||||
import type { ProblemRank } from "@oj2/contract"
|
import type { ProblemRank } from "@oj2/contract"
|
||||||
import type { LANGUAGE, SubmissionListItem } from "utils/types"
|
import type { LANGUAGE, SubmissionListItem } from "utils/types"
|
||||||
@@ -29,6 +29,7 @@ const ctx = useProblemPageContext()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
const problemDisplayId = computed(() => String(route.params.problemID ?? ""))
|
const problemDisplayId = computed(() => String(route.params.problemID ?? ""))
|
||||||
|
|
||||||
@@ -136,6 +137,23 @@ async function toggle(row: SubmissionListItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 这一页里被题单闸门藏起来的次数 */
|
||||||
|
const lockedCount = computed(() => submissions.value.filter((row) => !row.showLink).length)
|
||||||
|
|
||||||
|
/** 展开的代码先只露 5 行,「一共 11 行」点一下再全摆出来(设计稿) */
|
||||||
|
const PREVIEW_LINES = 5
|
||||||
|
const fullCode = reactive<Record<string, boolean>>({})
|
||||||
|
function preview(id: string) {
|
||||||
|
const code = loadedOf(id)?.code ?? ""
|
||||||
|
const lines = code.replace(/\s+$/, "").split("\n")
|
||||||
|
const more = !fullCode[id] && lines.length > PREVIEW_LINES + 1
|
||||||
|
return {
|
||||||
|
text: more ? `${lines.slice(0, PREVIEW_LINES).join("\n")}\n` : code,
|
||||||
|
more,
|
||||||
|
total: lines.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadedOf(id: string): Loaded | null {
|
function loadedOf(id: string): Loaded | null {
|
||||||
const value = details[id]
|
const value = details[id]
|
||||||
return value && typeof value === "object" ? value : null
|
return value && typeof value === "object" ? value : null
|
||||||
@@ -208,63 +226,102 @@ watch(query, () => {
|
|||||||
:description="peer ? '他还没有交过这道题' : '这道题你还没有交过'"
|
:description="peer ? '他还没有交过这道题' : '这道题你还没有交过'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul v-else class="list">
|
<!-- 设计稿「我的提交 / 统计 / 点评:抽屉盖住左栏」:一整块带框的列表,行间一条细线 -->
|
||||||
<li v-for="(row, index) in submissions" :key="row.id" class="item">
|
<div v-else-if="submissions.length" class="list">
|
||||||
<button
|
<template v-for="(row, index) in submissions" :key="row.id">
|
||||||
type="button"
|
<div v-if="row.showLink" class="item" :class="{ open: expanded === row.id }">
|
||||||
class="row"
|
<button
|
||||||
:class="{ open: expanded === row.id, locked: !row.showLink }"
|
type="button"
|
||||||
:disabled="!row.showLink"
|
class="row"
|
||||||
:aria-expanded="row.showLink ? expanded === row.id : undefined"
|
:aria-expanded="expanded === row.id"
|
||||||
@click="toggle(row)"
|
@click="toggle(row)"
|
||||||
>
|
>
|
||||||
<span class="attempt">第 {{ attemptNo(index) }} 次</span>
|
<span class="attempt">第 {{ attemptNo(index) }} 次</span>
|
||||||
<SubmissionResultTag :result="row.result" />
|
<span class="verdict" :class="JUDGE_STATUS[row.result]?.type">
|
||||||
<span class="meta">{{ LANGUAGE_SHOW_VALUE[row.language] }}</span>
|
{{ JUDGE_STATUS[row.result]?.name ?? "未知" }}
|
||||||
<span class="meta time">{{ parseTime(row.createTime, "MM-DD HH:mm") }}</span>
|
</span>
|
||||||
<span v-if="row.showLink" class="caret" aria-hidden="true">
|
<span class="meta">
|
||||||
{{ expanded === row.id ? "▾" : "▸" }}
|
{{ LANGUAGE_SHOW_VALUE[row.language] }} ·
|
||||||
</span>
|
{{ parseTime(row.createTime, "MM-DD HH:mm") }}
|
||||||
</button>
|
</span>
|
||||||
|
<svg
|
||||||
|
class="caret"
|
||||||
|
:class="{ up: expanded === row.id }"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<p v-if="!row.showLink" class="locked-note">
|
<div v-if="expanded === row.id" class="detail">
|
||||||
🔒 这是加入题单之前交的,先藏起来了:在题单里做出这道题就解锁,题单过了截止时间也会解锁
|
<n-flex v-if="details[row.id] === 'loading'" justify="center" class="pad">
|
||||||
</p>
|
<n-spin size="small" />
|
||||||
|
|
||||||
<div v-else-if="expanded === row.id" class="detail">
|
|
||||||
<n-flex v-if="details[row.id] === 'loading'" justify="center" class="pad">
|
|
||||||
<n-spin size="small" />
|
|
||||||
</n-flex>
|
|
||||||
<n-text v-else-if="details[row.id] === 'failed'" type="error">
|
|
||||||
代码读不出来,收起来再点开试试
|
|
||||||
</n-text>
|
|
||||||
<template v-else-if="loadedOf(row.id)">
|
|
||||||
<pre class="code">{{ loadedOf(row.id)!.code }}</pre>
|
|
||||||
<n-flex :size="8" align="center">
|
|
||||||
<n-button
|
|
||||||
v-if="!teacherCollab"
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
secondary
|
|
||||||
:disabled="!canPutBack(row.id)"
|
|
||||||
:title="
|
|
||||||
canPutBack(row.id)
|
|
||||||
? undefined
|
|
||||||
: `这道题现在不收 ${LANGUAGE_SHOW_VALUE[loadedOf(row.id)!.language]} 了,只能复制`
|
|
||||||
"
|
|
||||||
@click="putBack(row.id)"
|
|
||||||
>
|
|
||||||
放回编辑器
|
|
||||||
</n-button>
|
|
||||||
<n-button size="small" @click="copy(row.id)">复制</n-button>
|
|
||||||
<n-button size="small" text class="more" @click="openDetail(row.id)">
|
|
||||||
完整详情 ›
|
|
||||||
</n-button>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</template>
|
<n-text v-else-if="details[row.id] === 'failed'" type="error">
|
||||||
|
代码读不出来,收起来再点开试试
|
||||||
|
</n-text>
|
||||||
|
<template v-else-if="loadedOf(row.id)">
|
||||||
|
<pre class="code">{{ preview(row.id).text
|
||||||
|
}}<button
|
||||||
|
v-if="preview(row.id).more"
|
||||||
|
type="button"
|
||||||
|
class="code-more"
|
||||||
|
@click="fullCode[row.id] = true"
|
||||||
|
>…… 一共 {{ preview(row.id).total }} 行,点开看全部</button></pre>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<n-button
|
||||||
|
v-if="!teacherCollab"
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
:disabled="!canPutBack(row.id)"
|
||||||
|
:title="
|
||||||
|
canPutBack(row.id)
|
||||||
|
? undefined
|
||||||
|
: `这道题现在不收 ${LANGUAGE_SHOW_VALUE[loadedOf(row.id)!.language]} 了,只能复制`
|
||||||
|
"
|
||||||
|
@click="putBack(row.id)"
|
||||||
|
>
|
||||||
|
放回编辑器
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" @click="copy(row.id)">复制</n-button>
|
||||||
|
<n-button size="small" text class="more" @click="openDetail(row.id)">
|
||||||
|
完整详情 ›
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</template>
|
||||||
</ul>
|
<!-- 加入题单之前交的那几次:合成一行,点不开(后端也不给代码) -->
|
||||||
|
<div v-if="lockedCount" class="locked">
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<rect x="5" y="11" width="14" height="10" rx="2" />
|
||||||
|
<path d="M8 11V8a4 4 0 018 0v3" />
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
加入题单之前的
|
||||||
|
{{ lockedCount }} 次提交先藏起来了,在题单里做出这道题就能看(题单过了截止时间也会解锁)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="total > query.limit"
|
v-if="total > query.limit"
|
||||||
@@ -297,78 +354,86 @@ watch(query, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
list-style: none;
|
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
padding: 0;
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item + .item,
|
||||||
|
.item + .locked {
|
||||||
|
border-top: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
padding: 8px 12px;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 14px;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row:hover:not(:disabled),
|
.row:hover,
|
||||||
.row.open {
|
.item.open .row {
|
||||||
background-color: rgba(128, 128, 128, 0.08);
|
background-color: rgba(128, 128, 128, 0.06);
|
||||||
}
|
|
||||||
|
|
||||||
.row.locked {
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attempt {
|
.attempt {
|
||||||
flex: none;
|
flex: none;
|
||||||
min-width: 4.5em;
|
width: 4em;
|
||||||
font-weight: 600;
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.verdict {
|
||||||
flex: none;
|
flex: none;
|
||||||
opacity: 0.7;
|
min-width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.verdict.success {
|
||||||
margin-left: auto;
|
color: v-bind("theme.successColorPressed");
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verdict.error {
|
||||||
|
color: v-bind("theme.errorColorPressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
.verdict.warning {
|
||||||
|
color: v-bind("theme.warningColorPressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.caret {
|
.caret {
|
||||||
|
margin-left: auto;
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 1em;
|
color: v-bind("theme.textColor3");
|
||||||
opacity: 0.6;
|
transition: transform 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.locked-note {
|
.caret.up {
|
||||||
margin: 0;
|
transform: rotate(180deg);
|
||||||
padding: 0 12px 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail {
|
.detail {
|
||||||
padding: 4px 12px 12px;
|
padding: 10px 14px;
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pad {
|
.pad {
|
||||||
@@ -376,18 +441,48 @@ watch(query, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
margin: 8px 0 10px;
|
margin: 0;
|
||||||
padding: 10px 12px;
|
padding: 8px 10px;
|
||||||
max-height: 320px;
|
max-height: 360px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
background-color: rgba(128, 128, 128, 0.08);
|
background-color: rgba(128, 128, 128, 0.06);
|
||||||
font-family: Monaco, Consolas, monospace;
|
font-family: Consolas, Monaco, monospace;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 20px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-more {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-more:hover {
|
||||||
|
color: v-bind("theme.primaryColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked {
|
||||||
|
height: 44px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果页签里每一块结果的标题:圆形状态图标 + 大字 + 灰色小字,下面可以挂一条测试点进度。
|
||||||
|
* 设计稿「答案错误」「答案正确」「运行例子」「语法没过」几张画板都是这一个样子:
|
||||||
|
* 不套整块的彩色提示框,颜色只落在图标和大字上。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
kind: "success" | "error" | "warning" | "info" | "pending"
|
||||||
|
title: string
|
||||||
|
sub?: string
|
||||||
|
/** 测试点进度:通过几个、一共几个 */
|
||||||
|
progress?: { passed: number; total: number } | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
|
const color = computed(() => {
|
||||||
|
switch (props.kind) {
|
||||||
|
case "success":
|
||||||
|
return { icon: theme.value.successColor, text: theme.value.successColorPressed }
|
||||||
|
case "error":
|
||||||
|
return { icon: theme.value.errorColor, text: theme.value.errorColorPressed }
|
||||||
|
case "warning":
|
||||||
|
return { icon: theme.value.warningColor, text: theme.value.warningColorPressed }
|
||||||
|
default:
|
||||||
|
return { icon: theme.value.infoColor, text: theme.value.textColor1 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="result-header">
|
||||||
|
<div class="line">
|
||||||
|
<span class="badge" :class="kind" aria-hidden="true">
|
||||||
|
<svg
|
||||||
|
v-if="kind === 'success'"
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12l5 5 9-10" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="kind === 'error'"
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<path d="M6 6l12 12M18 6L6 18" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else-if="kind === 'pending'"
|
||||||
|
class="spin"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
>
|
||||||
|
<path d="M12 6v7" />
|
||||||
|
<path d="M12 18h.01" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="title">{{ title }}</span>
|
||||||
|
<span v-if="sub" class="sub">{{ sub }}</span>
|
||||||
|
<slot name="extra" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="progress && progress.total > 0"
|
||||||
|
class="progress"
|
||||||
|
role="img"
|
||||||
|
:aria-label="`通过 ${progress.passed}/${progress.total} 个测试点`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: `${(progress.passed / progress.total) * 100}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
flex: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
background-color: v-bind("color.icon");
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: v-bind("color.text");
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgba(208, 48, 80, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: v-bind("theme.successColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,9 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import { parseTime } from "utils/functions"
|
||||||
|
import ResultHeader from "./ResultHeader.vue"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 左栏「结果」页签:三段 —— 提交结果 / 运行例子 / 自己输入。
|
* 左栏「结果」页签:三段 —— 提交结果 / 运行例子 / 自己输入。
|
||||||
@@ -31,6 +34,28 @@ const codeStore = useCodeStore()
|
|||||||
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
||||||
const teacherCollab = useTeacherCollab()
|
const teacherCollab = useTeacherCollab()
|
||||||
|
|
||||||
|
const theme = useThemeVars()
|
||||||
|
const { samplesRunAt } = submissionStore.trial
|
||||||
|
|
||||||
|
type Segment = "submit" | "samples" | "custom"
|
||||||
|
const segments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
||||||
|
{
|
||||||
|
value: "submit",
|
||||||
|
// 语法没过就没交上去:那一段不叫「提交结果」
|
||||||
|
label: syntaxErrorInfo.value ? "交之前检查" : "提交结果",
|
||||||
|
time:
|
||||||
|
!syntaxErrorInfo.value && submission.value?.createTime
|
||||||
|
? parseTime(submission.value.createTime, "HH:mm")
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "samples",
|
||||||
|
label: "运行例子",
|
||||||
|
time: samplesRunAt.value ? parseTime(samplesRunAt.value, "HH:mm") : "",
|
||||||
|
},
|
||||||
|
{ value: "custom", label: "自己输入", time: "" },
|
||||||
|
])
|
||||||
|
|
||||||
/** 正在画流程图:结果就是 AI 的点评,没有例子可跑 */
|
/** 正在画流程图:结果就是 AI 的点评,没有例子可跑 */
|
||||||
const drawing = computed(() => codeStore.code.language === "Flowchart")
|
const drawing = computed(() => codeStore.code.language === "Flowchart")
|
||||||
|
|
||||||
@@ -44,21 +69,28 @@ const canTrial = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<FlowchartResult v-if="drawing" />
|
<FlowchartResult v-if="drawing" />
|
||||||
<div v-show="!drawing" class="result-pane">
|
<div v-show="!drawing" class="result-pane">
|
||||||
<n-radio-group v-if="canTrial" v-model:value="resultSegment" size="small" class="segments">
|
<!-- 设计稿:三个胶囊,选中的深色实心,带上那次结果的时间 -->
|
||||||
<!-- 语法没过就没交上去:那一段不叫「提交结果」 -->
|
<div v-if="canTrial" class="segments" role="tablist" aria-label="结果">
|
||||||
<n-radio-button value="submit">{{
|
<button
|
||||||
syntaxErrorInfo ? "交之前检查" : "提交结果"
|
v-for="segment in segments"
|
||||||
}}</n-radio-button>
|
:key="segment.value"
|
||||||
<n-radio-button value="samples">运行例子</n-radio-button>
|
type="button"
|
||||||
<n-radio-button value="custom">自己输入</n-radio-button>
|
role="tab"
|
||||||
</n-radio-group>
|
class="segment"
|
||||||
|
:class="{ active: resultSegment === segment.value }"
|
||||||
|
:aria-selected="resultSegment === segment.value"
|
||||||
|
@click="resultSegment = segment.value"
|
||||||
|
>
|
||||||
|
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
||||||
<div v-show="resultSegment === 'submit' || !canTrial">
|
<div v-show="resultSegment === 'submit' || !canTrial">
|
||||||
<n-flex v-if="syntaxErrorInfo" vertical>
|
<template v-if="syntaxErrorInfo">
|
||||||
<n-alert type="warning" title="代码有语法错误,还没有交上去" />
|
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
||||||
<PythonErrorExplain :err-info="syntaxErrorInfo" />
|
<PythonErrorExplain :err-info="syntaxErrorInfo" />
|
||||||
</n-flex>
|
</template>
|
||||||
<template v-else-if="submission">
|
<template v-else-if="submission">
|
||||||
<p v-if="formattedBeforeSubmit" class="formatted">
|
<p v-if="formattedBeforeSubmit" class="formatted">
|
||||||
提交之前自动整理了代码格式(缩进、空格),编辑器里的代码也跟着变了,按 Ctrl+Z 能撤回
|
提交之前自动整理了代码格式(缩进、空格),编辑器里的代码也跟着变了,按 Ctrl+Z 能撤回
|
||||||
@@ -69,7 +101,7 @@ const canTrial = computed(() => {
|
|||||||
<n-empty
|
<n-empty
|
||||||
v-else
|
v-else
|
||||||
class="empty"
|
class="empty"
|
||||||
description="还没有提交过。写完代码按「提交代码」,结果会出现在这里"
|
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="canTrial">
|
<template v-if="canTrial">
|
||||||
@@ -81,13 +113,33 @@ const canTrial = computed(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.result-pane {
|
.result-pane {
|
||||||
padding: 4px 0 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segments {
|
.segments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.segment {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: rgba(128, 128, 128, 0.12);
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment.active {
|
||||||
|
background-color: v-bind("theme.textColor1");
|
||||||
|
color: v-bind("theme.cardColor");
|
||||||
|
}
|
||||||
|
|
||||||
.formatted {
|
.formatted {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
submissionCaseResults,
|
submissionCaseResults,
|
||||||
submissionMemoryFormat,
|
submissionMemoryFormat,
|
||||||
submissionPartialCases,
|
submissionPartialCases,
|
||||||
submissionResultTitle,
|
|
||||||
submissionTimeFormat,
|
submissionTimeFormat,
|
||||||
} from "utils/functions"
|
} from "utils/functions"
|
||||||
import type { Submission } from "utils/types"
|
import type { Submission } from "utils/types"
|
||||||
@@ -19,6 +18,7 @@ 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 LessonNext from "./LessonNext.vue"
|
||||||
|
import ResultHeader from "./ResultHeader.vue"
|
||||||
import SimilarProblems from "./SimilarProblems.vue"
|
import SimilarProblems from "./SimilarProblems.vue"
|
||||||
import { useLessonStore } from "oj/store/lesson"
|
import { useLessonStore } from "oj/store/lesson"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||||
@@ -151,34 +151,70 @@ const failedOnSample = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结果标题(设计文档第 6 节)。比「答案错误 · 通过 1/3 个测试点」多走一步:
|
* 结果标题(设计文档第 6 节、设计稿「答案错误 / 答案正确 / 错在隐藏测试点」):
|
||||||
* 例子上就错了的说「在例子 N 上就错了」,例子都对的才报测试点;做对了说全对了。
|
* 大字是判题结果,灰色小字说清楚错在哪 —— 例子上就错了的说「在例子 N 上就错了」、不挂进度;
|
||||||
|
* 其余照旧报「通过 x/y 个测试点」(一个都没过就不报);做对了说全对了。
|
||||||
* 提交详情页还用通用的 submissionResultTitle
|
* 提交详情页还用通用的 submissionResultTitle
|
||||||
*/
|
*/
|
||||||
const title = computed(() => {
|
const header = computed(() => {
|
||||||
const submission = props.submission
|
const submission = props.submission
|
||||||
if (!submission) return ""
|
if (!submission) return null
|
||||||
if (submission.result === SubmissionStatus.accepted) return "答案正确 · 所有测试点都对了"
|
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) {
|
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」—— 那正说明例子之外的全错了。
|
* 例子都对了、错在隐藏测试点:「去『自己输入』拿 90、80 这些数试试」。数取题目描述里出现的、
|
||||||
* 其余情况沿用 submissionPartialCases(一个都没过就不报)
|
* 例子里没有的头两个 —— 分段、判断一类题的边界多半就写在描述里(设计稿「错在隐藏测试点」)。
|
||||||
|
* SQL 题、比赛里没有「自己输入」
|
||||||
*/
|
*/
|
||||||
const hiddenCases = computed(() => {
|
const submissionStore = useSubmissionStore()
|
||||||
const summary = props.submission?.caseSummary
|
const customRunHint = computed(() => {
|
||||||
if (!sampleCheck.value?.passed || !summary || summary.passed >= summary.total) return null
|
if (!sampleCheck.value?.passed || props.peer) return ""
|
||||||
return summary
|
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提示区域。
|
// 是否显示AI提示区域。
|
||||||
// 阈值和后端 POST /ai/hint 共用契约里的 HINT_MIN_FAILURES,别在这里写死数字;
|
// 阈值和后端 POST /ai/hint 共用契约里的 HINT_MIN_FAILURES,别在这里写死数字;
|
||||||
@@ -249,16 +285,13 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="submission">
|
<div v-if="submission">
|
||||||
<n-alert :type="JUDGE_STATUS[submission.result]['type']" :title="title" class="mb-3">
|
<ResultHeader
|
||||||
<template v-if="progressCases" #default>
|
v-if="header"
|
||||||
<n-progress
|
:kind="header.kind"
|
||||||
type="line"
|
:title="header.title"
|
||||||
status="success"
|
:sub="header.sub"
|
||||||
:percentage="(progressCases.passed / progressCases.total) * 100"
|
:progress="header.progress"
|
||||||
:show-indicator="false"
|
/>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</n-alert>
|
|
||||||
<LessonNext
|
<LessonNext
|
||||||
v-if="showLessonNext && problemStore.problem"
|
v-if="showLessonNext && problemStore.problem"
|
||||||
:problem-display-id="problemStore.problem._id"
|
: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-data-table v-if="infoTable.length" striped :data="infoTable" :columns="columns" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<!-- AI 提示区域 -->
|
<!--
|
||||||
<template v-if="showAIHint">
|
设计稿:还没问过 AI 时只是一个描边按钮;错在隐藏测试点时旁边给「去自己输入试试」。
|
||||||
<n-card size="small" style="margin-top: 12px; max-width: 480px">
|
问过之后,AI 的回答才放进卡片
|
||||||
<n-alert v-if="hintError" type="error" :title="hintError" class="mb-3" />
|
-->
|
||||||
<n-button
|
<div v-if="(showAIHint && !hintTarget) || customRunHint" class="actions">
|
||||||
v-if="!hintTarget && !hintLoading"
|
<n-button
|
||||||
type="primary"
|
v-if="showAIHint && !hintTarget"
|
||||||
@click="fetchHint(submission.id)"
|
type="primary"
|
||||||
>
|
ghost
|
||||||
让 AI 分析我的代码
|
:loading="hintLoading"
|
||||||
</n-button>
|
@click="fetchHint(submission.id)"
|
||||||
<n-spin v-else-if="hintLoading && !hintTarget" size="small" />
|
>
|
||||||
|
让 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
|
<MdPreview
|
||||||
v-if="hintContent"
|
v-if="hintContent"
|
||||||
:model-value="hintContent"
|
:model-value="hintContent"
|
||||||
@@ -379,6 +427,23 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.msg {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|||||||
@@ -52,14 +52,17 @@ function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 刚交对:降成绿色描边,让结果页签里的「下一题」是唯一的主按钮(设计稿「答案正确」) -->
|
||||||
<n-button
|
<n-button
|
||||||
:size="props.size ?? (isDesktop ? 'medium' : 'small')"
|
:size="props.size ?? (isDesktop ? 'medium' : 'small')"
|
||||||
:block="props.block"
|
:block="props.block"
|
||||||
:type="justAccepted ? 'default' : 'primary'"
|
type="primary"
|
||||||
|
:ghost="justAccepted"
|
||||||
:disabled="buttonState.disabled"
|
:disabled="buttonState.disabled"
|
||||||
|
class="submit"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template v-if="buttonState.icon" #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<Icon :icon="buttonState.icon" />
|
<Icon :icon="buttonState.icon" />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
@@ -67,3 +70,10 @@ function submit() {
|
|||||||
{{ buttonState.label }}
|
{{ buttonState.label }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.submit {
|
||||||
|
min-width: 72px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useDark } from "@vueuse/core"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { MdPreview } from "md-editor-v3"
|
|
||||||
import "md-editor-v3/lib/preview.css"
|
|
||||||
import { useProblemStore } from "oj/store/problem"
|
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 { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
|
||||||
import {
|
import {
|
||||||
classifyDiff,
|
classifyDiff,
|
||||||
@@ -34,16 +32,21 @@ const props = defineProps<{
|
|||||||
full?: boolean
|
full?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isDark = useDark()
|
const theme = useThemeVars()
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const submissionStore = useSubmissionStore()
|
|
||||||
|
|
||||||
/** 题目要求的输出(题面「输出」那一节的原文,Markdown)。SQL 题没有这一节 */
|
/**
|
||||||
const requirement = computed(() =>
|
* 卡片末尾那行「题目原话」(设计稿「答案错误」「错在隐藏测试点」):
|
||||||
props.full && !problem.value?.sqlConfig ? (problem.value?.outputDescription ?? "").trim() : "",
|
* 例子上就错了 —— 摆题面「输出」那一节,格式错、漏了一行这类,对着原话一看就明白;
|
||||||
)
|
* 例子都对了、错在隐藏测试点 —— 摆「描述」,边界条件多半写在那里。SQL 题两节都不摆
|
||||||
/** 自己输入走 Judge0,SQL 跑不了 */
|
*/
|
||||||
const canCustomRun = computed(() => props.full && !problem.value?.sqlConfig)
|
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 字,到了上限就是被截过 */
|
/** 后端每段截到 2000 字,到了上限就是被截过 */
|
||||||
const TEXT_LIMIT = 2000
|
const TEXT_LIMIT = 2000
|
||||||
@@ -64,6 +67,40 @@ const diffLine = computed(() =>
|
|||||||
firstDifferentLine(props.check.expected ?? "", props.check.output ?? ""),
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card embedded class="explain-card">
|
<div class="explain-card">
|
||||||
<n-flex v-if="check.passed" vertical :size="10">
|
<p v-if="check.passed" class="explain">
|
||||||
<div class="explain">
|
题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。
|
||||||
题目里的例子都对了,没通过的是隐藏的测试点。想想特殊情况:最大的数、最小的数、0、负数,或者题目里专门提到的情况。
|
</p>
|
||||||
</div>
|
<template v-else>
|
||||||
<n-button
|
<p class="explain">
|
||||||
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">
|
|
||||||
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? "。" : ":" }}</b
|
<b>例子 {{ (check.index ?? 0) + 1 }} 没有通过{{ plain ? "。" : ":" }}</b
|
||||||
><template v-if="!plain">{{ hint }}</template>
|
><template v-if="!plain">{{ hint }}</template>
|
||||||
</div>
|
</p>
|
||||||
<div class="blocks">
|
<div class="blocks">
|
||||||
<div class="block input">
|
<div class="block">
|
||||||
<div class="label">输入</div>
|
<div class="label">输入</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<div
|
<div
|
||||||
@@ -118,111 +144,84 @@ function lines(text: string) {
|
|||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="label">正确输出</div>
|
<div class="label">正确输出</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<div
|
<template v-for="(line, i) in lines(check.expected ?? '')" :key="i">
|
||||||
v-for="(line, i) in lines(check.expected ?? '')"
|
<div v-if="i === diffLine && diffParts?.expected" class="line">
|
||||||
:key="i"
|
<span v-text="diffParts.expected.before" /><span
|
||||||
class="line"
|
class="diff"
|
||||||
:class="{ diff: onSample && i === diffLine }"
|
v-text="diffParts.expected.marked"
|
||||||
v-text="line"
|
/><span v-text="diffParts.expected.after" />
|
||||||
/>
|
</div>
|
||||||
|
<div v-else class="line" v-text="line" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="label">你的输出</div>
|
<div class="label">你的输出</div>
|
||||||
<div v-if="check.output" class="text">
|
<div v-if="check.output" class="text">
|
||||||
<div
|
<template v-for="(line, i) in lines(check.output)" :key="i">
|
||||||
v-for="(line, i) in lines(check.output)"
|
<div v-if="i === diffLine && diffParts?.output" class="line">
|
||||||
:key="i"
|
<span v-text="diffParts.output.before" /><span
|
||||||
class="line"
|
class="diff"
|
||||||
:class="{ diff: onSample && i === diffLine }"
|
v-text="diffParts.output.marked"
|
||||||
v-text="line"
|
/><span v-text="diffParts.output.after" />
|
||||||
/>
|
</div>
|
||||||
|
<div v-else class="line" v-text="line" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text empty">(什么都没有输出)</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>
|
||||||
</div>
|
</div>
|
||||||
</n-flex>
|
</template>
|
||||||
<div v-if="requirement" class="requirement">
|
<p v-if="requirement" class="requirement">{{ requirement.label }}:{{ requirement.text }}</p>
|
||||||
<div class="label">题目要求的输出</div>
|
</div>
|
||||||
<MdPreview
|
|
||||||
preview-theme="vuepress"
|
|
||||||
:model-value="requirement"
|
|
||||||
:theme="isDark ? 'dark' : 'light'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 设计稿「答案错误」:一块浅灰卡片,三栏并排的白底小框 */
|
||||||
.explain-card {
|
.explain-card {
|
||||||
max-width: 560px;
|
padding: 12px 14px;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
background-color: rgba(128, 128, 128, 0.06);
|
||||||
.custom {
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
align-self: flex-start;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.explain {
|
.explain {
|
||||||
font-size: 16px;
|
margin: 0;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocks {
|
.blocks {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
padding: 6px 10px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
.block.input {
|
background-color: v-bind("theme.cardColor");
|
||||||
grid-column: 1 / -1;
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
opacity: 0.7;
|
color: v-bind("theme.textColor3");
|
||||||
margin-bottom: 4px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin: 0;
|
font-family: Consolas, Monaco, monospace;
|
||||||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: rgba(128, 128, 128, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 每行用 v-text 填,不写 {{ line }}:模板里标签两边的换行缩进会被 Vue 压成一个空格,
|
/* 每行用 v-text 填,不写 {{ line }}:模板里标签两边的换行缩进会被 Vue 压成一个空格,
|
||||||
@@ -233,14 +232,26 @@ function lines(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.diff {
|
.diff {
|
||||||
background-color: rgba(208, 48, 80, 0.2);
|
background-color: rgba(208, 48, 80, 0.18);
|
||||||
|
color: v-bind("theme.errorColorPressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
opacity: 0.6;
|
color: v-bind("theme.textColor3");
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cut {
|
.cut {
|
||||||
|
margin-top: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -23,7 +23,8 @@ export function getSubmitButtonState({
|
|||||||
}: SubmitButtonStateInput): SubmitButtonState {
|
}: SubmitButtonStateInput): SubmitButtonState {
|
||||||
const disabled = !isAuthed || !hasCode || isFormatting || isSubmitting || isJudging || isCooldown
|
const disabled = !isAuthed || !hasCode || isFormatting || isSubmitting || isJudging || isCooldown
|
||||||
|
|
||||||
let label = "提交代码"
|
// 设计稿里就叫「提交」:它旁边是「运行例子」,一个试跑、一个交,不用再说「代码」
|
||||||
|
let label = "提交"
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
label = "请先登录"
|
label = "请先登录"
|
||||||
} else if (isFormatting) {
|
} else if (isFormatting) {
|
||||||
@@ -36,12 +37,8 @@ export function getSubmitButtonState({
|
|||||||
label = "正在冷却"
|
label = "正在冷却"
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon =
|
// 平时不带图标,只在忙的时候转圈
|
||||||
isFormatting || isSubmitting || isJudging
|
const icon = isFormatting || isSubmitting || isJudging ? "eos-icons:loading" : ""
|
||||||
? "eos-icons:loading"
|
|
||||||
: isCooldown
|
|
||||||
? "ph:lightbulb-fill"
|
|
||||||
: "ph:play-fill"
|
|
||||||
|
|
||||||
return { disabled, label, icon }
|
return { disabled, label, icon }
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from "naive-ui"
|
||||||
import { errorCode } from "utils/api"
|
import { errorCode } from "utils/api"
|
||||||
import { getProblem } from "oj/api"
|
import { getProblem } from "oj/api"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
@@ -93,6 +94,9 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const drawer = ref<DrawerKey | null>(null)
|
const drawer = ref<DrawerKey | null>(null)
|
||||||
|
// 抽屉挂进左栏:要等左栏这个元素挂上之后才能当 Teleport 的目标
|
||||||
|
const leftPane = useTemplateRef<HTMLElement>("leftPane")
|
||||||
|
const theme = useThemeVars()
|
||||||
|
|
||||||
// 协作中的老师看的是学生的提交
|
// 协作中的老师看的是学生的提交
|
||||||
const teacherCollab = useTeacherCollab()
|
const teacherCollab = useTeacherCollab()
|
||||||
@@ -234,61 +238,70 @@ onBeforeUnmount(() => {
|
|||||||
<template v-if="problem">
|
<template v-if="problem">
|
||||||
<SubmissionEffects />
|
<SubmissionEffects />
|
||||||
<StatisticsModal />
|
<StatisticsModal />
|
||||||
|
<!--
|
||||||
|
桌面:两栏顶满整个内容区(设计稿「机房(方案 B)」),中间一条 1px 分隔线、可拖动。
|
||||||
|
布局给内容区留了 16px 内边距,这里用负边距抵掉
|
||||||
|
-->
|
||||||
<n-split
|
<n-split
|
||||||
v-if="isDesktop"
|
v-if="isDesktop"
|
||||||
|
class="problem-split"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
:default-size="0.43"
|
:default-size="0.43"
|
||||||
:min="0.2"
|
:min="0.2"
|
||||||
:max="0.8"
|
:max="0.8"
|
||||||
style="height: calc(100vh - 92px)"
|
:resize-trigger-size="1"
|
||||||
>
|
>
|
||||||
<template #1>
|
<template #1>
|
||||||
<div class="left-pane">
|
<div id="problem-left-pane" ref="leftPane" class="left-pane">
|
||||||
<ContextBar />
|
<ContextBar />
|
||||||
<div class="tab-row">
|
<div class="tab-row">
|
||||||
<n-tabs
|
<div class="page-tabs" role="tablist" aria-label="题目 / 结果">
|
||||||
v-model:value="currentTab"
|
<button
|
||||||
type="segment"
|
type="button"
|
||||||
class="page-tabs"
|
role="tab"
|
||||||
@click="drawer = null"
|
class="page-tab"
|
||||||
>
|
:class="{ active: currentTab === 'content' }"
|
||||||
<n-tab name="content">题目</n-tab>
|
:aria-selected="currentTab === 'content'"
|
||||||
<n-tab name="result"><ResultTabLabel /></n-tab>
|
@click="currentTab = 'content'"
|
||||||
</n-tabs>
|
>
|
||||||
<n-flex v-if="ctx.drawers.length" :size="6" :wrap="false">
|
题目
|
||||||
<n-button
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="page-tab"
|
||||||
|
:class="{ active: currentTab === 'result' }"
|
||||||
|
:aria-selected="currentTab === 'result'"
|
||||||
|
@click="currentTab = 'result'"
|
||||||
|
>
|
||||||
|
<ResultTabLabel />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="ctx.drawers.length" class="drawer-buttons">
|
||||||
|
<button
|
||||||
v-for="key in ctx.drawers"
|
v-for="key in ctx.drawers"
|
||||||
:key="key"
|
:key="key"
|
||||||
size="small"
|
type="button"
|
||||||
:type="drawer === key ? 'primary' : 'default'"
|
class="drawer-button"
|
||||||
:secondary="drawer === key"
|
|
||||||
@click="toggleDrawer(key)"
|
@click="toggleDrawer(key)"
|
||||||
>
|
>
|
||||||
{{ drawerLabel(key) }}
|
{{ drawerLabel(key) }}
|
||||||
</n-button>
|
</button>
|
||||||
</n-flex>
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
抽屉挂在页签行下面这一块(position: relative),盖住题面、编辑器不动。
|
|
||||||
页签行露在外面:同一个按钮再点一下就关、点另一个就换,点「题目 / 结果」也会关
|
|
||||||
-->
|
|
||||||
<div id="problem-pane-stack" class="pane-stack">
|
|
||||||
<!--
|
|
||||||
两个页签各滚各的:读题读到底下切去看结果,结果不该也停在底下。
|
|
||||||
v-show 挂在外面这层 div 上 —— 直接挂在 n-scrollbar 上不生效,它自己管根节点的 style
|
|
||||||
-->
|
|
||||||
<div v-show="currentTab === 'content'" class="pane-body">
|
|
||||||
<n-scrollbar content-style="padding-top: 8px">
|
|
||||||
<ProblemContent />
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="resultMounted" v-show="currentTab === 'result'" class="pane-body">
|
|
||||||
<n-scrollbar content-style="padding-top: 8px">
|
|
||||||
<ResultPane />
|
|
||||||
</n-scrollbar>
|
|
||||||
</div>
|
|
||||||
<ProblemDrawer v-model="drawer" to="#problem-pane-stack" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 两个页签各滚各的:读题读到底下切去看结果,结果不该也停在底下 -->
|
||||||
|
<div v-show="currentTab === 'content'" class="pane-body">
|
||||||
|
<n-scrollbar content-style="padding: 16px 20px">
|
||||||
|
<ProblemContent />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
<div v-if="resultMounted" v-show="currentTab === 'result'" class="pane-body">
|
||||||
|
<n-scrollbar content-style="padding: 14px 20px">
|
||||||
|
<ResultPane />
|
||||||
|
</n-scrollbar>
|
||||||
|
</div>
|
||||||
|
<!-- 抽屉盖住整个左栏(连上下文条和页签行),抽屉顶上有自己的页签可以互相切换;编辑器不动 -->
|
||||||
|
<ProblemDrawer v-if="leftPane" v-model="drawer" :to="leftPane" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
@@ -335,48 +348,100 @@ onBeforeUnmount(() => {
|
|||||||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.problem-split {
|
||||||
* 分隔条两边各留 12px:左栏页签行右端的「统计 / 点评 / 我的提交」和右栏工具栏最左的
|
margin: -16px;
|
||||||
* 「写代码 / 画流程图」都是描边按钮、又在同一高度,原来紧贴着分隔条,看上去连成一排
|
width: calc(100% + 32px);
|
||||||
*/
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分隔线:看上去 1px,拖的时候热区宽一点 */
|
||||||
|
.problem-split :deep(.n-split__resize-trigger) {
|
||||||
|
background-color: v-bind("theme.borderColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 热区挂在分隔线本身上:wrapper 不是定位元素,挂在它上面的 ::before 会盖住整个分栏 */
|
||||||
|
.problem-split :deep(.n-split__resize-trigger-wrapper) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-split :deep(.n-split__resize-trigger-wrapper)::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 -4px;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
.left-pane {
|
.left-pane {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-right: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-pane {
|
.right-pane {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-left: 12px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 「题目 / 结果」:下划线式页签,右边是抽屉的三个胶囊按钮 */
|
||||||
.tab-row {
|
.tab-row {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 20px 0 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 2px;
|
||||||
gap: 12px;
|
border-bottom: 1px solid v-bind("theme.dividerColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-tabs {
|
.page-tabs {
|
||||||
width: 180px;
|
display: flex;
|
||||||
flex: none;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-stack {
|
.page-tab {
|
||||||
position: relative;
|
height: 42px;
|
||||||
flex: 1;
|
padding: 0 12px;
|
||||||
min-height: 0;
|
border: 0;
|
||||||
overflow: hidden;
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tab.active {
|
||||||
|
border-bottom-color: v-bind("theme.primaryColor");
|
||||||
|
color: v-bind("theme.textColor1");
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-buttons {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 11px;
|
||||||
|
border: 1px solid v-bind("theme.borderColor");
|
||||||
|
border-radius: 16px;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor2");
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-button:hover {
|
||||||
|
border-color: v-bind("theme.primaryColorHover");
|
||||||
|
color: v-bind("theme.primaryColor");
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-body {
|
.pane-body {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 把题面里的一段 Markdown 摊成一行纯文字,放进结果页签里那种「题目原话:……」的小字里。
|
||||||
|
* 不是完整的 Markdown 解析:题面用到的就是强调、行内代码、链接、图片、标题、列表、
|
||||||
|
* 公式这几样,去掉记号、留下字就够了。太长的截断,免得一句提示撑满半屏。
|
||||||
|
*/
|
||||||
|
export function markdownToText(markdown: string, max = 120) {
|
||||||
|
const text = markdown
|
||||||
|
.replace(/```[\s\S]*?```/g, " ")
|
||||||
|
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
||||||
|
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
||||||
|
.replace(/<[^>]+>/g, " ")
|
||||||
|
.replace(/`([^`]*)`/g, "$1")
|
||||||
|
.replace(/\$\$?([^$]*)\$\$?/g, "$1")
|
||||||
|
.replace(/^\s{0,3}(#{1,6}|>|[-*+]|\d+\.)\s+/gm, "")
|
||||||
|
.replace(/(\*\*|__|\*|_|~~)(.+?)\1/g, "$2")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
return text.length > max ? `${text.slice(0, max)}…` : text
|
||||||
|
}
|
||||||
Reference in new issue
Block a user