Compare commits
4
Commits
e4905fcec1
...
6e946c99fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e946c99fb | ||
|
|
b9ece7d38f | ||
|
|
04c90e3f89 | ||
|
|
6b7d87d677 |
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
flowToMermaid,
|
||||||
|
parseFlowDoc,
|
||||||
addContestProblemRequestSchema,
|
addContestProblemRequestSchema,
|
||||||
reorderContestProblemsRequestSchema,
|
reorderContestProblemsRequestSchema,
|
||||||
astCheckRequestSchema,
|
astCheckRequestSchema,
|
||||||
@@ -206,6 +208,7 @@ async function serialize(row: ProblemRow) {
|
|||||||
allowFlowchart: row.allowFlowchart,
|
allowFlowchart: row.allowFlowchart,
|
||||||
showFlowchart: row.showFlowchart,
|
showFlowchart: row.showFlowchart,
|
||||||
mermaidCode: row.mermaidCode,
|
mermaidCode: row.mermaidCode,
|
||||||
|
flowchartData: asRecord(row.flowchartData),
|
||||||
flowchartHint: row.flowchartHint,
|
flowchartHint: row.flowchartHint,
|
||||||
astRules: row.astRules,
|
astRules: row.astRules,
|
||||||
answers: Array.isArray(row.answers) ? row.answers : [],
|
answers: Array.isArray(row.answers) ? row.answers : [],
|
||||||
@@ -276,6 +279,17 @@ async function generateSqlDisplay(
|
|||||||
return { display: outcome.value }
|
return { display: outcome.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考流程图:画布画的(结构树)以它为准,Mermaid 照它生成 —— 题面里给学生看的、评分退回
|
||||||
|
* 老办法时拿来比的都是这份,和学生交的图同一套形状。只有手写 Mermaid 的老题原样存
|
||||||
|
*/
|
||||||
|
function referenceFlowchart(raw: Record<string, unknown> | null, mermaidCode: string | null) {
|
||||||
|
const doc = parseFlowDoc(raw)
|
||||||
|
return doc && doc.steps.length
|
||||||
|
? { flowchartData: doc, mermaidCode: flowToMermaid(doc) }
|
||||||
|
: { flowchartData: {}, mermaidCode }
|
||||||
|
}
|
||||||
|
|
||||||
function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>, isSql: boolean) {
|
function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>, isSql: boolean) {
|
||||||
return {
|
return {
|
||||||
displayId: data._id,
|
displayId: data._id,
|
||||||
@@ -296,7 +310,7 @@ function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>
|
|||||||
source: data.source,
|
source: data.source,
|
||||||
allowFlowchart: data.allowFlowchart,
|
allowFlowchart: data.allowFlowchart,
|
||||||
showFlowchart: data.showFlowchart,
|
showFlowchart: data.showFlowchart,
|
||||||
mermaidCode: data.mermaidCode,
|
...referenceFlowchart(data.flowchartData, data.mermaidCode),
|
||||||
flowchartHint: data.flowchartHint,
|
flowchartHint: data.flowchartHint,
|
||||||
astRules: pickAstRules(data.astRules, data.languages),
|
astRules: pickAstRules(data.astRules, data.languages),
|
||||||
answers: data.answers,
|
answers: data.answers,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
parseFlowDoc,
|
||||||
batchProblemTagRequestSchema,
|
batchProblemTagRequestSchema,
|
||||||
generateFlowchartRequestSchema,
|
generateFlowchartRequestSchema,
|
||||||
renameTagRequestSchema,
|
renameTagRequestSchema,
|
||||||
@@ -16,7 +17,7 @@ import type { AuthUser } from "../../auth/session"
|
|||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, parseBody, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { completeChat } from "../../services/ai"
|
import { completeChat } from "../../services/ai"
|
||||||
import { queryInteger } from "../helpers"
|
import { asRecord, queryInteger } from "../helpers"
|
||||||
import { canEdit, findTagsByName, normalizeTagNames } from "./problem"
|
import { canEdit, findTagsByName, normalizeTagNames } from "./problem"
|
||||||
|
|
||||||
export const adminTagRoutes = new Hono<AppEnv>()
|
export const adminTagRoutes = new Hono<AppEnv>()
|
||||||
@@ -254,18 +255,32 @@ adminTagRoutes.put("/problems/:id/visibility", requireProblemPermission, async (
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- Python → 流程图
|
// ---------------------------------------------------------------- Python → 流程图
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 照标准答案画参考流程图。原来让 AI 直接写 Mermaid,老师拿到的是一段代码,要改只能改代码;
|
||||||
|
* 现在出结构树,后台画布直接摆上去,和学生画图是同一个编辑器
|
||||||
|
*/
|
||||||
|
const PYTHON_TO_FLOWCHART = `把下面这段 Python 标准答案画成流程图,给中职学生看。只返回 JSON:{"steps":[…]}
|
||||||
|
每一步是下面五种之一(开始、结束不用写):
|
||||||
|
{"kind":"input","text":"输入 n"}
|
||||||
|
{"kind":"output","text":"输出 a(保留两位小数)"}
|
||||||
|
{"kind":"process","text":"i = i + 1"}
|
||||||
|
{"kind":"if","text":"a > b","yes":[…],"no":[…]}
|
||||||
|
{"kind":"while","text":"i < 365","body":[…]}
|
||||||
|
- 框里写中文和简单的式子,像课本上那样;不要写 input、print、int、range 这些函数名
|
||||||
|
- for 循环改成计数变量 + while:先一个处理框给变量赋初值,while 写条件,循环体最后一个处理框让变量加一
|
||||||
|
- elif 写成 else 里再套一个 if
|
||||||
|
- break、continue、函数定义尽量改写成等价的顺序 / 判断 / 循环;实在改不了就用一个处理框写一句说明`
|
||||||
|
|
||||||
adminTagRoutes.post("/problems/flowchart", requireProblemPermission, async (c) => {
|
adminTagRoutes.post("/problems/flowchart", requireProblemPermission, async (c) => {
|
||||||
const parsed = await parseBody(c, generateFlowchartRequestSchema, "python 代码不能为空")
|
const parsed = await parseBody(c, generateFlowchartRequestSchema, "python 代码不能为空")
|
||||||
if (!parsed.success) return parsed.response
|
if (!parsed.success) return parsed.response
|
||||||
try {
|
try {
|
||||||
const flowchart = await completeChat(
|
const reply = asRecord(
|
||||||
`你是一个可以将Python代码转换为mermaid的助手。
|
JSON.parse(await completeChat(PYTHON_TO_FLOWCHART, parsed.data.python, { json: true })),
|
||||||
请将用户提供的Python代码转换为 Mermaid 纯文本。
|
|
||||||
注意括号内的内容用引号包裹,如果本身就有引号,请注意双引号和单引号的问题。
|
|
||||||
请只返回 mermaid 代码,连 \`\`\` 都不需要。`,
|
|
||||||
parsed.data.python,
|
|
||||||
)
|
)
|
||||||
return success(c, { flowchart } satisfies GenerateFlowchartResponse)
|
const doc = parseFlowDoc({ version: 2, steps: reply.steps })
|
||||||
|
if (!doc || !doc.steps.length) throw new Error("AI returned no flowchart steps")
|
||||||
|
return success(c, { doc: { ...doc } } satisfies GenerateFlowchartResponse)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Flowchart generation failed", error)
|
console.error("Flowchart generation failed", error)
|
||||||
return failure(c, 502, "ai-unavailable", "生成失败,请稍后再试")
|
return failure(c, 502, "ai-unavailable", "生成失败,请稍后再试")
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
createFlowchartRequestSchema,
|
createFlowchartRequestSchema,
|
||||||
isFlowchartPass,
|
isFlowchartPass,
|
||||||
type CreateFlowchartResponse,
|
type CreateFlowchartResponse,
|
||||||
type FlowchartCurrent,
|
|
||||||
type FlowchartDetail,
|
|
||||||
type FlowchartScores,
|
type FlowchartScores,
|
||||||
type FlowchartList,
|
type FlowchartList,
|
||||||
type FlowchartListItem,
|
type FlowchartListItem,
|
||||||
@@ -612,6 +610,7 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
|||||||
personCount: roster.length,
|
personCount: roster.length,
|
||||||
completedCount: 0,
|
completedCount: 0,
|
||||||
wordFrequencies: [],
|
wordFrequencies: [],
|
||||||
|
commonMistakes: [],
|
||||||
// 一条提交都没有时,花名册上的人**全都**是「没做」—— 原来这里写死空数组,
|
// 一条提交都没有时,花名册上的人**全都**是「没做」—— 原来这里写死空数组,
|
||||||
// 于是一节课刚开始、最该点名的时候,教师面板反而一个名字都不给
|
// 于是一节课刚开始、最该点名的时候,教师面板反而一个名字都不给
|
||||||
dataUnaccepted: roster.map((row) => ({
|
dataUnaccepted: roster.map((row) => ({
|
||||||
@@ -647,8 +646,16 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
|||||||
const pushText = (value: string) => {
|
const pushText = (value: string) => {
|
||||||
if (texts.length < WORDCLOUD_TEXT_LIMIT) texts.push(value)
|
if (texts.length < WORDCLOUD_TEXT_LIMIT) texts.push(value)
|
||||||
}
|
}
|
||||||
|
const mistakes = new Map<string, number>()
|
||||||
for (const row of textRows) {
|
for (const row of textRows) {
|
||||||
for (const [key, value] of Object.entries(asRecord(row.criteria))) {
|
const report = asRecord(row.criteria)
|
||||||
|
if (report.version === 2 && Array.isArray(report.cases)) {
|
||||||
|
const first = report.cases.map((item) => asRecord(item)).find((item) => item.ok === false)
|
||||||
|
if (typeof first?.why === "string" && first.why) {
|
||||||
|
mistakes.set(first.why, (mistakes.get(first.why) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(report)) {
|
||||||
const detail = asRecord(value)
|
const detail = asRecord(value)
|
||||||
// 和上面那条聚合同一道闸:分数不是数字的项当没配过,满分和评语也都不收
|
// 和上面那条聚合同一道闸:分数不是数字的项当没配过,满分和评语也都不收
|
||||||
if (typeof detail.score !== "number") continue
|
if (typeof detail.score !== "number") continue
|
||||||
@@ -678,6 +685,10 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
|||||||
personCount: roster.length,
|
personCount: roster.length,
|
||||||
completedCount: totals.completedCount,
|
completedCount: totals.completedCount,
|
||||||
wordFrequencies: await buildWordFrequencies(texts),
|
wordFrequencies: await buildWordFrequencies(texts),
|
||||||
|
commonMistakes: [...mistakes]
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(([text, n]) => ({ text, count: n })),
|
||||||
dataUnaccepted: roster
|
dataUnaccepted: roster
|
||||||
.filter((row) => !submitted.has(row.id))
|
.filter((row) => !submitted.has(row.id))
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
@@ -777,10 +788,10 @@ flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 自己在这道题上评完的每一次,按时间从早到晚,已经滤掉加入题单之前的(题单闸门)。
|
* 自己在这道题上评完的每一次,按时间从早到晚,已经滤掉加入题单之前的(题单闸门)。
|
||||||
* current / history / scores 三个接口都从这一份出。
|
* 结果页签的历次分数(scores)从这一份出。
|
||||||
*
|
*
|
||||||
* 只取分数和闸门要的几列:整行带着画布 JSON 和 mermaid 源码(一条约 5KB),画了几十次
|
* 只取分数和闸门要的几列:整行带着画布 JSON 和 mermaid 源码(一条约 5KB),画了几十次
|
||||||
* 的学生每切一次题就要搬几百 KB,而结果页签只要分数。要整行的 history 自己按 id 再取。
|
* 的学生每切一次题就要搬几百 KB,而结果页签只要分数。
|
||||||
* userId / problemId / createTime 三列是 hiddenByProblemSet 要的,别删。
|
* userId / problemId / createTime 三列是 hiddenByProblemSet 要的,别删。
|
||||||
*/
|
*/
|
||||||
async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
||||||
@@ -809,46 +820,9 @@ async function myEvaluatedFlowcharts(user: AuthUser, problemId: number) {
|
|||||||
return { visible, hidden: rows.length - visible.length }
|
return { visible, hidden: rows.length - visible.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* current / history:前端从 2026-09-28 起改用下面的 scores + GET /flowcharts/:id,不再调这两个。
|
|
||||||
* 先留着给上线那一刻还开着旧页面的浏览器(机房里一节课都不刷新是常态),下一次清理时删掉,
|
|
||||||
* 连同契约里的 flowchartCurrentSchema / flowchartDetailSchema。
|
|
||||||
*/
|
|
||||||
flowchartRoutes.get("/problems/:id/flowchart/current", requireAuth, async (c) => {
|
|
||||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
|
||||||
const { visible } = await myEvaluatedFlowcharts(c.get("user")!, problemId)
|
|
||||||
const latest = visible.at(-1)
|
|
||||||
return success(c, {
|
|
||||||
count: visible.length,
|
|
||||||
score: latest?.aiScore ?? 0,
|
|
||||||
grade: latest?.aiGrade ?? "",
|
|
||||||
} satisfies FlowchartCurrent)
|
|
||||||
})
|
|
||||||
|
|
||||||
flowchartRoutes.get("/problems/:id/flowchart/history", requireAuth, async (c) => {
|
|
||||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
|
||||||
const page = queryInteger(c.req.query("page"), 0, { min: 0 })
|
|
||||||
const user = c.get("user")!
|
|
||||||
const { visible: rows } = await myEvaluatedFlowcharts(user, problemId)
|
|
||||||
const selected = page === 0 ? rows.at(-1) : rows[page - 1]
|
|
||||||
if (page > rows.length) return failure(c, 400, "page-out-of-range", "Page out of range")
|
|
||||||
// 列表只带了分数,选中的那一条再按 id 取整行。都是本人的提交,用户名就是自己的
|
|
||||||
const [full] = selected
|
|
||||||
? await db
|
|
||||||
.select()
|
|
||||||
.from(schema.flowchartSubmission)
|
|
||||||
.where(eq(schema.flowchartSubmission.id, selected.id))
|
|
||||||
.limit(1)
|
|
||||||
: []
|
|
||||||
return success(c, {
|
|
||||||
submission: full ? flowchartData(full, user.username) : null,
|
|
||||||
count: rows.length,
|
|
||||||
} satisfies FlowchartDetail)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 历次分数:结果页签上那一排「第 N 次 xx 分」。原来 history 一次只给一条,
|
* 历次分数:结果页签上那一排「C → B → A」。原来的 history 一次只给一条,
|
||||||
* 要看分数走势得一页一页翻。`hidden` 是被题单闸门藏起来的次数,界面上给一句说明。
|
* 要看走势得一页一页翻(它和 current 已经删了)。`hidden` 是被题单闸门藏起来的次数,界面上给一句说明。
|
||||||
*/
|
*/
|
||||||
flowchartRoutes.get("/problems/:id/flowchart/scores", requireAuth, async (c) => {
|
flowchartRoutes.get("/problems/:id/flowchart/scores", requireAuth, async (c) => {
|
||||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function toggleProblemVisible(problemID: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateFlowchartFromPythonCode(python: string) {
|
export function generateFlowchartFromPythonCode(python: string) {
|
||||||
return api.post<{ flowchart: string }>("admin/problems/flowchart", {
|
return api.post<{ doc: Record<string, unknown> }>("admin/problems/flowchart", {
|
||||||
python,
|
python,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,9 @@ export function generateTestInputs(data: GenerateTestInputsRequest) {
|
|||||||
|
|
||||||
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
||||||
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
||||||
const p = problem as Partial<AdminProblem>
|
const p = problem as Partial<Omit<AdminProblem, "flowchartData">> & {
|
||||||
|
flowchartData?: Record<string, unknown> | null
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
_id: p._id,
|
_id: p._id,
|
||||||
title: p.title,
|
title: p.title,
|
||||||
@@ -300,6 +302,7 @@ function toProblemBody(problem: AdminProblem | BlankProblem) {
|
|||||||
allowFlowchart: p.allowFlowchart ?? false,
|
allowFlowchart: p.allowFlowchart ?? false,
|
||||||
showFlowchart: p.showFlowchart ?? false,
|
showFlowchart: p.showFlowchart ?? false,
|
||||||
mermaidCode: p.mermaidCode ?? null,
|
mermaidCode: p.mermaidCode ?? null,
|
||||||
|
flowchartData: p.flowchartData ?? null,
|
||||||
flowchartHint: p.flowchartHint ?? null,
|
flowchartHint: p.flowchartHint ?? null,
|
||||||
astRules: p.astRules ?? null,
|
astRules: p.astRules ?? null,
|
||||||
sqlConfig: p.sqlConfig ?? null,
|
sqlConfig: p.sqlConfig ?? null,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const props = defineProps<{
|
|||||||
astChecks: AstCheckState
|
astChecks: AstCheckState
|
||||||
flowchartMode: FlowchartMode
|
flowchartMode: FlowchartMode
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
flowchartHint: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tab = defineModel<PreviewTab>("tab", { required: true })
|
const tab = defineModel<PreviewTab>("tab", { required: true })
|
||||||
@@ -248,18 +247,15 @@ onMounted(() => nextTick(render))
|
|||||||
<!-- 流程图:一直挂着,改了图的代码随时知道画不画得出来 -->
|
<!-- 流程图:一直挂着,改了图的代码随时知道画不画得出来 -->
|
||||||
<div v-show="tab === 'flowchart'" class="page">
|
<div v-show="tab === 'flowchart'" class="page">
|
||||||
<p v-if="flowchartMode === 'none'" class="placeholder">这道题不用流程图</p>
|
<p v-if="flowchartMode === 'none'" class="placeholder">这道题不用流程图</p>
|
||||||
<p v-else-if="flowchartMode === 'draw'" class="muted small">
|
<p v-else-if="flowchartMode === 'draw'" class="placeholder">
|
||||||
学生自己画,这张图<b>学生看不到</b>,只拿来给学生画的图打分。
|
学生在题目页右边自己画,交上去按例子和测试数据评
|
||||||
<template v-if="flowchartHint.trim()">学生看到的提示:{{ flowchartHint }}</template>
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="muted small">学生在题面里点开「老师给的参考流程图」看到的是:</p>
|
<template v-else>
|
||||||
<p v-if="flowchartMode !== 'none' && !mermaidCode.trim()" class="placeholder">
|
<p class="muted small">学生在题面里点开「老师给的参考流程图」看到的是:</p>
|
||||||
还没写图的代码
|
<p v-if="!mermaidCode.trim()" class="placeholder">还没画</p>
|
||||||
</p>
|
<div v-if="renderError" class="chartError">画不出来:{{ renderError }}</div>
|
||||||
<div v-if="renderError && flowchartMode !== 'none'" class="chartError">
|
</template>
|
||||||
画不出来:{{ renderError }}
|
<div v-show="flowchartMode === 'show'" ref="chart" class="chart"></div>
|
||||||
</div>
|
|
||||||
<div v-show="flowchartMode !== 'none'" ref="chart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,8 +42,17 @@ import {
|
|||||||
type TemplateParts,
|
type TemplateParts,
|
||||||
} from "./components/codeTemplate"
|
} from "./components/codeTemplate"
|
||||||
import type { AstCheckState, FlowchartMode, PreviewTab } from "./components/editorTypes"
|
import type { AstCheckState, FlowchartMode, PreviewTab } from "./components/editorTypes"
|
||||||
|
import {
|
||||||
|
emptyDoc,
|
||||||
|
parseDoc,
|
||||||
|
toMermaid,
|
||||||
|
type FlowDoc,
|
||||||
|
} from "shared/components/FlowchartEditor/model"
|
||||||
|
|
||||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||||
|
const FlowchartEditor = defineAsyncComponent(
|
||||||
|
() => import("shared/components/FlowchartEditor/index.vue"),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编程题(C / C++ / Python)的出题页,设计稿「编程题出题页」方案 B:左边写,右边是学生
|
* 编程题(C / C++ / Python)的出题页,设计稿「编程题出题页」方案 B:左边写,右边是学生
|
||||||
@@ -93,6 +102,8 @@ interface CodeForm {
|
|||||||
astRules: AstRules | null
|
astRules: AstRules | null
|
||||||
flowchartMode: FlowchartMode
|
flowchartMode: FlowchartMode
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
|
/** 画布画的参考流程图;null 是老题(只有手写的 Mermaid)或者还没画 */
|
||||||
|
flowchartDoc: FlowDoc | null
|
||||||
flowchartHint: string
|
flowchartHint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +129,7 @@ const blankForm = (): CodeForm => ({
|
|||||||
astRules: null,
|
astRules: null,
|
||||||
flowchartMode: "none",
|
flowchartMode: "none",
|
||||||
mermaidCode: "",
|
mermaidCode: "",
|
||||||
|
flowchartDoc: null,
|
||||||
flowchartHint: "",
|
flowchartHint: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -437,13 +449,26 @@ const FLOWCHART_LABEL: Record<FlowchartMode, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mermaidOk = ref(false)
|
const mermaidOk = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 老题的参考图是手写的 Mermaid(没有结构树):照旧给代码框;新画的、或者点了「改用画布画」
|
||||||
|
* 的用画布。保存时画布的图以它为准,后端照它重新生成 Mermaid
|
||||||
|
*/
|
||||||
|
const legacyMermaid = computed(() => !form.flowchartDoc && !!form.mermaidCode.trim())
|
||||||
|
/** 右边预览画的那张:画布有图就照画布生成,不然是老的代码 */
|
||||||
|
const previewMermaid = computed(() => {
|
||||||
|
if (!form.flowchartDoc) return form.mermaidCode
|
||||||
|
return form.flowchartDoc.steps.length ? toMermaid(form.flowchartDoc) : ""
|
||||||
|
})
|
||||||
const aiDrawing = ref(false)
|
const aiDrawing = ref(false)
|
||||||
|
|
||||||
async function aiFlowchart() {
|
async function aiFlowchart() {
|
||||||
aiDrawing.value = true
|
aiDrawing.value = true
|
||||||
try {
|
try {
|
||||||
const res = await generateFlowchartFromPythonCode(form.answers.Python)
|
const res = await generateFlowchartFromPythonCode(form.answers.Python)
|
||||||
form.mermaidCode = res.flowchart
|
const doc = parseDoc(res.doc)
|
||||||
|
if (!doc) throw new Error("AI 画出来的图读不了")
|
||||||
|
form.flowchartDoc = doc
|
||||||
preview.value = "flowchart"
|
preview.value = "flowchart"
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error(errorMessage(err, "生成失败,请稍后再试"))
|
message.error(errorMessage(err, "生成失败,请稍后再试"))
|
||||||
@@ -599,6 +624,7 @@ async function loadProblem(data: AdminProblem) {
|
|||||||
astRules: data.astRules ?? null,
|
astRules: data.astRules ?? null,
|
||||||
flowchartMode: data.allowFlowchart ? "draw" : data.showFlowchart ? "show" : "none",
|
flowchartMode: data.allowFlowchart ? "draw" : data.showFlowchart ? "show" : "none",
|
||||||
mermaidCode: data.mermaidCode ?? "",
|
mermaidCode: data.mermaidCode ?? "",
|
||||||
|
flowchartDoc: parseDoc(data.flowchartData),
|
||||||
flowchartHint: data.flowchartHint ?? "",
|
flowchartHint: data.flowchartHint ?? "",
|
||||||
} satisfies CodeForm)
|
} satisfies CodeForm)
|
||||||
answerTab.value = RUNNABLE.find((l) => languages.includes(l) && answers[l].trim()) ?? "Python"
|
answerTab.value = RUNNABLE.find((l) => languages.includes(l) && answers[l].trim()) ?? "Python"
|
||||||
@@ -703,10 +729,17 @@ const blockers = computed<Blocker[]>(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (form.flowchartMode !== "none") {
|
if (form.flowchartMode === "show") {
|
||||||
if (!form.mermaidCode.trim()) list.push({ text: "写流程图的代码", section: "flowchart" })
|
if (legacyMermaid.value) {
|
||||||
else if (!mermaidOk.value)
|
if (!mermaidOk.value)
|
||||||
list.push({ text: "改好流程图的代码(画不出来)", section: "flowchart", severe: true })
|
list.push({ text: "改好流程图的代码(画不出来)", section: "flowchart", severe: true })
|
||||||
|
} else if (!form.flowchartDoc?.steps.length) {
|
||||||
|
list.push({ text: "画参考流程图", section: "flowchart" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 学生画的图拼成程序拿例子和测试数据跑(flowchart/program.ts),一组例子都没有就没得评
|
||||||
|
if (form.flowchartMode === "draw" && !examples.value.length) {
|
||||||
|
list.push({ text: "勾至少一组测试数据当例子(流程图照例子评)", section: "cases" })
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
@@ -775,6 +808,10 @@ async function save() {
|
|||||||
allowFlowchart: form.flowchartMode === "draw",
|
allowFlowchart: form.flowchartMode === "draw",
|
||||||
showFlowchart: form.flowchartMode === "show",
|
showFlowchart: form.flowchartMode === "show",
|
||||||
mermaidCode: form.flowchartMode === "none" ? form.mermaidCode : form.mermaidCode.trim(),
|
mermaidCode: form.flowchartMode === "none" ? form.mermaidCode : form.mermaidCode.trim(),
|
||||||
|
flowchartData:
|
||||||
|
form.flowchartMode === "show" && form.flowchartDoc?.steps.length
|
||||||
|
? { ...form.flowchartDoc }
|
||||||
|
: null,
|
||||||
flowchartHint: form.flowchartHint.trim(),
|
flowchartHint: form.flowchartHint.trim(),
|
||||||
astRules: form.astRules,
|
astRules: form.astRules,
|
||||||
sqlConfig: null,
|
sqlConfig: null,
|
||||||
@@ -1196,15 +1233,27 @@ onMounted(async () => {
|
|||||||
<label class="mode" :class="{ on: form.flowchartMode === 'draw' }">
|
<label class="mode" :class="{ on: form.flowchartMode === 'draw' }">
|
||||||
<n-radio value="draw" />
|
<n-radio value="draw" />
|
||||||
<span>
|
<span>
|
||||||
<b>让学生自己画,按这张图打分</b>
|
<b>让学生自己画</b>
|
||||||
<small>这张图学生看不到,只拿来给学生画的图打分;画到 A / S 级算做完</small>
|
<small>按例子和测试数据评;画到 A / S 级算做完</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
<template v-if="form.flowchartMode !== 'none'">
|
<!-- 学生画:按例子和测试数据评(拼成程序跑),不再需要参考图 -->
|
||||||
|
<n-text v-if="form.flowchartMode === 'draw'" depth="3" class="note">
|
||||||
|
学生画的图会拼成程序,拿这道题的例子和测试数据跑一遍,逐组看输出对不对,不用准备参考图。
|
||||||
|
</n-text>
|
||||||
|
<template v-if="form.flowchartMode === 'show'">
|
||||||
<div class="inlineRow">
|
<div class="inlineRow">
|
||||||
<span class="label">图的代码(Mermaid)</span>
|
<span class="label">{{ legacyMermaid ? "图的代码(Mermaid)" : "参考流程图" }}</span>
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
|
<n-button
|
||||||
|
v-if="legacyMermaid"
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
@click="form.flowchartDoc = emptyDoc()"
|
||||||
|
>
|
||||||
|
改用画布画
|
||||||
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:loading="aiDrawing"
|
:loading="aiDrawing"
|
||||||
@@ -1215,25 +1264,20 @@ onMounted(async () => {
|
|||||||
AI 照 Python 答案画一张
|
AI 照 Python 答案画一张
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<n-input
|
<template v-if="legacyMermaid">
|
||||||
v-model:value="form.mermaidCode"
|
<n-input
|
||||||
type="textarea"
|
v-model:value="form.mermaidCode"
|
||||||
class="mono"
|
type="textarea"
|
||||||
:autosize="{ minRows: 6, maxRows: 18 }"
|
class="mono"
|
||||||
placeholder="graph TD A[开始] --> B[输入 n]"
|
:autosize="{ minRows: 6, maxRows: 18 }"
|
||||||
/>
|
/>
|
||||||
<n-text
|
<n-text :type="mermaidOk ? 'success' : 'error'" class="note">
|
||||||
v-if="form.mermaidCode.trim()"
|
{{ mermaidOk ? "✓ 画得出来(右边就是)" : "✗ 还画不出来,看右边的报错" }}
|
||||||
:type="mermaidOk ? 'success' : 'error'"
|
</n-text>
|
||||||
class="note"
|
</template>
|
||||||
>
|
<div v-else class="flowCanvas">
|
||||||
{{ mermaidOk ? "✓ 画得出来(右边就是)" : "✗ 还画不出来,看右边的报错" }}
|
<FlowchartEditor v-model:value="form.flowchartDoc" :draft="false" height="460px" />
|
||||||
</n-text>
|
</div>
|
||||||
<n-input
|
|
||||||
v-if="form.flowchartMode === 'draw'"
|
|
||||||
v-model:value="form.flowchartHint"
|
|
||||||
placeholder="给学生的提示(选填),比如:先判断门是不是开着"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
@@ -1326,8 +1370,7 @@ onMounted(async () => {
|
|||||||
:ast-rules="form.astRules"
|
:ast-rules="form.astRules"
|
||||||
:ast-checks="astChecks"
|
:ast-checks="astChecks"
|
||||||
:flowchart-mode="form.flowchartMode"
|
:flowchart-mode="form.flowchartMode"
|
||||||
:mermaid-code="form.mermaidCode"
|
:mermaid-code="previewMermaid"
|
||||||
:flowchart-hint="form.flowchartHint"
|
|
||||||
@render-state="mermaidOk = $event"
|
@render-state="mermaidOk = $event"
|
||||||
@collapse="previewCollapsed = true"
|
@collapse="previewCollapsed = true"
|
||||||
/>
|
/>
|
||||||
@@ -1658,4 +1701,10 @@ onMounted(async () => {
|
|||||||
.blocker:hover {
|
.blocker:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flowCanvas {
|
||||||
|
border: 1px solid v-bind("theme.borderColor");
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -206,13 +206,13 @@ const changeLanguage = (v: LANGUAGE) => {
|
|||||||
运行例子
|
运行例子
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart'" />
|
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart' && isDesktop" />
|
||||||
|
|
||||||
<SubmitCode v-else-if="isDesktop" />
|
<SubmitCode v-else-if="isDesktop" />
|
||||||
|
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
|
|
||||||
<FlowchartControls v-if="drawing && isDesktop" />
|
<FlowchartControls v-if="drawing" />
|
||||||
|
|
||||||
<n-button
|
<n-button
|
||||||
v-if="showHelpButton"
|
v-if="showHelpButton"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useFlowchartStore } from "oj/store/flowchart"
|
import { useFlowchartStore } from "oj/store/flowchart"
|
||||||
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,6 +12,8 @@ import { parseTime } from "utils/functions"
|
|||||||
*/
|
*/
|
||||||
const { editor, unchanged } = storeToRefs(useFlowchartStore())
|
const { editor, unchanged } = storeToRefs(useFlowchartStore())
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
|
// 手机上工具栏挤,「已保存」那几个字不放
|
||||||
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
const saved = computed(() => {
|
const saved = computed(() => {
|
||||||
// 「交给 AI 点评」灰着的时候说一句为什么(设计稿 FlowResPass)
|
// 「交给 AI 点评」灰着的时候说一句为什么(设计稿 FlowResPass)
|
||||||
@@ -22,7 +25,7 @@ const saved = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="editor" class="flowchart-controls">
|
<div v-if="editor" class="flowchart-controls">
|
||||||
<span v-if="saved" class="saved">{{ saved }}</span>
|
<span v-if="saved && isDesktop" class="saved">{{ saved }}</span>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="!editor.canUndo"
|
:disabled="!editor.canUndo"
|
||||||
aria-label="撤销"
|
aria-label="撤销"
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import { caseHeadline, caseReport } from "utils/flowchart"
|
|||||||
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
||||||
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
||||||
*
|
*
|
||||||
* viewOnly:手机上只能看、不能画(设计文档 5.6),不给「照着它写代码」「载回画布」。
|
|
||||||
*/
|
*/
|
||||||
const { viewOnly = false } = defineProps<{ viewOnly?: boolean }>()
|
|
||||||
|
|
||||||
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
||||||
|
|
||||||
@@ -154,7 +152,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => [selectedCase.value, report.value, canvasSame.value, editor.value] as const,
|
() => [selectedCase.value, report.value, canvasSame.value, editor.value] as const,
|
||||||
([index, next, same, instance]) => {
|
([index, next, same, instance]) => {
|
||||||
if (!instance || viewOnly) return
|
if (!instance) return
|
||||||
const row = index === null ? undefined : next?.cases[index]
|
const row = index === null ? undefined : next?.cases[index]
|
||||||
if (!row || !same || !versionDoc.value) {
|
if (!row || !same || !versionDoc.value) {
|
||||||
instance.setHighlight(null)
|
instance.setHighlight(null)
|
||||||
@@ -189,11 +187,7 @@ function goCode() {
|
|||||||
<n-empty
|
<n-empty
|
||||||
v-else-if="!scores.length && phase !== 'evaluating'"
|
v-else-if="!scores.length && phase !== 'evaluating'"
|
||||||
class="empty"
|
class="empty"
|
||||||
:description="
|
description="还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里"
|
||||||
viewOnly
|
|
||||||
? '还没交过流程图。流程图要在电脑上画(手机上拖不动),交给 AI 点评之后,评分和建议在这里也看得到'
|
|
||||||
: '还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里'
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
||||||
@@ -273,7 +267,7 @@ function goCode() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="detail">
|
<template v-if="detail">
|
||||||
<div v-if="detail.mermaidCode && (!report || canvasDiffers || viewOnly)" class="version">
|
<div v-if="detail.mermaidCode && (!report || canvasDiffers)" class="version">
|
||||||
<div class="version-head">
|
<div class="version-head">
|
||||||
<span class="version-title">{{ versionTitle }}</span>
|
<span class="version-title">{{ versionTitle }}</span>
|
||||||
<n-button text type="primary" size="small" @click="versionZoom = true">
|
<n-button text type="primary" size="small" @click="versionZoom = true">
|
||||||
@@ -327,7 +321,7 @@ function goCode() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!viewOnly" class="actions">
|
<div class="actions">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="isFlowchartPass(current.grade) && canCode"
|
v-if="isFlowchartPass(current.grade) && canCode"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import SubmitCode from "./SubmitCode.vue"
|
import SubmitCode from "./SubmitCode.vue"
|
||||||
|
|
||||||
|
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手机上屏幕底部固定的「运行例子 / 提交」(设计文档 5.6)。原来这两个按钮在「代码」页签
|
* 手机上屏幕底部固定的「运行例子 / 提交」(设计文档 5.6)。原来这两个按钮在「代码」页签
|
||||||
* 顶上,和语言、更多挤成两行;看着题面想交一次,还得先切回代码页签。
|
* 顶上,和语言、更多挤成两行;看着题面想交一次,还得先切回代码页签。
|
||||||
@@ -41,7 +43,8 @@ const canRunSamples = computed(
|
|||||||
运行例子
|
运行例子
|
||||||
</n-button>
|
</n-button>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<SubmitCode size="large" block />
|
<SubmitFlowchart v-if="codeStore.code.language === 'Flowchart'" size="large" block />
|
||||||
|
<SubmitCode v-else size="large" block />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -191,13 +191,12 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
|
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
|
||||||
// 把旧版本载回去。子组件 SubmitFlowchart 还是从 inject 拿
|
// 把旧版本载回去;「交给 AI 点评」在手机上挂在屏幕底部那条,也从 store 拿
|
||||||
const flowchartStore = useFlowchartStore()
|
const flowchartStore = useFlowchartStore()
|
||||||
watch(flowchartEditorRef, (editor) => flowchartStore.attachEditor(editor ?? null), {
|
watch(flowchartEditorRef, (editor) => flowchartStore.attachEditor(editor ?? null), {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
onBeforeUnmount(() => flowchartStore.attachEditor(null))
|
||||||
provide("flowchartEditorRef", flowchartEditorRef)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { useProblemStore } from "oj/store/problem"
|
|||||||
import { useSubmissionStore } from "oj/store/submission"
|
import { useSubmissionStore } from "oj/store/submission"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import ResultHeader from "./ResultHeader.vue"
|
import ResultHeader from "./ResultHeader.vue"
|
||||||
@@ -33,8 +31,6 @@ const { submission, syntaxErrorInfo, resultSegment, formattedBeforeSubmit } =
|
|||||||
storeToRefs(submissionStore)
|
storeToRefs(submissionStore)
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const codeStore = useCodeStore()
|
const codeStore = useCodeStore()
|
||||||
const ctx = useProblemPageContext()
|
|
||||||
const { isDesktop } = useBreakpoints()
|
|
||||||
|
|
||||||
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
||||||
const teacherCollab = useTeacherCollab()
|
const teacherCollab = useTeacherCollab()
|
||||||
@@ -42,24 +38,7 @@ const teacherCollab = useTeacherCollab()
|
|||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const { samplesRunAt } = submissionStore.trial
|
const { samplesRunAt } = submissionStore.trial
|
||||||
|
|
||||||
/**
|
type Segment = "submit" | "samples" | "custom"
|
||||||
* 手机上流程图题只能看点评、不能画(设计文档 5.6)。画不了,语言也就落不到 Flowchart 上,
|
|
||||||
* 下面的 drawing 永远是假 —— 原来学生在电脑上画过、AI 评过的,手机上一条都看不到。
|
|
||||||
* 这时多给一段「流程图点评」,只读。
|
|
||||||
*/
|
|
||||||
const flowchartReadable = computed(
|
|
||||||
() => !isDesktop.value && !!problem.value?.allowFlowchart && ctx.value.entry !== "contest",
|
|
||||||
)
|
|
||||||
/** 在看「流程图点评」那一段。只在这一页记着:点运行例子、提交时照常切回对应的分段 */
|
|
||||||
const viewingFlowchart = ref(false)
|
|
||||||
watch(
|
|
||||||
() => [submissionStore.resultSeq, problem.value?.id, flowchartReadable.value],
|
|
||||||
() => {
|
|
||||||
viewingFlowchart.value = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
type Segment = "submit" | "samples" | "custom" | "flowchart"
|
|
||||||
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
||||||
{
|
{
|
||||||
value: "submit",
|
value: "submit",
|
||||||
@@ -84,25 +63,16 @@ const drawing = computed(() => codeStore.code.language === "Flowchart")
|
|||||||
// 试跑走判题机,SQL 题不走那里;流程图也没得跑。这两种只留「提交结果」
|
// 试跑走判题机,SQL 题不走那里;流程图也没得跑。这两种只留「提交结果」
|
||||||
const canTrial = computed(() => !problem.value || !isSqlProblem(problem.value))
|
const canTrial = computed(() => !problem.value || !isSqlProblem(problem.value))
|
||||||
|
|
||||||
const segments = computed(() => {
|
const segments = computed(() =>
|
||||||
const list = canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1)
|
canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1),
|
||||||
return flowchartReadable.value
|
)
|
||||||
? [...list, { value: "flowchart" as const, label: "流程图点评", time: "" }]
|
/** 只有一段(SQL 题)就不摆分段了 */
|
||||||
: list
|
|
||||||
})
|
|
||||||
/** 只有一段(SQL 题、又不是手机上的流程图题)就不摆分段了 */
|
|
||||||
const showSegments = computed(() => segments.value.length > 1)
|
const showSegments = computed(() => segments.value.length > 1)
|
||||||
|
|
||||||
function isActive(segment: Segment) {
|
function isActive(segment: Segment) {
|
||||||
if (viewingFlowchart.value) return segment === "flowchart"
|
|
||||||
return resultSegment.value === segment
|
return resultSegment.value === segment
|
||||||
}
|
}
|
||||||
function pick(segment: Segment) {
|
function pick(segment: Segment) {
|
||||||
if (segment === "flowchart") {
|
|
||||||
viewingFlowchart.value = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewingFlowchart.value = false
|
|
||||||
resultSegment.value = segment
|
resultSegment.value = segment
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -124,14 +94,13 @@ function pick(segment: Segment) {
|
|||||||
>
|
>
|
||||||
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="!viewingFlowchart && resultSegment === 'custom'" class="segment-note"
|
<span v-if="resultSegment === 'custom'" class="segment-note"
|
||||||
>自己编数据试试,这里不判对错</span
|
>自己编数据试试,这里不判对错</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FlowchartResult v-if="viewingFlowchart && flowchartReadable" view-only />
|
|
||||||
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
||||||
<div v-show="!viewingFlowchart && (resultSegment === 'submit' || !canTrial)">
|
<div v-show="resultSegment === 'submit' || !canTrial">
|
||||||
<template v-if="syntaxErrorInfo">
|
<template v-if="syntaxErrorInfo">
|
||||||
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
||||||
<CompileErrorExplain :err-info="syntaxErrorInfo" />
|
<CompileErrorExplain :err-info="syntaxErrorInfo" />
|
||||||
@@ -149,7 +118,7 @@ function pick(segment: Segment) {
|
|||||||
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="canTrial && !viewingFlowchart">
|
<template v-if="canTrial">
|
||||||
<SampleRunResult v-if="resultSegment === 'samples'" />
|
<SampleRunResult v-if="resultSegment === 'samples'" />
|
||||||
<CustomRun v-else-if="resultSegment === 'custom'" />
|
<CustomRun v-else-if="resultSegment === 'custom'" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { storeToRefs } from "pinia"
|
|||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { countBlank } from "shared/components/FlowchartEditor/model"
|
import { countBlank } from "shared/components/FlowchartEditor/model"
|
||||||
import { useProblemStore } from "oj/store/problem"
|
import { useProblemStore } from "oj/store/problem"
|
||||||
import { useFlowchartStore, type FlowchartEditorInstance } from "oj/store/flowchart"
|
import { useFlowchartStore } from "oj/store/flowchart"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「交给 AI 点评」按钮。
|
* 「交给 AI 点评」按钮。
|
||||||
@@ -13,16 +13,20 @@ import { useFlowchartStore, type FlowchartEditorInstance } from "oj/store/flowch
|
|||||||
* 弹框盖住画布,学生没法一边看改进建议一边改图。
|
* 弹框盖住画布,学生没法一边看改进建议一边改图。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const flowchartEditorRef = inject<Ref<FlowchartEditorInstance | null>>("flowchartEditorRef")
|
/** 手机上它在屏幕底部那条(MobileActionBar),不在编辑器里面,所以画布从 store 拿 */
|
||||||
|
const { size, block = false } = defineProps<{
|
||||||
|
size?: "small" | "medium" | "large"
|
||||||
|
block?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { problem } = storeToRefs(useProblemStore())
|
const { problem } = storeToRefs(useProblemStore())
|
||||||
const flowchartStore = useFlowchartStore()
|
const flowchartStore = useFlowchartStore()
|
||||||
const { loading, unchanged } = storeToRefs(flowchartStore)
|
const { loading, unchanged, editor: canvas } = storeToRefs(flowchartStore)
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const editor = flowchartEditorRef?.value
|
const editor = canvas.value
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const doc = editor.getDoc()
|
const doc = editor.getDoc()
|
||||||
if (!doc.steps.length) {
|
if (!doc.steps.length) {
|
||||||
@@ -44,7 +48,8 @@ watch(() => problem.value?.id, flowchartStore.ensureLoaded, { immediate: true })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-button
|
<n-button
|
||||||
:size="isDesktop ? 'medium' : 'small'"
|
:size="size ?? (isDesktop ? 'medium' : 'small')"
|
||||||
|
:block="block"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="loading || unchanged"
|
:disabled="loading || unchanged"
|
||||||
|
|||||||
@@ -200,13 +200,11 @@ const people = computed(() => [
|
|||||||
...props.data.dataUnaccepted.map((row) => ({
|
...props.data.dataUnaccepted.map((row) => ({
|
||||||
username: row.username,
|
username: row.username,
|
||||||
grade: null as string | null,
|
grade: null as string | null,
|
||||||
score: null as number | null,
|
|
||||||
count: 0,
|
count: 0,
|
||||||
})),
|
})),
|
||||||
...props.data.people.map((row) => ({
|
...props.data.people.map((row) => ({
|
||||||
username: row.username,
|
username: row.username,
|
||||||
grade: row.bestGrade,
|
grade: row.bestGrade,
|
||||||
score: row.bestScore,
|
|
||||||
count: row.count,
|
count: row.count,
|
||||||
})),
|
})),
|
||||||
])
|
])
|
||||||
@@ -229,7 +227,6 @@ function gradeStyle(grade: string | null) {
|
|||||||
<div class="flow-stats">
|
<div class="flow-stats">
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<StatItem label="提交" :value="data.totalCount" unit="张" />
|
<StatItem label="提交" :value="data.totalCount" unit="张" />
|
||||||
<StatItem label="平均分" :value="data.avgScore" />
|
|
||||||
<StatItem
|
<StatItem
|
||||||
label="拿到 A / S"
|
label="拿到 A / S"
|
||||||
:value="`${goodCount} / ${personCount}`"
|
:value="`${goodCount} / ${personCount}`"
|
||||||
@@ -267,31 +264,54 @@ function gradeStyle(grade: string | null) {
|
|||||||
<Doughnut :key="chartKey" :data="gradeData" :options="gradeOptions" />
|
<Doughnut :key="chartKey" :data="gradeData" :options="gradeOptions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="criteria.length" class="card radar">
|
<template v-if="criteria.length">
|
||||||
<div class="card-title">四项评分(平均得分率)</div>
|
<div class="card radar">
|
||||||
<div class="chart">
|
<div class="card-title">四项评分(平均得分率)</div>
|
||||||
<Radar :key="chartKey" :data="radarData" :options="radarOptions" />
|
<div class="chart">
|
||||||
|
<Radar :key="chartKey" :data="radarData" :options="radarOptions" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="card grow">
|
||||||
<div v-if="criteria.length" class="card grow">
|
<div class="card-title">各项平均分</div>
|
||||||
<div class="card-title">各项平均分</div>
|
<div class="chart"><Bar :key="chartKey" :data="barData" :options="barOptions" /></div>
|
||||||
<div class="chart"><Bar :key="chartKey" :data="barData" :options="barOptions" /></div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 全是 2026-10 之后「走例子」评的图:没有四项分,这个位置给最常见的错 -->
|
||||||
|
<div v-else class="card grow">
|
||||||
|
<div class="card-title">最常见的错(最近交的图里)</div>
|
||||||
|
<ol v-if="data.commonMistakes.length" class="mistakes">
|
||||||
|
<li v-for="item in data.commonMistakes" :key="item.text">
|
||||||
|
<span class="mistake-text">{{ item.text }}</span>
|
||||||
|
<span class="muted">{{ item.count }} 张</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div v-else class="muted">交上来的图都走对了</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-cards bottom">
|
<div class="row-cards bottom">
|
||||||
<div class="card grow">
|
<!-- 新老评法混着:有走例子的错就列错,只有老数据时照旧是词云 -->
|
||||||
|
<div v-if="criteria.length && data.commonMistakes.length" class="card grow">
|
||||||
|
<div class="card-title">最常见的错(最近交的图里)</div>
|
||||||
|
<ol class="mistakes">
|
||||||
|
<li v-for="item in data.commonMistakes" :key="item.text">
|
||||||
|
<span class="mistake-text">{{ item.text }}</span>
|
||||||
|
<span class="muted">{{ item.count }} 张</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="criteria.length" class="card grow">
|
||||||
<div class="card-title">常见问题高频词(最近的评语里)</div>
|
<div class="card-title">常见问题高频词(最近的评语里)</div>
|
||||||
<div class="cloud"><canvas ref="cloudCanvas"></canvas></div>
|
<div class="cloud"><canvas ref="cloudCanvas"></canvas></div>
|
||||||
<div v-if="!data.wordFrequencies.length" class="muted">评语太少,还拼不出词云</div>
|
<div v-if="!data.wordFrequencies.length" class="muted">评语太少,还拼不出词云</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card people">
|
<div class="card people" :class="{ wide: !criteria.length }">
|
||||||
<div class="card-title">每人最好的一次(没交的在前)</div>
|
<div class="card-title">每人最好的一次(没交的在前)</div>
|
||||||
<div v-if="data.peopleTruncated" class="muted">人太多,只列了成绩靠后的 500 位</div>
|
<div v-if="data.peopleTruncated" class="muted">人太多,只列了成绩靠后的 500 位</div>
|
||||||
<div class="people-list">
|
<div class="people-list">
|
||||||
<div v-for="row in people" :key="row.username" class="person">
|
<div v-for="row in people" :key="row.username" class="person">
|
||||||
<UserName :username="row.username" class="p-name" />
|
<UserName :username="row.username" class="p-name" />
|
||||||
<span class="grade-pill" :style="gradeStyle(row.grade)">
|
<span class="grade-pill" :style="gradeStyle(row.grade)">
|
||||||
{{ row.grade ? `${row.grade} 级 ${row.score ?? 0} 分` : "没交" }}
|
{{ row.grade ? `${row.grade} 级` : "没交" }}
|
||||||
</span>
|
</span>
|
||||||
<span class="muted">{{ row.count }} 次</span>
|
<span class="muted">{{ row.count }} 次</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -489,4 +509,35 @@ function gradeStyle(grade: string | null) {
|
|||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mistakes {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mistakes li {
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mistake-text {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 没有词云、四项分的时候,每人一格铺满一行 */
|
||||||
|
.card.people.wide {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.people.wide .people-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
align-content: start;
|
||||||
|
column-gap: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
|
||||||
import type { LANGUAGE, ProblemDetail } from "utils/types"
|
import type { LANGUAGE, ProblemDetail } from "utils/types"
|
||||||
import { problemEntryOf } from "oj/problem/composables/problemPageContext"
|
import { problemEntryOf } from "oj/problem/composables/problemPageContext"
|
||||||
import { useCodeStore } from "oj/store/code"
|
import { useCodeStore } from "oj/store/code"
|
||||||
@@ -28,14 +27,13 @@ export const useProblemStore = defineStore("problem", () => {
|
|||||||
*/
|
*/
|
||||||
const failCount = computed(() => (problem.value?.myFailedCount ?? 0) + sessionFailCount.value)
|
const failCount = computed(() => (problem.value?.myFailedCount ?? 0) + sessionFailCount.value)
|
||||||
|
|
||||||
const { isDesktop } = useBreakpoints()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这道题能不能画流程图。比赛里不给(入口差异表);题单里原来也不给,现在给了,画到 A/S
|
* 这道题能不能画流程图。比赛里不给(入口差异表);题单里原来也不给,现在给了,画到 A/S
|
||||||
* 算完成(设计文档第 3 节决定 4)。手机上不给:节点库是 HTML5 拖放,手机上拖不动。
|
* 算完成(设计文档第 3 节决定 4)。手机上原来不给(节点库是 HTML5 拖放,拖不动),
|
||||||
|
* 2026-10 换成插入式编辑器之后只用点,手机上也能画
|
||||||
*/
|
*/
|
||||||
const canDraw = computed(
|
const canDraw = computed(
|
||||||
() => !!problem.value?.allowFlowchart && problemEntryOf(route) !== "contest" && isDesktop.value,
|
() => !!problem.value?.allowFlowchart && problemEntryOf(route) !== "contest",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,8 +30,14 @@ import { layoutDoc, PLACEHOLDER, type LaidNode, type LaidSlot } from "./layout"
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
height?: string
|
height?: string
|
||||||
|
/**
|
||||||
|
* 题目页(默认):每道题一份本地草稿,自动存。
|
||||||
|
* false:图交给外面管(后台出题页的参考流程图,v-model:value),不存草稿
|
||||||
|
*/
|
||||||
|
draft?: boolean
|
||||||
}
|
}
|
||||||
const { height = "calc(100vh - 134px)" } = defineProps<Props>()
|
const { height = "calc(100vh - 134px)", draft = true } = defineProps<Props>()
|
||||||
|
const model = defineModel<FlowDoc | null>("value", { default: null })
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
@@ -98,6 +104,13 @@ const cacheKey = computed(() => {
|
|||||||
const savedAt = ref<Date | null>(null)
|
const savedAt = ref<Date | null>(null)
|
||||||
|
|
||||||
function loadDraft() {
|
function loadDraft() {
|
||||||
|
if (!draft) {
|
||||||
|
doc.value = model.value ? cloneDoc(model.value) : emptyDoc()
|
||||||
|
deselect()
|
||||||
|
resetHistory()
|
||||||
|
fitSoon()
|
||||||
|
return
|
||||||
|
}
|
||||||
let saved: unknown = null
|
let saved: unknown = null
|
||||||
try {
|
try {
|
||||||
saved = storage.get(cacheKey.value)
|
saved = storage.get(cacheKey.value)
|
||||||
@@ -135,12 +148,20 @@ watch(
|
|||||||
doc,
|
doc,
|
||||||
() => {
|
() => {
|
||||||
if (loading) return
|
if (loading) return
|
||||||
saveDraft(cacheKey.value)
|
if (draft) saveDraft(cacheKey.value)
|
||||||
|
else model.value = cloneDoc(doc.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 外面换了一张图(后台点了「AI 照答案画一张」):摆上来,进历史,Ctrl+Z 能回去 */
|
||||||
|
watch(model, (next) => {
|
||||||
|
if (draft || !next || signature(next) === signature(doc.value)) return
|
||||||
|
setDoc(next)
|
||||||
|
})
|
||||||
|
|
||||||
watch(cacheKey, () => {
|
watch(cacheKey, () => {
|
||||||
|
if (!draft) return
|
||||||
loading = true
|
loading = true
|
||||||
loadDraft()
|
loadDraft()
|
||||||
nextTick(() => (loading = false))
|
nextTick(() => (loading = false))
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ type ExcludeKeys =
|
|||||||
| "isPublic"
|
| "isPublic"
|
||||||
| "contestId"
|
| "contestId"
|
||||||
|
|
||||||
export type BlankProblem = Omit<AdminProblem, ExcludeKeys | "hint" | "mermaidCode"> & {
|
export type BlankProblem = Omit<
|
||||||
|
AdminProblem,
|
||||||
|
ExcludeKeys | "hint" | "mermaidCode" | "flowchartData"
|
||||||
|
> & {
|
||||||
id?: number
|
id?: number
|
||||||
// 新建比赛题时由 detail.vue 在提交前写进来
|
// 新建比赛题时由 detail.vue 在提交前写进来
|
||||||
contestId?: number | null
|
contestId?: number | null
|
||||||
@@ -151,6 +154,8 @@ export type BlankProblem = Omit<AdminProblem, ExcludeKeys | "hint" | "mermaidCod
|
|||||||
// v-model 要的是 lvalue,模板里没法 ?? 兜底,所以在类型上就收掉 null
|
// v-model 要的是 lvalue,模板里没法 ?? 兜底,所以在类型上就收掉 null
|
||||||
hint: string
|
hint: string
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
|
/** 画布画的参考流程图(FlowDoc);不给就照存 mermaidCode(老题手写的 Mermaid) */
|
||||||
|
flowchartData?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProblemRow {
|
export interface ProblemRow {
|
||||||
|
|||||||
@@ -675,8 +675,9 @@ export const generateFlowchartRequestSchema = z.object({
|
|||||||
.min(1)
|
.min(1)
|
||||||
.max(64 * 1024),
|
.max(64 * 1024),
|
||||||
})
|
})
|
||||||
|
/** AI 照 Python 答案画的参考流程图:结构树(FlowDoc),后台画布直接摆上去接着改 */
|
||||||
export const generateFlowchartResponseSchema = z.object({
|
export const generateFlowchartResponseSchema = z.object({
|
||||||
flowchart: z.string(),
|
doc: z.record(z.string(), z.unknown()),
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 题目管理
|
// ---------------------------------------------------------------- 题目管理
|
||||||
@@ -771,6 +772,8 @@ export const adminProblemSchema = z.object({
|
|||||||
allowFlowchart: z.boolean(),
|
allowFlowchart: z.boolean(),
|
||||||
showFlowchart: z.boolean(),
|
showFlowchart: z.boolean(),
|
||||||
mermaidCode: z.string().nullable(),
|
mermaidCode: z.string().nullable(),
|
||||||
|
/** 参考流程图的结构树(FlowDoc);2026-10 之前出的题只有 mermaidCode,这里是空对象 */
|
||||||
|
flowchartData: z.record(z.string(), z.unknown()),
|
||||||
flowchartHint: z.string().nullable(),
|
flowchartHint: z.string().nullable(),
|
||||||
astRules: astRulesSchema.nullable(),
|
astRules: astRulesSchema.nullable(),
|
||||||
answers: z.array(problemAnswerSchema),
|
answers: z.array(problemAnswerSchema),
|
||||||
@@ -823,6 +826,11 @@ export const createProblemRequestSchema = z.object({
|
|||||||
allowFlowchart: z.boolean().default(false),
|
allowFlowchart: z.boolean().default(false),
|
||||||
showFlowchart: z.boolean().default(false),
|
showFlowchart: z.boolean().default(false),
|
||||||
mermaidCode: z.string().nullable().default(null),
|
mermaidCode: z.string().nullable().default(null),
|
||||||
|
/**
|
||||||
|
* 用画布画的参考流程图(FlowDoc)。给了就以它为准,mermaidCode 由后端照它重新生成;
|
||||||
|
* 不给(老题只有手写的 Mermaid)就原样存 mermaidCode
|
||||||
|
*/
|
||||||
|
flowchartData: z.record(z.string(), z.unknown()).nullable().default(null),
|
||||||
flowchartHint: z.string().nullable().default(null),
|
flowchartHint: z.string().nullable().default(null),
|
||||||
astRules: astRulesSchema.nullable().default(null),
|
astRules: astRulesSchema.nullable().default(null),
|
||||||
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
||||||
|
|||||||
@@ -66,16 +66,6 @@ export const createFlowchartResponseSchema = z.object({
|
|||||||
submissionId: z.string(),
|
submissionId: z.string(),
|
||||||
status: z.literal("pending"),
|
status: z.literal("pending"),
|
||||||
})
|
})
|
||||||
export const flowchartCurrentSchema = z.object({
|
|
||||||
count: z.number().int(),
|
|
||||||
score: z.number(),
|
|
||||||
grade: z.string(),
|
|
||||||
})
|
|
||||||
export const flowchartDetailSchema = z.object({
|
|
||||||
submission: flowchartSubmissionSchema.nullable(),
|
|
||||||
count: z.number().int(),
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程图画到这两档算这道题做完:题单进度、课堂条、课堂看板、「你画的流程图」、结果页签的 ✓
|
* 流程图画到这两档算这道题做完:题单进度、课堂条、课堂看板、「你画的流程图」、结果页签的 ✓
|
||||||
* 都按它(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 4、6)。前后端只有这一份
|
* 都按它(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 4、6)。前后端只有这一份
|
||||||
@@ -107,6 +97,12 @@ export const flowchartStatisticsSchema = z.object({
|
|||||||
personCount: z.number().int(),
|
personCount: z.number().int(),
|
||||||
completedCount: z.number().int(),
|
completedCount: z.number().int(),
|
||||||
wordFrequencies: z.array(z.object({ word: z.string(), count: z.number().int() })),
|
wordFrequencies: z.array(z.object({ word: z.string(), count: z.number().int() })),
|
||||||
|
/**
|
||||||
|
* 2026-10 起「走例子」评的图里最常见的错:每张图取第一组没走对的原因(「『一级果』多输出了
|
||||||
|
* 一行『一级果』」),同一句话数一下,最多 8 条。新评法几乎没有评语,词云拼不出来,
|
||||||
|
* 而同一个错在一个班里往往原样出现几十次 —— 老师要看的就是它
|
||||||
|
*/
|
||||||
|
commonMistakes: z.array(z.object({ text: z.string(), count: z.number().int() })),
|
||||||
// 与提交统计共用「未完成学生」的形状,见 submission.ts 的 unacceptedStudentSchema
|
// 与提交统计共用「未完成学生」的形状,见 submission.ts 的 unacceptedStudentSchema
|
||||||
dataUnaccepted: z.array(z.object({ username: z.string(), realName: z.string() })),
|
dataUnaccepted: z.array(z.object({ username: z.string(), realName: z.string() })),
|
||||||
/**
|
/**
|
||||||
@@ -177,8 +173,6 @@ export type FlowchartStatistics = z.infer<typeof flowchartStatisticsSchema>
|
|||||||
export type FlowchartSubmission = z.infer<typeof flowchartSubmissionSchema>
|
export type FlowchartSubmission = z.infer<typeof flowchartSubmissionSchema>
|
||||||
export type FlowchartListItem = z.infer<typeof flowchartListItemSchema>
|
export type FlowchartListItem = z.infer<typeof flowchartListItemSchema>
|
||||||
export type FlowchartList = z.infer<typeof flowchartListSchema>
|
export type FlowchartList = z.infer<typeof flowchartListSchema>
|
||||||
export type FlowchartCurrent = z.infer<typeof flowchartCurrentSchema>
|
|
||||||
export type FlowchartDetail = z.infer<typeof flowchartDetailSchema>
|
|
||||||
export type FlowchartScores = z.infer<typeof flowchartScoresSchema>
|
export type FlowchartScores = z.infer<typeof flowchartScoresSchema>
|
||||||
export type CreateFlowchartResponse = z.infer<typeof createFlowchartResponseSchema>
|
export type CreateFlowchartResponse = z.infer<typeof createFlowchartResponseSchema>
|
||||||
export type CreateFlowchartRequest = z.infer<typeof createFlowchartRequestSchema>
|
export type CreateFlowchartRequest = z.infer<typeof createFlowchartRequestSchema>
|
||||||
|
|||||||
Reference in new issue
Block a user