feat(统计): 流程图统计列出「最常见的错」,不再显示新评法下没意义的平均分
新评法(走例子)没有四项分、几乎没有评语,雷达图、各项平均分、词云都拼不出来, 页面上只剩一个等级圆环。改成: - 「最常见的错」:每张图第一组没走对的原因原样数一下(「『一级果』多输出了一行 『一级果』2 张」),最多 8 条;全是新数据时放在等级分布旁边,新老混着时顶替词云 - 只有新数据时「每人最好的一次」铺满一行、分几栏 - 去掉「平均分」:新评法的分数只是走对的比例,看它不如看「拿到 A / S」; 「每人最好的一次」也只写等级 老数据(2026-10 之前)照旧是四项分和词云。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
04c90e3f89
commit
b9ece7d38f
3 files changed
+85
-15
No files matched your search
@@ -610,6 +610,7 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
||||
personCount: roster.length,
|
||||
completedCount: 0,
|
||||
wordFrequencies: [],
|
||||
commonMistakes: [],
|
||||
// 一条提交都没有时,花名册上的人**全都**是「没做」—— 原来这里写死空数组,
|
||||
// 于是一节课刚开始、最该点名的时候,教师面板反而一个名字都不给
|
||||
dataUnaccepted: roster.map((row) => ({
|
||||
@@ -645,8 +646,16 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
||||
const pushText = (value: string) => {
|
||||
if (texts.length < WORDCLOUD_TEXT_LIMIT) texts.push(value)
|
||||
}
|
||||
const mistakes = new Map<string, number>()
|
||||
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)
|
||||
// 和上面那条聚合同一道闸:分数不是数字的项当没配过,满分和评语也都不收
|
||||
if (typeof detail.score !== "number") continue
|
||||
@@ -676,6 +685,10 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
|
||||
personCount: roster.length,
|
||||
completedCount: totals.completedCount,
|
||||
wordFrequencies: await buildWordFrequencies(texts),
|
||||
commonMistakes: [...mistakes]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([text, n]) => ({ text, count: n })),
|
||||
dataUnaccepted: roster
|
||||
.filter((row) => !submitted.has(row.id))
|
||||
.map((row) => ({
|
||||
|
||||
@@ -200,13 +200,11 @@ const people = computed(() => [
|
||||
...props.data.dataUnaccepted.map((row) => ({
|
||||
username: row.username,
|
||||
grade: null as string | null,
|
||||
score: null as number | null,
|
||||
count: 0,
|
||||
})),
|
||||
...props.data.people.map((row) => ({
|
||||
username: row.username,
|
||||
grade: row.bestGrade,
|
||||
score: row.bestScore,
|
||||
count: row.count,
|
||||
})),
|
||||
])
|
||||
@@ -229,7 +227,6 @@ function gradeStyle(grade: string | null) {
|
||||
<div class="flow-stats">
|
||||
<div class="summary">
|
||||
<StatItem label="提交" :value="data.totalCount" unit="张" />
|
||||
<StatItem label="平均分" :value="data.avgScore" />
|
||||
<StatItem
|
||||
label="拿到 A / S"
|
||||
:value="`${goodCount} / ${personCount}`"
|
||||
@@ -267,31 +264,54 @@ function gradeStyle(grade: string | null) {
|
||||
<Doughnut :key="chartKey" :data="gradeData" :options="gradeOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="criteria.length" class="card radar">
|
||||
<div class="card-title">四项评分(平均得分率)</div>
|
||||
<div class="chart">
|
||||
<Radar :key="chartKey" :data="radarData" :options="radarOptions" />
|
||||
<template v-if="criteria.length">
|
||||
<div class="card radar">
|
||||
<div class="card-title">四项评分(平均得分率)</div>
|
||||
<div class="chart">
|
||||
<Radar :key="chartKey" :data="radarData" :options="radarOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="criteria.length" class="card grow">
|
||||
<div class="card-title">各项平均分</div>
|
||||
<div class="chart"><Bar :key="chartKey" :data="barData" :options="barOptions" /></div>
|
||||
<div class="card grow">
|
||||
<div class="card-title">各项平均分</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 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="cloud"><canvas ref="cloudCanvas"></canvas></div>
|
||||
<div v-if="!data.wordFrequencies.length" class="muted">评语太少,还拼不出词云</div>
|
||||
</div>
|
||||
<div class="card people">
|
||||
<div class="card people" :class="{ wide: !criteria.length }">
|
||||
<div class="card-title">每人最好的一次(没交的在前)</div>
|
||||
<div v-if="data.peopleTruncated" class="muted">人太多,只列了成绩靠后的 500 位</div>
|
||||
<div class="people-list">
|
||||
<div v-for="row in people" :key="row.username" class="person">
|
||||
<UserName :username="row.username" class="p-name" />
|
||||
<span class="grade-pill" :style="gradeStyle(row.grade)">
|
||||
{{ row.grade ? `${row.grade} 级 ${row.score ?? 0} 分` : "没交" }}
|
||||
{{ row.grade ? `${row.grade} 级` : "没交" }}
|
||||
</span>
|
||||
<span class="muted">{{ row.count }} 次</span>
|
||||
</div>
|
||||
@@ -489,4 +509,35 @@ function gradeStyle(grade: string | null) {
|
||||
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>
|
||||
@@ -97,6 +97,12 @@ export const flowchartStatisticsSchema = z.object({
|
||||
personCount: z.number().int(),
|
||||
completedCount: 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
|
||||
dataUnaccepted: z.array(z.object({ username: z.string(), realName: z.string() })),
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user