feat(流程图): 结果先说「还没画对 / 画对了」和哪里错、怎么改,逐组数据收成一行点开才看

学生不关心过了几组数据。标题不再写「6 组走对 3 组」;点评做主体(AI 没写出来时
拿程序找到的第一处现象顶上);画布上只把要改的框标红、旁边写「要改这个框」,
别的框不再变淡,点开某一组才亮它走过的路;画对了直接给「照着它写代码」。
提交列表右栏老师看到的和学生一样,逐组数据也收着。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-19 22:03:16 +08:00
1 parent d2fc0382ec
commit ba90fe893e
5 files changed
+251 -108

No files matched your search

@@ -8,9 +8,8 @@ import { useUserStore } from "shared/store/user"
import { sortFlowchartCriteria } from "utils/constants"
import { parseTime } from "utils/functions"
import { mermaidOrder, signature } from "shared/components/FlowchartEditor/model"
import FlowchartCases from "shared/components/FlowchartCases.vue"
import { caseHeadline, caseReport } from "utils/flowchart"
import { useTone } from "shared/composables/tone"
import FlowchartNote from "shared/components/FlowchartNote.vue"
import { caseReport } from "utils/flowchart"
/**
* 「结果」页签里流程图作业的那一块:AI 在评 / 评完了几分 / 历次分数 / 评语和改进建议。
@@ -40,7 +39,6 @@ watch(
)
const message = useMessage()
const theme = useThemeVars()
const tone = useTone()
// 暗色主题的成功色是浅薄荷绿,上面的白字看不清
const isDark = useDark()
const passText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
@@ -136,37 +134,47 @@ function loadToCanvas() {
/** 2026-10 起的评法:逐组走一遍。老提交是 null,照旧显示四项分 */
const report = computed(() => caseReport(detail.value?.aiCriteriaDetails))
/** 点开的是哪一组;换一次提交就默认点开第一组没走对的(设计稿 FlowResFail) */
/**
* 逐组数据默认收着、一组也不点开(设计稿 FlowNoteFail:学生不关心过了几组,先看哪里错、怎么改)。
* 换一次提交就收回去
*/
const selectedCase = ref<number | null>(null)
const casesOpen = ref(false)
watch(
() => [current.value?.id, report.value] as const,
([, next]) => {
const firstWrong = next?.cases.findIndex((row) => !row.ok) ?? -1
selectedCase.value = firstWrong >= 0 ? firstWrong : null
() => current.value?.id,
() => {
selectedCase.value = null
casesOpen.value = false
},
{ immediate: true },
)
/**
* 点开的那一组在画布上走过的路。AI 报的是 Mermaid 的框号(n3),按交上去那一版的结构对回
* 画布上标什么。没点开哪一组:只把要改的那个框标红、旁边写「要改这个框」,别的框照常;
* 点开了一组:亮出它走过的路。AI 报的是 Mermaid 的框号(n3),按交上去那一版的结构对回
* 画布上的框 —— 所以只在画布还是那一版时亮,改过了就不亮(载回画布之后又能亮)
*/
watch(
() => [selectedCase.value, report.value, canvasSame.value, editor.value] as const,
([index, next, same, instance]) => {
if (!instance) return
const row = index === null ? undefined : next?.cases[index]
if (!row || !same || !versionDoc.value) {
if (!next || !same || !versionDoc.value) {
instance.setHighlight(null)
return
}
const order = mermaidOrder(versionDoc.value)
const toStep = (id: string) => order[Number(id.slice(1))]
// 没走对的组标红 AI 指出的要改的框(错的根子),没有才退回程序找到的出结果的那个框
const bad = row.ok ? null : (next?.focus ?? row.wrongAt)
const toStep = (id: string | null) => (id ? (order[Number(id.slice(1))] ?? null) : null)
// 要改的框:AI 对着答案指出的(错的根子),没有才退回程序找到的第一处出错的框
const focus = next.focus ?? next.cases.find((row) => !row.ok)?.wrongAt ?? null
const row = index === null ? undefined : next.cases[index]
if (!row) {
const bad = toStep(focus)
instance.setHighlight(bad ? { order: [], bad, label: "要改这个框" } : null)
return
}
const bad = row.ok ? null : toStep(focus)
instance.setHighlight({
order: row.path.map(toStep).filter((id): id is string => !!id),
bad: bad ? (toStep(bad) ?? null) : null,
bad,
})
},
// 切回「结果」页签时组件重新挂载,这些值早就齐了,不会再变一次
@@ -244,7 +252,9 @@ function goCode() {
<!-- 设计稿 FlowResFail / FlowResPass:等级 + 几组走对,下面逐组 -->
<div v-if="report" class="headline">
<span class="grade" :class="gradeType(current.grade)">{{ current.grade }} 级</span>
<span class="headline-text">{{ caseHeadline(report) }}</span>
<span class="headline-text">{{
report.passed === report.total ? "画对了" : "还没画对"
}}</span>
<span v-if="isFlowchartPass(current.grade)" class="headline-note">这道题算做完了</span>
</div>
<div v-else class="score-line">
@@ -262,25 +272,23 @@ function goCode() {
</n-flex>
<template v-if="detail && report">
<!-- 没走对:AI 对着答案说的错在哪、怎么改,放在逐组结果前面(要改的框在画布上标红) -->
<div
v-if="report.passed < report.total && (detail.aiFeedback || detail.aiSuggestions)"
class="review"
<n-button
v-if="isFlowchartPass(current.grade) && canCode"
class="code-btn"
type="primary"
size="large"
@click="goCode"
>
<div v-if="detail.aiFeedback">
<div class="review-title">哪里错了</div>
<p>{{ detail.aiFeedback }}</p>
</div>
<div v-if="detail.aiSuggestions">
<div class="review-title">可以这样改</div>
<p>{{ detail.aiSuggestions }}</p>
</div>
</div>
<FlowchartCases v-model:selected="selectedCase" :report="report" class="block" />
<div v-if="report.passed === report.total && detail.aiSuggestions" class="better">
<div class="better-title">还可以更好</div>
<p>{{ detail.aiSuggestions }}</p>
</div>
照着它写代码 →
</n-button>
<FlowchartNote
v-model:selected="selectedCase"
v-model:open="casesOpen"
:report="report"
:feedback="detail.aiFeedback"
:suggestions="detail.aiSuggestions"
class="block"
/>
</template>
<template v-if="detail">
@@ -340,7 +348,7 @@ function goCode() {
<div class="actions">
<n-button
v-if="isFlowchartPass(current.grade) && canCode"
v-if="!report && isFlowchartPass(current.grade) && canCode"
type="primary"
size="large"
@click="goCode"
@@ -573,40 +581,8 @@ function goCode() {
margin-bottom: 12px;
}
.review {
margin: 0 0 14px;
padding: 10px 12px;
border-radius: 6px;
background: v-bind("tone('error').background");
display: flex;
flex-direction: column;
gap: 10px;
font-size: 14px;
line-height: 1.7;
}
.review-title {
font-weight: 600;
}
.review p {
margin: 2px 0 0;
}
.better {
.code-btn {
margin-bottom: 14px;
font-size: 14px;
line-height: 1.7;
}
.better-title {
font-weight: 600;
font-size: 15px;
margin-bottom: 4px;
}
.better p {
margin: 0;
}
/* 四项得分:两列,每项一条进度 */
@@ -10,8 +10,8 @@ import { useTone } from "shared/composables/tone"
import { submissionTimeText } from "../utils"
import FlowchartState from "./FlowchartState.vue"
import UserName from "shared/components/UserName.vue"
import FlowchartCases from "shared/components/FlowchartCases.vue"
import { caseHeadline, caseReport } from "utils/flowchart"
import FlowchartNote from "shared/components/FlowchartNote.vue"
import { caseReport } from "utils/flowchart"
/**
* 流程图模式的右栏:左边画出来的图,右边四项评分和建议。原来要点编号再开一个
@@ -92,9 +92,12 @@ async function load() {
markFocus(graph.value)
}
/** AI 指出的要改的那个框标红。pele 把 Mermaid 的框号放在节点 <g> 的 data-id 上 */
/**
* 要改的那个框标红:AI 对着答案指出的,没有就退回程序找到的第一处出错的框。
* pele 把 Mermaid 的框号放在节点 <g> 的 data-id 上
*/
function markFocus(el: HTMLElement | null) {
const focus = report.value?.focus
const focus = report.value?.focus ?? report.value?.cases.find((row) => !row.ok)?.wrongAt
if (!el || !focus) return
el.querySelector(`.pele-node[data-id="${focus}"]`)?.classList.add("oj-focus")
}
@@ -140,9 +143,14 @@ function barColor(score: number, max: number) {
const report = computed(() => caseReport(shown.value?.aiCriteriaDetails))
const allPassed = computed(() => !!report.value && report.value.passed === report.value.total)
const openCase = ref<number | null>(null)
/** 逐组数据默认收着,老师和学生一样(设计稿 FlowNotePane);翻到下一张收回去 */
const casesOpen = ref(false)
watch(
() => shown.value?.id,
() => (openCase.value = null),
() => {
openCase.value = null
casesOpen.value = false
},
)
const suggestions = computed(() =>
@@ -267,23 +275,15 @@ const canRetry = computed(
<span class="muted">这次评分失败了{{ teacher ? ",可以点「重新评分」" : "" }}</span>
</template>
<template v-else-if="shown && report">
<b class="headline">{{ caseHeadline(report) }}</b>
<!-- 没走对:AI 对着答案说的错在哪、怎么改,放在逐组结果前面(要改的框在左边图上标红) -->
<div v-if="!allPassed && (shown.aiFeedback || shown.aiSuggestions)" class="review">
<div v-if="shown.aiFeedback">
<b>哪里错了</b>
<p>{{ shown.aiFeedback }}</p>
</div>
<div v-if="shown.aiSuggestions">
<b>可以这样改</b>
<p>{{ shown.aiSuggestions }}</p>
</div>
</div>
<FlowchartCases v-model:selected="openCase" :report="report" />
<div v-if="allPassed && shown.aiSuggestions" class="note">
<b>还可以更好:</b>
<div>{{ shown.aiSuggestions }}</div>
</div>
<b class="headline">{{ allPassed ? "画对了" : "还没画对" }}</b>
<FlowchartNote
v-model:selected="openCase"
v-model:open="casesOpen"
:report="report"
:feedback="shown.aiFeedback"
:suggestions="shown.aiSuggestions"
compact
/>
</template>
<template v-else-if="shown">
<div v-for="[name, item] in criteria" :key="name" class="crit">
@@ -538,7 +538,7 @@ const canRetry = computed(
}
.headline {
font-size: 16px;
font-size: 17px;
}
/* 要改的框:压过 useMermaid 里按框类型上的色(那边也是 !important) */
@@ -549,21 +549,6 @@ const canRetry = computed(
stroke-width: 3px !important;
}
.review {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px;
border-radius: 6px;
background: v-bind("tone('error').background");
font-size: 14px;
line-height: 1.7;
}
.review p {
margin: 2px 0 0;
}
.score-box {
flex: 1 1 0;
min-width: 0;
@@ -576,9 +576,21 @@ function setHighlight(next: FlowHighlight | null) {
function nodeState(node: LaidNode) {
if (!highlight.value) return ""
if (node.id === highlight.value.bad) return "bad"
// 只标一个框(没点开哪一组)时别的框照常,不变淡
if (!highlight.value.order.length) return ""
return stepNo.value.has(node.id) ? "onpath" : "dim"
}
/** 标红那个框旁边的标签,贴在框的右边;判断、循环右顶点外写着「否」,再让开一点 */
const badTag = computed(() => {
const label = highlight.value?.label
const node = label && layout.value.nodes.find((item) => item.id === highlight.value?.bad)
if (!label || !node) return null
const w = label.length * 12 + 20
const gap = node.kind === "if" || node.kind === "while" ? 26 : 8
return { label, x: node.x + node.w + gap, y: node.y + node.h / 2 - 11, w }
})
/** 编号圆点的位置:方框贴在左上角,菱形挪到顶点右边(左边是「是」) */
function badgeAt(node: LaidNode) {
if (node.kind === "if" || node.kind === "while")
@@ -739,6 +751,12 @@ defineExpose({ getDoc, setDoc, undo, redo, clear, setHighlight, canUndo, canRedo
</text>
</g>
</template>
<g v-if="badTag" class="bad-tag" aria-hidden="true">
<rect :x="badTag.x" :y="badTag.y" :width="badTag.w" height="22" rx="11" />
<text :x="badTag.x + badTag.w / 2" :y="badTag.y + 15" text-anchor="middle">
{{ badTag.label }}
</text>
</g>
</svg>
<button
@@ -939,6 +957,15 @@ defineExpose({ getDoc, setDoc, undo, redo, clear, setHighlight, canUndo, canRedo
fill: rgba(208, 48, 80, 0.12);
}
.bad-tag rect {
fill: #d03050;
}
.bad-tag text {
font-size: 12px;
fill: #ffffff;
}
.badge circle {
fill: v-bind("theme.textColor1");
}
@@ -41,6 +41,8 @@ export interface Slot {
export interface FlowHighlight {
order: string[]
bad: string | null
/** 标红那个框旁边的小标签(「要改这个框」);不给就不画 */
label?: string
}
export const STEP_LABEL: Record<StepKind, string> = {
@@ -0,0 +1,153 @@
<script setup lang="ts">
import type { FlowchartCaseReport } from "@oj2/contract"
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import FlowchartCases from "shared/components/FlowchartCases.vue"
import { useTone } from "shared/composables/tone"
import { caseHeadline } from "utils/flowchart"
/**
* 流程图「走例子」评完以后给人看的那一块(设计稿 FlowNoteFail / FlowNotePass / FlowNotePane):
* 学生不关心过没过几组数据,关心的是哪里画错了、怎么改 —— 点评做主体,逐组数据收成一行,
* 点开才看。题目页的结果区和提交列表右栏共用,老师看到的和学生一样。
*
* 没走对:AI 对着答案写的「哪里错了 / 可以这样改」(ai_feedback / ai_suggestions);AI 没写出来
* (超时、被过滤、2026-10-19 之前的提交)就拿程序找到的第一处现象顶上「哪里错了」。
* 全对:只有写法建议时才有「还可以更好」。
*/
const props = defineProps<{
report: FlowchartCaseReport
feedback: string | null
suggestions: string | null
/** 提交列表右栏窄:字小一号 */
compact?: boolean
}>()
/** 点开的是哪一组(题目页拿它在画布上亮路径) */
const selected = defineModel<number | null>("selected", { default: null })
/** 逐组数据展开没有 */
const open = defineModel<boolean>("open", { default: false })
const theme = useThemeVars()
const tone = useTone()
const passed = computed(() => props.report.passed === props.report.total)
const wrong = computed(() => {
if (passed.value) return ""
return props.feedback?.trim() || props.report.cases.find((row) => !row.ok)?.why || ""
})
const fix = computed(() => (passed.value ? "" : (props.suggestions?.trim() ?? "")))
const better = computed(() => (passed.value ? (props.suggestions?.trim() ?? "") : ""))
/** 收起时连点开的那一组一起收掉(画布回到只标要改的框)。先算好再写:外面接着 v-model 时,
* 写完马上读 open.value 读到的还是旧值 */
function toggle() {
const next = !open.value
open.value = next
if (!next) selected.value = null
}
</script>
<template>
<div class="flowchart-note" :class="{ compact }">
<div v-if="wrong || fix || better" class="card">
<div v-if="wrong">
<div class="label" :style="{ color: tone('error').color }">
<Icon icon="ph:warning-circle-bold" :width="compact ? 15 : 16" />哪里错了
</div>
<p>{{ wrong }}</p>
</div>
<div v-if="fix">
<div class="label" :style="{ color: tone('success').color }">
<Icon icon="ph:pencil-simple-line-bold" :width="compact ? 15 : 16" />可以这样改
</div>
<p>{{ fix }}</p>
</div>
<div v-if="better">
<div class="label" :style="{ color: tone('success').color }">
<Icon icon="ph:lightbulb-bold" :width="compact ? 15 : 16" />还可以更好
</div>
<p>{{ better }}</p>
</div>
</div>
<button type="button" class="toggle" :aria-expanded="open" @click="toggle">
看每组数据 · {{ caseHeadline(report) }}
<Icon icon="ph:caret-down-bold" :width="13" class="caret" :class="{ up: open }" />
</button>
<FlowchartCases v-if="open" v-model:selected="selected" :report="report" />
</div>
</template>
<style scoped>
.flowchart-note {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 18px;
border-radius: 8px;
background: rgba(128, 128, 128, 0.08);
}
.compact .card {
gap: 12px;
padding: 12px 14px;
}
.label {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
font-weight: 700;
}
.compact .label {
font-size: 14px;
}
.card p {
margin: 6px 0 0;
font-size: 16px;
line-height: 1.75;
color: v-bind("theme.textColor1");
}
.compact .card p {
margin-top: 4px;
font-size: 15px;
line-height: 1.7;
}
.toggle {
align-self: flex-start;
height: 30px;
padding: 0 4px;
border: 0;
background: transparent;
font: inherit;
font-size: 13px;
color: v-bind("theme.textColor2");
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.toggle:hover {
color: v-bind("theme.primaryColor");
}
.caret {
transition: transform 0.15s;
}
.caret.up {
transform: rotate(180deg);
}
</style>