style(题目): 运行例子、自己输入、Python 报错、流程图点评这几块结果也照设计稿重排,手机上题目页顶满屏幕
- 运行例子:标题带每个例子的对错胶囊,全对时提示「再按提交」 - 自己输入:说明「这里不判对错」,输出框留出固定高度 - Python 报错:灰底卡片,改法按钮放显眼处 - 流程图点评:画过几次连成一排,大分数 + 等级 + 四项分数条,重新提交后直接显示新的这一次 - 工具栏「写代码 / 画流程图」换成设计稿的分段切换,窄时缩成「代码 / 流程图」 - 设计文档记下和画板有意不同的几处(去掉限时、保留新轮盘等) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fd9bf67c2
commit
5836902c6b
14 files changed
+598
-289
No files matched your search
@@ -427,7 +427,9 @@ onSolvedHere(() => {
|
||||
color: v-bind("tone.setText");
|
||||
}
|
||||
|
||||
/* 比赛名不跟题号那段抢地方:题号多了本来就能横向滚,名字挤成两个字就认不出了 */
|
||||
.contest-back {
|
||||
flex: none;
|
||||
max-width: 160px;
|
||||
color: v-bind("tone.contestText");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
@@ -15,6 +16,7 @@ const submissionStore = useSubmissionStore()
|
||||
const { customInput, customOutput, customResult, customRunning } = submissionStore.trial
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const codeStore = useCodeStore()
|
||||
const theme = useThemeVars()
|
||||
|
||||
function fillSample(input: string) {
|
||||
customInput.value = input
|
||||
@@ -22,11 +24,9 @@ function fillSample(input: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical :size="10">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span class="label">输入</span>
|
||||
<n-text depth="3" class="hint">自己编数据试试,这里不判对错、不算提交</n-text>
|
||||
</n-flex>
|
||||
<!-- 设计稿「自己输入:顶替原来的『自测』模式」;「这里不判对错」在分段那一行右边(ResultPane) -->
|
||||
<div class="custom">
|
||||
<span class="label">输入</span>
|
||||
<n-input
|
||||
v-model:value="customInput"
|
||||
type="textarea"
|
||||
@@ -34,10 +34,10 @@ function fillSample(input: string) {
|
||||
placeholder="在这里写输入,和题目里的「输入」格式一样"
|
||||
class="mono"
|
||||
/>
|
||||
<n-flex align="center" :size="8">
|
||||
<div class="actions">
|
||||
<n-button
|
||||
type="primary"
|
||||
secondary
|
||||
ghost
|
||||
:loading="customRunning"
|
||||
:disabled="!codeStore.code.value.trim()"
|
||||
@click="submissionStore.runCustom()"
|
||||
@@ -47,49 +47,69 @@ function fillSample(input: string) {
|
||||
<n-button
|
||||
v-for="(sample, index) in problem?.samples ?? []"
|
||||
:key="index"
|
||||
quaternary
|
||||
size="small"
|
||||
text
|
||||
class="fill"
|
||||
@click="fillSample(sample.input)"
|
||||
>
|
||||
填入例子 {{ index + 1 }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<template v-if="customResult !== null">
|
||||
<span class="label">输出</span>
|
||||
<TrialErrorNote
|
||||
v-if="customResult !== SubmissionStatus.accepted"
|
||||
:result="customResult"
|
||||
:output="customOutput"
|
||||
:language="codeStore.code.language"
|
||||
/>
|
||||
<pre v-else-if="customOutput" class="output">{{ customOutput }}</pre>
|
||||
<n-text v-else depth="3">(什么都没有输出)</n-text>
|
||||
</template>
|
||||
</n-flex>
|
||||
<span class="label">输出</span>
|
||||
<TrialErrorNote
|
||||
v-if="customResult !== null && customResult !== SubmissionStatus.accepted"
|
||||
:result="customResult"
|
||||
:output="customOutput"
|
||||
:language="codeStore.code.language"
|
||||
/>
|
||||
<pre v-else class="output" :class="{ empty: !customOutput }">{{
|
||||
customResult === null ? "" : customOutput || "(什么都没有输出)"
|
||||
}}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 13px;
|
||||
.mono :deep(textarea) {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
}
|
||||
|
||||
.mono :deep(textarea) {
|
||||
font-family: Monaco, Consolas, monospace;
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fill {
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.output {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
min-height: 88px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 10px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.08);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: Monaco, Consolas, monospace;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.output.empty {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
</style>
|
||||
@@ -35,6 +35,10 @@ const { problem, codeLanguages, canDraw } = storeToRefs(problemStore)
|
||||
* Flowchart 的「语言」—— 有的题流程图交了几百次、代码个位数,入口却在下拉的第一项里。
|
||||
*/
|
||||
const drawing = computed(() => codeStore.code.language === "Flowchart")
|
||||
const MODES = [
|
||||
{ value: "code", label: "写代码", short: "代码" },
|
||||
{ value: "draw", label: "画流程图", short: "流程图" },
|
||||
] as const
|
||||
const mode = computed({
|
||||
get: () => (drawing.value ? "draw" : "code"),
|
||||
set: (value: "draw" | "code") => {
|
||||
@@ -156,10 +160,26 @@ const changeLanguage = (v: LANGUAGE) => {
|
||||
<!-- 设计稿:语言、运行例子、提交靠左;求助、课堂统计、⋯ 推到最右 -->
|
||||
<div ref="toolbarRef" class="toolbar" :class="{ narrow }">
|
||||
<!-- 协作中的老师不会落到画图:求助入口本身就排掉了流程图 -->
|
||||
<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="draw">{{ narrow ? "流程图" : "画流程图" }}</n-radio-button>
|
||||
</n-radio-group>
|
||||
<!-- 设计稿:灰底分段,选中的白底加粗 -->
|
||||
<div
|
||||
v-if="canDraw && !showCollabBar"
|
||||
class="mode"
|
||||
role="tablist"
|
||||
aria-label="写代码 / 画流程图"
|
||||
>
|
||||
<button
|
||||
v-for="item in MODES"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="mode-item"
|
||||
:class="{ active: mode === item.value }"
|
||||
:aria-selected="mode === item.value"
|
||||
@click="mode = item.value"
|
||||
>
|
||||
{{ narrow ? item.short : item.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 协作中编辑器的语言跟着学生走,这个选择器改了也不会生效,索性禁掉 -->
|
||||
<n-select
|
||||
@@ -248,6 +268,33 @@ const changeLanguage = (v: LANGUAGE) => {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.mode {
|
||||
flex: none;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.12);
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
height: 28px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
color: v-bind("theme.textColor2");
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mode-item.active {
|
||||
background-color: v-bind("theme.cardColor");
|
||||
color: v-bind("theme.textColor1");
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.more {
|
||||
width: 34px;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
||||
@@ -19,15 +20,16 @@ const { phase, scores, hiddenCount, selectedId, details, detailLoading, editor }
|
||||
const problemStore = useProblemStore()
|
||||
const codeStore = useCodeStore()
|
||||
const message = useMessage()
|
||||
const theme = useThemeVars()
|
||||
// 暗色主题的成功色是浅薄荷绿,上面的白字看不清
|
||||
const isDark = useDark()
|
||||
const passText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
|
||||
|
||||
/** 正在看的那一次:点过哪次就是哪次,否则是最新的 */
|
||||
const current = computed(() => {
|
||||
const id = selectedId.value ?? scores.value.at(-1)?.id
|
||||
return id ? (scores.value.find((row) => row.id === id) ?? null) : null
|
||||
})
|
||||
const currentIndex = computed(() =>
|
||||
current.value ? scores.value.findIndex((row) => row.id === current.value!.id) : -1,
|
||||
)
|
||||
const detail = computed(() => (current.value ? details.value[current.value.id] : undefined))
|
||||
const viewingLatest = computed(() => selectedId.value === null)
|
||||
|
||||
@@ -62,8 +64,7 @@ function gradeType(grade: string) {
|
||||
}
|
||||
|
||||
function percentType(ratio: number) {
|
||||
if (ratio >= 0.8) return "success"
|
||||
if (ratio >= 0.6) return "info"
|
||||
if (ratio >= 0.6) return "success"
|
||||
if (ratio >= 0.4) return "warning"
|
||||
return "error"
|
||||
}
|
||||
@@ -159,110 +160,113 @@ function goCode() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 设计稿「画流程图:画布占满右栏,AI 点评在左边」「流程图评分中」 -->
|
||||
<div class="flowchart-result">
|
||||
<!-- 正在评:评的是最新这一次,所以只在看最新时显示 -->
|
||||
<n-alert
|
||||
v-if="phase === 'evaluating' && viewingLatest"
|
||||
type="info"
|
||||
:show-icon="false"
|
||||
class="block"
|
||||
>
|
||||
<n-flex align="center" :size="10" :wrap="false">
|
||||
<n-spin :size="18" />
|
||||
<div>
|
||||
<b>AI 正在看你的图</b>(一般 5 秒左右)
|
||||
<div class="note">评的时候画布还能改,改了不影响这一次</div>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-alert>
|
||||
|
||||
<n-alert
|
||||
v-else-if="phase === 'failed' && viewingLatest"
|
||||
type="warning"
|
||||
:show-icon="false"
|
||||
class="block"
|
||||
>
|
||||
AI 这次没评出来,再点一次「交给 AI 点评」试试
|
||||
</n-alert>
|
||||
|
||||
<n-empty
|
||||
v-if="!scores.length && phase !== 'evaluating'"
|
||||
class="empty"
|
||||
description="还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里"
|
||||
/>
|
||||
|
||||
<template v-if="scores.length">
|
||||
<!-- 历次分数:一眼看出是不是越改越好 -->
|
||||
<div class="history" role="list" aria-label="历次分数">
|
||||
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
||||
<div v-if="scores.length || phase === 'evaluating'" class="history">
|
||||
<span class="history-lead"
|
||||
>AI 点评 · 画过 {{ scores.length + (phase === "evaluating" ? 1 : 0) }} 次:</span
|
||||
>
|
||||
<template v-for="(row, index) in scores" :key="row.id">
|
||||
<span v-if="index > 0" class="arrow" aria-hidden="true">→</span>
|
||||
<button
|
||||
v-for="(row, index) in scores"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
role="listitem"
|
||||
class="attempt"
|
||||
:class="[gradeType(row.grade), { active: current?.id === row.id }]"
|
||||
:title="parseTime(row.createTime, 'MM-DD HH:mm')"
|
||||
:class="{
|
||||
pass: isFlowchartPass(row.grade),
|
||||
active: current?.id === row.id && !(phase === 'evaluating' && viewingLatest),
|
||||
}"
|
||||
:title="`第 ${index + 1} 次 · ${parseTime(row.createTime, 'MM-DD HH:mm')}`"
|
||||
@click="flowchartStore.select(index === scores.length - 1 ? null : row.id)"
|
||||
>
|
||||
<span class="attempt-no">第 {{ index + 1 }} 次</span>
|
||||
<b>{{ row.score }}</b>
|
||||
<span>{{ row.grade }}</span>
|
||||
{{ row.grade }} {{ row.score }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="phase === 'evaluating'">
|
||||
<span v-if="scores.length" class="arrow" aria-hidden="true">→</span>
|
||||
<span class="attempt pending">第 {{ scores.length + 1 }} 次</span>
|
||||
</template>
|
||||
</div>
|
||||
<p v-if="hiddenCount" class="note">
|
||||
另外 {{ hiddenCount }} 次是加入题单之前画的,先藏起来了:在题单里画到 A 或 S 就解锁
|
||||
</p>
|
||||
|
||||
<!-- 正在评:评的是最新这一次,所以只在看最新时显示 -->
|
||||
<template v-if="phase === 'evaluating' && viewingLatest">
|
||||
<div class="grading">
|
||||
<n-spin :size="26" />
|
||||
<div>
|
||||
<div class="grading-title">AI 正在看你的图</div>
|
||||
<div class="grading-note">一般 5 秒左右。可以先去看题目,评完这里会亮起来</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="hiddenCount" class="note">
|
||||
另外 {{ hiddenCount }} 次是加入题单之前画的,先藏起来了:在题单里画到 A 或 S 就解锁
|
||||
</p>
|
||||
<p class="note">评的时候画布上还能改,改了不影响这一次的分数</p>
|
||||
</template>
|
||||
|
||||
<section v-if="current" class="block">
|
||||
<n-flex align="baseline" :size="10">
|
||||
<n-text :type="gradeType(current.grade)" class="score">
|
||||
{{ current.score }} 分 · {{ current.grade }} 级
|
||||
</n-text>
|
||||
<n-text depth="3" class="note">
|
||||
第 {{ currentIndex + 1 }} 次 · {{ parseTime(current.createTime, "MM-DD HH:mm") }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
<div v-else-if="phase === 'failed' && viewingLatest" class="failed">
|
||||
AI 这次没评出来,再点一次「交给 AI 点评」试试
|
||||
</div>
|
||||
|
||||
<n-flex v-if="isFlowchartPass(current.grade)" align="center" class="pass" :size="12">
|
||||
<span>画到 {{ current.grade }} 级了,这道题的流程图算做完 ✓</span>
|
||||
<n-button v-if="canCode" size="small" type="primary" @click="goCode">
|
||||
<section v-else-if="current" class="current">
|
||||
<div class="score-line">
|
||||
<span class="score" :class="gradeType(current.grade)">{{ current.score }}</span>
|
||||
<span class="score-unit">分</span>
|
||||
<span class="grade" :class="gradeType(current.grade)">{{ current.grade }} 级</span>
|
||||
<span v-if="detail?.aiFeedback || isFlowchartPass(current.grade)" class="feedback">
|
||||
{{ detail?.aiFeedback ?? ""
|
||||
}}{{ isFlowchartPass(current.grade) ? " 这道题算做完了。" : "" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<n-flex v-if="detailLoading && !detail" justify="center" class="pad">
|
||||
<n-spin size="small" />
|
||||
</n-flex>
|
||||
|
||||
<template v-if="detail">
|
||||
<div v-if="criteria.length" class="criteria">
|
||||
<div v-for="[name, item] in criteria" :key="name" class="criterion">
|
||||
<div class="criterion-head">
|
||||
<b>{{ name }}</b>
|
||||
<span class="criterion-score">{{ item.score || 0 }} / {{ item.max }}</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div
|
||||
class="bar-fill"
|
||||
:class="percentType(item.score / item.max)"
|
||||
:style="{ width: `${Math.min(100, ((item.score || 0) / item.max) * 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="item.comment" class="comment">{{ item.comment }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="suggestions.length" class="suggestions">
|
||||
<div class="suggestions-title">还可以改进</div>
|
||||
<div v-for="(line, index) in suggestions" :key="index" class="suggestion">
|
||||
<span v-if="index === 0" class="tag">重点</span>
|
||||
<span>{{ line }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<n-button
|
||||
v-if="isFlowchartPass(current.grade) && canCode"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="goCode"
|
||||
>
|
||||
照着它写代码 →
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-if="detailLoading && !detail" justify="center" class="pad">
|
||||
<n-spin size="small" />
|
||||
</n-flex>
|
||||
|
||||
<template v-if="detail">
|
||||
<p v-if="detail.aiFeedback" class="feedback">{{ detail.aiFeedback }}</p>
|
||||
|
||||
<div v-if="suggestions.length" class="suggestions">
|
||||
<h4>可以这样改</h4>
|
||||
<ol>
|
||||
<li v-for="(line, index) in suggestions" :key="index">{{ line }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div v-if="criteria.length" class="criteria">
|
||||
<h4>四项得分</h4>
|
||||
<div v-for="[name, item] in criteria" :key="name" class="criterion">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<b>{{ name }}</b>
|
||||
<n-tag :type="percentType(item.score / item.max)" size="small" round>
|
||||
{{ item.score || 0 }} / {{ item.max }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-text v-if="item.comment" depth="3" class="comment">{{ item.comment }}</n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-button v-if="canvasDiffers" secondary class="load" @click="loadToCanvas">
|
||||
把这一版载回画布
|
||||
</n-button>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
<n-button v-if="canvasDiffers" secondary @click="loadToCanvas">把这一版载回画布</n-button>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -271,18 +275,14 @@ function goCode() {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.block {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 13px;
|
||||
opacity: 0.75;
|
||||
margin: 2px 0 0;
|
||||
color: v-bind("theme.textColor3");
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.pad {
|
||||
@@ -291,98 +291,220 @@ function goCode() {
|
||||
|
||||
.history {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding: 2px 2px 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.history-lead {
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.attempt {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
min-width: 58px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.25);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 9px;
|
||||
border-radius: 13px;
|
||||
border: 1px solid transparent;
|
||||
background-color: rgba(128, 128, 128, 0.12);
|
||||
color: v-bind("theme.textColor2");
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.attempt b {
|
||||
font-size: 17px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.attempt-no {
|
||||
font-size: 11px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.attempt.success b {
|
||||
color: #18a058;
|
||||
}
|
||||
|
||||
.attempt.warning b {
|
||||
color: #f0a020;
|
||||
}
|
||||
|
||||
.attempt.error b {
|
||||
color: #d03050;
|
||||
}
|
||||
|
||||
.attempt.active {
|
||||
border-color: #2080f0;
|
||||
box-shadow: 0 0 0 1px #2080f0;
|
||||
background-color: v-bind("theme.textColor2");
|
||||
color: v-bind("theme.cardColor");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 22px;
|
||||
.attempt.pass.active {
|
||||
background-color: v-bind("theme.successColor");
|
||||
color: v-bind("passText");
|
||||
}
|
||||
|
||||
.attempt.pending {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
background: transparent;
|
||||
border: 1px dashed v-bind("theme.successColor");
|
||||
color: v-bind("theme.successColorPressed");
|
||||
}
|
||||
|
||||
.grading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(24, 160, 88, 0.07);
|
||||
border: 1px solid rgba(24, 160, 88, 0.25);
|
||||
}
|
||||
|
||||
.grading-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pass {
|
||||
margin: 10px 0 4px;
|
||||
padding: 8px 12px;
|
||||
.grading-note {
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.failed {
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(24, 160, 88, 0.1);
|
||||
background-color: rgba(240, 160, 32, 0.1);
|
||||
border: 1px solid rgba(240, 160, 32, 0.35);
|
||||
}
|
||||
|
||||
.score-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.score.success {
|
||||
color: v-bind("theme.successColorPressed");
|
||||
}
|
||||
|
||||
.score.warning {
|
||||
color: v-bind("theme.warningColorPressed");
|
||||
}
|
||||
|
||||
.score.error {
|
||||
color: v-bind("theme.errorColorPressed");
|
||||
}
|
||||
|
||||
.score-unit {
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.grade {
|
||||
align-self: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background-color: v-bind("theme.errorColor");
|
||||
}
|
||||
|
||||
.grade.success {
|
||||
background-color: v-bind("theme.successColor");
|
||||
color: v-bind("passText");
|
||||
}
|
||||
|
||||
.grade.warning {
|
||||
background-color: v-bind("theme.warningColor");
|
||||
}
|
||||
|
||||
.feedback {
|
||||
margin: 12px 0;
|
||||
line-height: 1.7;
|
||||
flex: 1 1 200px;
|
||||
align-self: center;
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 14px 0 6px;
|
||||
/* 四项得分:两列,每项一条进度 */
|
||||
.criteria {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.criterion-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.suggestions ol {
|
||||
margin: 0;
|
||||
padding-left: 1.4em;
|
||||
line-height: 1.7;
|
||||
.criterion-score {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.criterion {
|
||||
padding: 6px 0;
|
||||
.bar {
|
||||
height: 6px;
|
||||
margin: 4px 0;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(128, 128, 128, 0.14);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.criterion + .criterion {
|
||||
border-top: 1px dashed rgba(128, 128, 128, 0.2);
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background-color: v-bind("theme.successColor");
|
||||
}
|
||||
|
||||
.bar-fill.warning {
|
||||
background-color: v-bind("theme.warningColor");
|
||||
}
|
||||
|
||||
.bar-fill.error {
|
||||
background-color: v-bind("theme.errorColor");
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.load {
|
||||
margin-top: 14px;
|
||||
.suggestions {
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
}
|
||||
|
||||
.suggestions-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.tag {
|
||||
flex: none;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
background-color: rgba(240, 160, 32, 0.16);
|
||||
color: v-bind("theme.warningColorPressed");
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -31,6 +31,8 @@ const canRunSamples = computed(
|
||||
<n-button
|
||||
v-if="canRunSamples"
|
||||
size="large"
|
||||
type="primary"
|
||||
ghost
|
||||
class="action"
|
||||
:loading="samplesRunning"
|
||||
:disabled="!codeStore.code.value.trim()"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { findChinesePunctuation } from "oj/problem/utils/chinesePunctuation"
|
||||
import {
|
||||
@@ -23,23 +24,28 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const codeStore = useCodeStore()
|
||||
const theme = useThemeVars()
|
||||
|
||||
const explanation = computed(() => explainPythonCompileError(props.errInfo))
|
||||
|
||||
/** 判题机的临时目录名(`/judger/run/<32 位随机串>/`)对学生没有意义,只剩文件名 */
|
||||
const rawError = computed(() => props.errInfo.replace(/\/judger\/run\/[^/"]+\//g, ""))
|
||||
|
||||
/** 出错那一行拆成三段,中间那段是 `^` 标的地方 */
|
||||
const sourceParts = computed(() => {
|
||||
/**
|
||||
* 报错只点名第一处。别的行也有中文标点的话,一句话带过(设计稿「语法没过」:
|
||||
* 「第 4 行也有一个中文冒号,编辑器里都标出来了」)。原来这里还摆一段出错那行的代码摘录,
|
||||
* 编辑器里已经标着红,设计稿里去掉了
|
||||
*/
|
||||
const otherPunctuationLines = computed(() => {
|
||||
const ex = explanation.value
|
||||
if (!ex?.sourceLine) return null
|
||||
const { sourceLine: text, caret } = ex
|
||||
if (!caret) return { before: text, marked: "", after: "" }
|
||||
return {
|
||||
before: text.slice(0, caret.from),
|
||||
marked: text.slice(caret.from, caret.to),
|
||||
after: text.slice(caret.to),
|
||||
if (!ex?.punctuation) return []
|
||||
const code = codeStore.code.value
|
||||
const lines = new Set<number>()
|
||||
for (const fix of findChinesePunctuation(code)) {
|
||||
lines.add(code.slice(0, fix.from).split("\n").length)
|
||||
}
|
||||
lines.delete(ex.line ?? -1)
|
||||
return [...lines].sort((a, b) => a - b)
|
||||
})
|
||||
|
||||
// 数的是编辑器里**现在**的代码:学生可能已经自己改掉几处了
|
||||
@@ -72,68 +78,63 @@ onUnmounted(clearErrorMark)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card embedded class="explain-card">
|
||||
<n-flex vertical :size="12">
|
||||
<template v-if="explanation">
|
||||
<div class="explain">
|
||||
<b v-if="explanation.line">第 {{ explanation.line }} 行:</b>{{ explanation.message }}
|
||||
</div>
|
||||
<pre v-if="sourceParts" class="explain-code">{{ sourceParts.before
|
||||
}}<mark>{{ sourceParts.marked }}</mark>{{ sourceParts.after }}</pre>
|
||||
<n-flex v-if="explanation.punctuation" align="center">
|
||||
<n-button v-if="punctuationFixCount" type="primary" size="small" @click="fixPunctuation">
|
||||
把中文标点都换成英文({{ punctuationFixCount }} 处)
|
||||
</n-button>
|
||||
<n-text v-else-if="fixedCount" type="success">
|
||||
换好了 {{ fixedCount }} 处,再提交一次试试。
|
||||
</n-text>
|
||||
</n-flex>
|
||||
<n-collapse>
|
||||
<n-collapse-item title="原始报错(英文)" name="raw">
|
||||
<div class="raw">{{ rawError }}</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</template>
|
||||
<!-- 翻译表没覆盖到的句式(生产数据里约 0.3%):照旧给原文 -->
|
||||
<template v-else>
|
||||
<div class="explain">请仔细检查,看看代码的格式是不是写错了!</div>
|
||||
<div class="raw">{{ rawError }}</div>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
<div class="explain-card">
|
||||
<template v-if="explanation">
|
||||
<p class="explain">
|
||||
<b v-if="explanation.line">第 {{ explanation.line }} 行:</b>{{ explanation.message }}
|
||||
</p>
|
||||
<p v-if="otherPunctuationLines.length" class="explain other">
|
||||
第 {{ otherPunctuationLines.join("、") }} 行也有中文标点,编辑器里都标出来了。
|
||||
</p>
|
||||
<div v-if="explanation.punctuation">
|
||||
<n-button v-if="punctuationFixCount" type="primary" @click="fixPunctuation">
|
||||
把中文标点都换成英文({{ punctuationFixCount }} 处)
|
||||
</n-button>
|
||||
<n-text v-else-if="fixedCount" type="success">
|
||||
换好了 {{ fixedCount }} 处,再提交一次试试。
|
||||
</n-text>
|
||||
</div>
|
||||
<n-collapse>
|
||||
<n-collapse-item title="原始报错(英文)" name="raw">
|
||||
<div class="raw">{{ rawError }}</div>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</template>
|
||||
<!-- 翻译表没覆盖到的句式(生产数据里约 0.3%):照旧给原文 -->
|
||||
<template v-else>
|
||||
<p class="explain">请仔细检查,看看代码的格式是不是写错了!</p>
|
||||
<div class="raw">{{ rawError }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 结果弹窗不限宽,长句子不折行会把弹窗撑出屏幕 */
|
||||
/* 和答错的说明同一种浅灰卡片(设计稿「语法没过」) */
|
||||
.explain-card {
|
||||
max-width: 560px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.explain {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.explain-code {
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
|
||||
.explain-code mark {
|
||||
color: inherit;
|
||||
background-color: rgba(208, 48, 80, 0.25);
|
||||
text-decoration: underline wavy #d03050;
|
||||
text-underline-offset: 3px;
|
||||
.explain.other {
|
||||
font-size: 14px;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.raw {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -83,6 +83,9 @@ const canTrial = computed(() => {
|
||||
>
|
||||
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
||||
</button>
|
||||
<span v-if="resultSegment === 'custom'" class="segment-note"
|
||||
>自己编数据试试,这里不判对错</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
||||
@@ -135,6 +138,13 @@ const canTrial = computed(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.segment-note {
|
||||
margin-left: auto;
|
||||
align-self: center;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.segment.active {
|
||||
background-color: v-bind("theme.textColor1");
|
||||
color: v-bind("theme.cardColor");
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { SubmissionStatus } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import TrialErrorNote from "./TrialErrorNote.vue"
|
||||
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
|
||||
import ResultHeader from "./ResultHeader.vue"
|
||||
|
||||
/**
|
||||
* 「结果」页签里「运行例子」那一段:每个例子对没对上,点一个看细节。
|
||||
* 没对上的说明复用判完之后那一套(WrongAnswerExplain);比赛里只摆输出、不给提示句。
|
||||
*/
|
||||
|
||||
const { sampleRuns, samplesRunning, samplesRunAt, samplesCode } = useSubmissionStore().trial
|
||||
const { sampleRuns, samplesRunning, samplesCode } = useSubmissionStore().trial
|
||||
const theme = useThemeVars()
|
||||
const route = useRoute()
|
||||
const inContest = computed(() => !!route.params.contestID)
|
||||
@@ -43,38 +42,62 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
/>
|
||||
|
||||
<n-flex v-else vertical :size="12">
|
||||
<n-flex align="center" :size="10">
|
||||
<n-icon :size="22" :color="failedCount ? theme.errorColor : theme.successColor">
|
||||
<Icon :icon="failedCount ? 'ph:x-circle-fill' : 'ph:check-circle-fill'" />
|
||||
</n-icon>
|
||||
<span class="title" :style="{ color: failedCount ? theme.errorColor : theme.successColor }">
|
||||
{{ failedCount ? `试跑:${failedCount} 个例子没对上` : "试跑:例子都对上了" }}
|
||||
</span>
|
||||
<n-text depth="3"
|
||||
>(不算提交{{ samplesRunAt ? ` · ${parseTime(samplesRunAt, "HH:mm")}` : "" }})</n-text
|
||||
>
|
||||
</n-flex>
|
||||
|
||||
<n-flex :size="6">
|
||||
<n-button
|
||||
v-for="run in sampleRuns"
|
||||
:key="run.index"
|
||||
size="small"
|
||||
round
|
||||
:type="run.result === SubmissionStatus.accepted ? 'success' : 'error'"
|
||||
:secondary="selected !== run.index"
|
||||
@click="selected = run.index"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon :icon="run.result === SubmissionStatus.accepted ? 'ph:check-bold' : 'ph:x-bold'" />
|
||||
</template>
|
||||
例子 {{ run.index + 1 }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<!-- 设计稿「运行例子:例子 2 没对上(不算提交)」:标题、「不算提交」、各例子的芯片在一行 -->
|
||||
<ResultHeader
|
||||
:kind="failedCount ? 'error' : 'success'"
|
||||
:title="failedCount ? `试跑:${failedCount} 个例子没对上` : '试跑:例子都对上了'"
|
||||
sub="(不算提交)"
|
||||
>
|
||||
<template #extra>
|
||||
<div class="runs">
|
||||
<button
|
||||
v-for="run in sampleRuns"
|
||||
:key="run.index"
|
||||
type="button"
|
||||
class="run"
|
||||
:class="[
|
||||
run.result === SubmissionStatus.accepted ? 'ok' : 'bad',
|
||||
{ active: selected === run.index },
|
||||
]"
|
||||
:aria-pressed="selected === run.index"
|
||||
@click="selected = run.index"
|
||||
>
|
||||
<svg
|
||||
v-if="run.result === SubmissionStatus.accepted"
|
||||
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
|
||||
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>
|
||||
例子 {{ run.index + 1 }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</ResultHeader>
|
||||
|
||||
<template v-if="current">
|
||||
<!-- 对上了:三栏摆出来就行 -->
|
||||
<n-card v-if="current.result === SubmissionStatus.accepted" embedded size="small">
|
||||
<div v-if="current.result === SubmissionStatus.accepted" class="passed-card">
|
||||
<div class="blocks">
|
||||
<div class="block">
|
||||
<div class="label">输入</div>
|
||||
@@ -89,7 +112,7 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
<pre>{{ current.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
<!-- 跑完了、输出对不上:和判完之后同一套说明 -->
|
||||
<WrongAnswerExplain
|
||||
v-else-if="current.result === SubmissionStatus.wrong_answer"
|
||||
@@ -114,9 +137,7 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
/>
|
||||
</template>
|
||||
|
||||
<n-text depth="3" class="note">
|
||||
例子都对上了再按「提交代码」。例子对了也可能在别的测试点上错,提交才算数。
|
||||
</n-text>
|
||||
<p class="note">例子都对上了再按「提交」。例子对了也可能在别的测试点上错,提交才算数。</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
@@ -126,9 +147,55 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
.runs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.run {
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
border-radius: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.run.ok {
|
||||
border: 1px solid rgba(24, 160, 88, 0.35);
|
||||
background-color: rgba(24, 160, 88, 0.1);
|
||||
color: v-bind("theme.successColorPressed");
|
||||
}
|
||||
|
||||
.run.bad {
|
||||
border: 1px solid rgba(208, 48, 80, 0.4);
|
||||
background-color: rgba(208, 48, 80, 0.07);
|
||||
color: v-bind("theme.errorColorPressed");
|
||||
}
|
||||
|
||||
.run.active {
|
||||
font-weight: 600;
|
||||
border-width: 1.5px;
|
||||
}
|
||||
|
||||
.run.ok.active {
|
||||
border-color: v-bind("theme.successColor");
|
||||
}
|
||||
|
||||
.run.bad.active {
|
||||
border-color: v-bind("theme.errorColor");
|
||||
}
|
||||
|
||||
.passed-card {
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
}
|
||||
|
||||
.blocks {
|
||||
@@ -139,6 +206,10 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
|
||||
.block {
|
||||
min-width: 0;
|
||||
padding: 6px 10px 8px;
|
||||
border-radius: 5px;
|
||||
background-color: v-bind("theme.cardColor");
|
||||
border: 1px solid v-bind("theme.dividerColor");
|
||||
}
|
||||
|
||||
.label {
|
||||
@@ -156,6 +227,8 @@ const current = computed(() => sampleRuns.value[selected.value] ?? null)
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
</style>
|
||||
@@ -435,6 +435,14 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 手机上「让 AI 分析我的代码」占满一行、44px 高,好按 */
|
||||
@media (max-width: 768px) {
|
||||
.actions :deep(.n-button:not(.n-button--text-type)) {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.hint-error {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,6 @@ watch(() => problem.value?.id, flowchartStore.ensureLoaded, { immediate: true })
|
||||
:disabled="loading"
|
||||
@click="submit"
|
||||
>
|
||||
{{ loading ? "AI 正在看…" : "交给 AI 点评" }}
|
||||
{{ loading ? "AI 点评中…" : "交给 AI 点评" }}
|
||||
</n-button>
|
||||
</template>
|
||||
@@ -182,6 +182,7 @@ function lines(text: string) {
|
||||
<style scoped>
|
||||
/* 设计稿「答案错误」:一块浅灰卡片,三栏并排的白底小框 */
|
||||
.explain-card {
|
||||
container-type: inline-size;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
@@ -202,6 +203,13 @@ function lines(text: string) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 窄的时候(手机)一栏一个往下排,三个窄框里什么都看不清 */
|
||||
@container (max-width: 440px) {
|
||||
.blocks {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
min-width: 0;
|
||||
padding: 6px 10px 8px;
|
||||
|
||||
@@ -344,10 +344,17 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 手机:课堂条贴满屏宽(设计稿「手机:题目 / 代码 / 结果」),下面的内容照旧留 16px */
|
||||
.mobile {
|
||||
margin: -16px -16px 0;
|
||||
padding-bottom: calc(72px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.mobile > :deep(.n-tabs) {
|
||||
padding: 10px 16px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.problem-split {
|
||||
margin: -16px;
|
||||
width: calc(100% + 32px);
|
||||
|
||||
@@ -211,6 +211,8 @@ export const useFlowchartStore = defineStore("flowchart", () => {
|
||||
lastSubmittedMermaidCode.value = mermaidCode
|
||||
loading.value = true
|
||||
phase.value = "evaluating"
|
||||
// 结果页签回到最新这一次:刚才可能在翻以前的某一次,那样就看不到「AI 正在看你的图」
|
||||
selectedId.value = null
|
||||
// 交上去就切到「结果」页签,和交代码一样
|
||||
useSubmissionStore().revealResult()
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# OJ2 设计文档:题目详情页重设计
|
||||
|
||||
日期:2026-09-28
|
||||
状态:**第 14 节的 6 步和第 7 节都已实施**(2026-09-28)。和本文有出入的地方:协作中上下文条的跳转没有改成开新标签(老师不在课堂名单里,题库入口下这一条本来就不出现)。效果图在 claude.ai 的私有画布「题目详情页重设计」里(「机房(方案 B)」
|
||||
那一页是定稿,「初稿 1280×800」那一页已作废,只留着对照)。效果图表达不了的规则都写在这里,
|
||||
**实施时以本文为准**。
|
||||
状态:**第 14 节的 6 步和第 7 节都已实施**(2026-09-28),界面已照画布「机房(方案 B)」的画板逐块对齐过。和本文有出入的地方:协作中上下文条的跳转没有改成开新标签(老师不在课堂名单里,题库入口下这一条本来就不出现)。效果图在 claude.ai 的私有画布「题目详情页重设计」里(「机房(方案 B)」
|
||||
那一页是定稿,「初稿 1280×800」那一页已作废,只留着对照)。**画布管样子,本文管规则**:
|
||||
版式、颜色、文案照画板,效果图表达不了的规则写在这里。
|
||||
|
||||
和画板有意不同的几处(都是拍过板的):
|
||||
|
||||
- 题面**不写时间和内存限制**(画板上有「限时 1 秒」):用户 2026-09-28 拍板,中职学生看了反而迷惑。
|
||||
- 点评用重新设计过的 SVG 轮盘(第 7 节),不是画板上的样子。
|
||||
- 流程图编辑器内部(节点库、撤销重做那一排)沿用原来的布局,没照画板重排。
|
||||
- 「交之前检查」那一段不带时间;「运行例子」的结果胶囊在窄屏会折行。
|
||||
|
||||
题目页是全站的核心页面:学生在这一页读题、写代码、提交、看结果、改、去下一题,老师在这一页
|
||||
接求助、协作。动它之前先把第 12 节「旧功能 → 新位置」对一遍,不能漏。
|
||||
@@ -115,12 +122,12 @@ B 看得到。
|
||||
|
||||
### 5.2 「题目」页签
|
||||
|
||||
内容顺序:标题(题号 + 名称)→ 难度、状态标签、「限时 1 秒」→ **代码要求**(AST 规则,放最前)→
|
||||
内容顺序:标题(题号 + 名称)→ 难度、状态标签 → **代码要求**(AST 规则,放最前)→
|
||||
描述 → 输入 / 输出 → 参考流程图(折叠)或你画的流程图 → 例子 → 提示 → 来源 → 相似题(按原规则出现)。
|
||||
|
||||
- 例子改成表格:例子 N | 输入 | 输出,每格带「复制」。去掉每个例子旁的「测试」按钮。
|
||||
例子标题旁有「自己输入数据试试 ›」,跳到结果页签的「自己输入」。
|
||||
- 时间 / 内存限制只写「限时 1 秒」,不写 ms / MB。
|
||||
- 不写时间 / 内存限制(2026-09-28 去掉了「限时 1 秒」:学生的程序很少碰到限制,写出来反而迷惑)。
|
||||
- 流程图小节同一个位置,二选一:老师给的参考图(`showFlowchart`,默认折叠)或学生自己画到
|
||||
A/S 的那张(`allowFlowchart`,默认高约 150px,可「放大看」)。两个开关本来就互斥
|
||||
(`admin/problem/detail.vue`)。**不再**一进页面就强制切到「我的流程图」。
|
||||
|
||||
Reference in new issue
Block a user