feat(流程图): 没画对也分得出远近——五档说法、完成度条、比上次多对了几组;右栏的图放大

只说「还没画对」,学生改了几次还是这一句,看不出在往前走。等级照旧存 S/A/B/C,
标题换成话:画对了,画得很清楚 / 画对了 / 快画对了 / 对了一部分 / 还没画对;
下面一根完成度条,走对几组亮几段;第二次起没画对时说比上次多(少)对了几组
(详情接口带上同一道题上一次走对了几组)。提交列表右栏的图从 42% 加宽到 62%,
小图也放大填满图框(最多 1.5 倍)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-19 22:11:42 +08:00
1 parent ba90fe893e
commit 9ae6287fb1
6 files changed
+154 -13

No files matched your search

+25 -2
View File
@@ -12,7 +12,7 @@ import {
NO_CLASS,
STUDENT_ROLES,
} from "@oj2/contract"
import { and, asc, count, desc, eq, inArray, isNull, sql, type SQL } from "drizzle-orm"
import { and, asc, count, desc, eq, inArray, isNull, lt, sql, type SQL } from "drizzle-orm"
import { Hono } from "hono"
import { requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
@@ -80,9 +80,31 @@ function cutoffsFor(user: AuthUser, rows: Array<{ userId: number; problemId: num
return problemSetLockCutoffs(user.id, [...new Set(own)])
}
/** 上一次走对了几组(见契约 previousCases):同一个人、同一道题、更早、评完了的那一次 */
async function previousCases(flowchart: typeof schema.flowchartSubmission.$inferSelect) {
const [previous] = await db
.select({ details: schema.flowchartSubmission.aiCriteriaDetails })
.from(schema.flowchartSubmission)
.where(
and(
eq(schema.flowchartSubmission.userId, flowchart.userId),
eq(schema.flowchartSubmission.problemId, flowchart.problemId),
eq(schema.flowchartSubmission.status, 2),
lt(schema.flowchartSubmission.createTime, flowchart.createTime),
),
)
.orderBy(desc(schema.flowchartSubmission.createTime))
.limit(1)
const details = asRecord(previous?.details)
if (details.version !== 2 || !Array.isArray(details.cases)) return null
const cases = details.cases.map((item) => asRecord(item))
return { passed: cases.filter((item) => item.ok === true).length, total: cases.length }
}
function flowchartData(
flowchart: typeof schema.flowchartSubmission.$inferSelect,
username: string,
previous: { passed: number; total: number } | null = null,
) {
return {
id: flowchart.id,
@@ -101,6 +123,7 @@ function flowchartData(
aiModel: flowchart.aiModel,
processingTime: flowchart.processingTime,
evaluationTime: flowchart.evaluationTime,
previousCases: previous,
} satisfies FlowchartSubmission
}
@@ -716,7 +739,7 @@ flowchartRoutes.get("/flowcharts/:id", requireAuth, async (c) => {
const user = c.get("user")!
if (!row || !canView(user, row.flowchart, row.problem, await cutoffsFor(user, [row.flowchart])))
return failure(c, 404, "flowchart-not-found", "Submission does not exist")
return success(c, flowchartData(row.flowchart, row.username))
return success(c, flowchartData(row.flowchart, row.username, await previousCases(row.flowchart)))
})
flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
@@ -9,7 +9,7 @@ import { sortFlowchartCriteria } from "utils/constants"
import { parseTime } from "utils/functions"
import { mermaidOrder, signature } from "shared/components/FlowchartEditor/model"
import FlowchartNote from "shared/components/FlowchartNote.vue"
import { caseReport } from "utils/flowchart"
import { caseReport, flowchartVerdict } from "utils/flowchart"
/**
* 「结果」页签里流程图作业的那一块:AI 在评 / 评完了几分 / 历次分数 / 评语和改进建议。
@@ -252,9 +252,7 @@ function goCode() {
<!-- 设计稿 FlowResFail / FlowResPass:等级 + 几组走对,下面逐组 -->
<div v-if="report" class="headline">
<span class="grade" :class="gradeType(current.grade)">{{ current.grade }} 级</span>
<span class="headline-text">{{
report.passed === report.total ? "画对了" : "还没画对"
}}</span>
<span class="headline-text">{{ flowchartVerdict(report, current.grade) }}</span>
<span v-if="isFlowchartPass(current.grade)" class="headline-note">这道题算做完了</span>
</div>
<div v-else class="score-line">
@@ -287,6 +285,7 @@ function goCode() {
:report="report"
:feedback="detail.aiFeedback"
:suggestions="detail.aiSuggestions"
:previous="detail.previousCases"
class="block"
/>
</template>
@@ -11,7 +11,7 @@ import { submissionTimeText } from "../utils"
import FlowchartState from "./FlowchartState.vue"
import UserName from "shared/components/UserName.vue"
import FlowchartNote from "shared/components/FlowchartNote.vue"
import { caseReport } from "utils/flowchart"
import { caseReport, flowchartVerdict } from "utils/flowchart"
/**
* 流程图模式的右栏:左边画出来的图,右边四项评分和建议。原来要点编号再开一个
@@ -89,9 +89,26 @@ async function load() {
detail.value = res
await nextTick()
await renderFlowchart(graph.value, res.mermaidCode)
fitGraph(graph.value)
markFocus(graph.value)
}
/**
* 图填满图框:大图照旧缩小,小图也放大,但最多到原尺寸的 1.5 倍(再大字就粗笨了)。
* 原来只缩不放,比大小这种五六个框的图在一大块空白中间只占一小团
*/
function fitGraph(el: HTMLElement | null) {
const svg = el?.querySelector("svg")
const w = Number.parseFloat(svg?.getAttribute("width") ?? "")
const h = Number.parseFloat(svg?.getAttribute("height") ?? "")
if (!svg || !w || !h) return
if (!svg.getAttribute("viewBox")) svg.setAttribute("viewBox", `0 0 ${w} ${h}`)
svg.style.width = "100%"
svg.style.height = "100%"
svg.style.maxWidth = `${w * 1.5}px`
svg.style.maxHeight = `${h * 1.5}px`
}
/**
* 要改的那个框标红:AI 对着答案指出的,没有就退回程序找到的第一处出错的框。
* pele 把 Mermaid 的框号放在节点 <g> 的 data-id 上
@@ -141,7 +158,6 @@ function barColor(score: number, max: number) {
/** 2026-10 起的「走例子」报告;老提交是 null,照旧显示四项分 */
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)
@@ -275,13 +291,14 @@ const canRetry = computed(
<span class="muted">这次评分失败了{{ teacher ? ",可以点「重新评分」" : "" }}</span>
</template>
<template v-else-if="shown && report">
<b class="headline">{{ allPassed ? "画对了" : "还没画对" }}</b>
<b class="headline">{{ flowchartVerdict(report, shown.aiGrade) }}</b>
<FlowchartNote
v-model:selected="openCase"
v-model:open="casesOpen"
:report="report"
:feedback="shown.aiFeedback"
:suggestions="shown.aiSuggestions"
:previous="shown.previousCases"
compact
/>
</template>
@@ -512,8 +529,9 @@ const canRetry = computed(
background: v-bind("theme.cardColor");
}
/* 图占大头(设计稿 FlowNotePane):42% 时蜜橘分拣这种宽图只剩三成大,框里的字看不清 */
.graph-box {
flex: 0 0 42%;
flex: 0 0 62%;
min-width: 0;
display: flex;
flex-direction: column;
@@ -19,6 +19,8 @@ const props = defineProps<{
report: FlowchartCaseReport
feedback: string | null
suggestions: string | null
/** 上一次走对了几组(详情接口的 previousCases) */
previous?: { passed: number; total: number } | null
/** 提交列表右栏窄:字小一号 */
compact?: boolean
}>()
@@ -40,6 +42,24 @@ const better = computed(() => (passed.value ? (props.suggestions?.trim() ?? "")
/** 收起时连点开的那一组一起收掉(画布回到只标要改的框)。先算好再写:外面接着 v-model 时,
* 写完马上读 open.value 读到的还是旧值 */
/**
* 「比上次多对了 n 组」:只在这次没画对时说(画对了标题已经够了),一样多不说;
* 两次的组数不一样(老师中途改了例子)就没法比,也不说
*/
const compare = computed(() => {
const previous = props.previous
if (passed.value || !previous || previous.total !== props.report.total) return null
const diff = props.report.passed - previous.passed
if (diff > 0)
return { up: true, text: `比上次多对了 ${diff} 组${props.compact ? "" : ",接着改"}` }
if (diff < 0)
return {
up: false,
text: `比上次少对了 ${-diff} 组${props.compact ? "" : ",看看刚才改了哪里"}`,
}
return null
})
function toggle() {
const next = !open.value
open.value = next
@@ -49,6 +69,28 @@ function toggle() {
<template>
<div class="flowchart-note" :class="{ compact }">
<!-- 完成度:一组一段,走对几组亮几段(不写「组」,学生不关心是几组数据) -->
<div class="progress">
<div
class="segments"
role="img"
:aria-label="`完成度:${report.total} 段亮了 ${report.passed} 段`"
>
<!-- 从左往右亮,是条进度,不按哪一组对哪一组错排 -->
<span
v-for="index in report.total"
:key="index"
:class="{ lit: index <= report.passed }"
></span>
</div>
<span
v-if="compare"
class="compare"
:style="{ color: tone(compare.up ? 'success' : 'warning').color }"
>
{{ compare.text }}
</span>
</div>
<div v-if="wrong || fix || better" class="card">
<div v-if="wrong">
<div class="label" :style="{ color: tone('error').color }">
@@ -85,6 +127,46 @@ function toggle() {
gap: 12px;
}
.progress {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 12px;
}
.segments {
display: flex;
gap: 4px;
}
.segments span {
width: 36px;
height: 8px;
border-radius: 4px;
background: v-bind("theme.dividerColor");
}
.segments span.lit {
background: v-bind("theme.successColor");
}
.compact .segments {
gap: 3px;
}
.compact .segments span {
width: 22px;
height: 7px;
}
.compare {
font-size: 14px;
}
.compact .compare {
font-size: 13px;
}
.card {
display: flex;
flex-direction: column;
@@ -96,7 +178,7 @@ function toggle() {
.compact .card {
gap: 12px;
padding: 12px 14px;
padding: 10px 12px;
}
.label {
@@ -120,8 +202,8 @@ function toggle() {
.compact .card p {
margin-top: 4px;
font-size: 15px;
line-height: 1.7;
font-size: 14px;
line-height: 1.65;
}
.toggle {
+11
View File
@@ -30,6 +30,17 @@ export function caseReport(details: Record<string, unknown> | null | undefined)
} satisfies FlowchartCaseReport
}
/**
* 标题那句话(设计稿 FlowNote*)。等级照旧存 S/A/B/C,学生看到的是话:全对时 S「画得很清楚」、
* A「画对了」;没全对按走对几组分三档,C 里一组都没对和对了几组分开说 —— 一直是「还没画对」
* 会让人泄气
*/
export function flowchartVerdict(report: FlowchartCaseReport, grade: string | null) {
if (report.passed === report.total) return grade === "S" ? "画对了,画得很清楚" : "画对了"
if (report.passed * 2 >= report.total) return "快画对了"
return report.passed > 0 ? "对了一部分" : "还没画对"
}
/** 「6 组走对 3 组」「6 组全部走对」 */
export function caseHeadline(report: FlowchartCaseReport) {
return report.passed === report.total
+8
View File
@@ -40,6 +40,14 @@ export const flowchartSubmissionSchema = z.object({
aiModel: z.string(),
processingTime: z.number().nullable(),
evaluationTime: z.string().nullable(),
/**
* 同一个人同一道题的上一次(评完了的)走对了几组,给「比上次多对了 n 组」用。
* 上一次不是走例子的评法(2026-10 之前的四项分)、或者没有上一次,给 null
*/
previousCases: z
.object({ passed: z.number().int(), total: z.number().int() })
.nullable()
.optional(),
})
export const flowchartListItemSchema = z.object({