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
@@ -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>
|
||||
Reference in new issue
Block a user