style(题目): 运行例子、自己输入、Python 报错、流程图点评这几块结果也照设计稿重排,手机上题目页顶满屏幕

- 运行例子:标题带每个例子的对错胶囊,全对时提示「再按提交」
- 自己输入:说明「这里不判对错」,输出框留出固定高度
- Python 报错:灰底卡片,改法按钮放显眼处
- 流程图点评:画过几次连成一排,大分数 + 等级 + 四项分数条,重新提交后直接显示新的这一次
- 工具栏「写代码 / 画流程图」换成设计稿的分段切换,窄时缩成「代码 / 流程图」
- 设计文档记下和画板有意不同的几处(去掉限时、保留新轮盘等)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-28 23:13:23 +08:00
1 parent 1fd9bf67c2
commit 5836902c6b
14 files changed
+598 -289

No files matched your search

@@ -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>