diff --git a/apps/api/src/routes/flowchart.ts b/apps/api/src/routes/flowchart.ts
index b6e6156..6149dfa 100644
--- a/apps/api/src/routes/flowchart.ts
+++ b/apps/api/src/routes/flowchart.ts
@@ -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) => {
diff --git a/apps/web/src/oj/problem/components/FlowchartResult.vue b/apps/web/src/oj/problem/components/FlowchartResult.vue
index df712dc..7690b45 100644
--- a/apps/web/src/oj/problem/components/FlowchartResult.vue
+++ b/apps/web/src/oj/problem/components/FlowchartResult.vue
@@ -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() {
{{ current.grade }} 级
- {{
- report.passed === report.total ? "画对了" : "还没画对"
- }}
+ {{ flowchartVerdict(report, current.grade) }}
这道题算做完了
@@ -287,6 +285,7 @@ function goCode() {
:report="report"
:feedback="detail.aiFeedback"
:suggestions="detail.aiSuggestions"
+ :previous="detail.previousCases"
class="block"
/>
diff --git a/apps/web/src/oj/submission/components/FlowchartPane.vue b/apps/web/src/oj/submission/components/FlowchartPane.vue
index 12fbbaa..db1c42f 100644
--- a/apps/web/src/oj/submission/components/FlowchartPane.vue
+++ b/apps/web/src/oj/submission/components/FlowchartPane.vue
@@ -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 的框号放在节点
的 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(null)
/** 逐组数据默认收着,老师和学生一样(设计稿 FlowNotePane);翻到下一张收回去 */
const casesOpen = ref(false)
@@ -275,13 +291,14 @@ const canRetry = computed(
这次评分失败了{{ teacher ? ",可以点「重新评分」" : "" }}
- {{ allPassed ? "画对了" : "还没画对" }}
+ {{ flowchartVerdict(report, shown.aiGrade) }}
@@ -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;
diff --git a/apps/web/src/shared/components/FlowchartNote.vue b/apps/web/src/shared/components/FlowchartNote.vue
index 4ada4fd..594d777 100644
--- a/apps/web/src/shared/components/FlowchartNote.vue
+++ b/apps/web/src/shared/components/FlowchartNote.vue
@@ -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() {
+
+
+
+
+
+
+
+ {{ compare.text }}
+
+
@@ -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 {
diff --git a/apps/web/src/utils/flowchart.ts b/apps/web/src/utils/flowchart.ts
index c826aa5..a993eb9 100644
--- a/apps/web/src/utils/flowchart.ts
+++ b/apps/web/src/utils/flowchart.ts
@@ -30,6 +30,17 @@ export function caseReport(details: Record | 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
diff --git a/packages/contract/src/flowchart.ts b/packages/contract/src/flowchart.ts
index 1703806..3df523a 100644
--- a/packages/contract/src/flowchart.ts
+++ b/packages/contract/src/flowchart.ts
@@ -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({