refactor(智能分析): 重排版面,砍掉重复的三张周期图,补三个新维度
原来 8 张图挤在两列大网格里,左列还套了一层两列小网格 —— 四张小图各自只有半页的 一半宽,"难度掌握情况"的标题被挤断成两行、周期图的 x 轴标签斜着叠在一起、一年热力图 53 列塞进半宽几乎看不清。改成单列为主,宽的图给全宽,窄的图两两并排。 砍掉的重复:进步曲线 / 提交效率 / 周期综合读的是同一个 durationData,半年视图统共 6 个桶四个字段,摊成三张卡六条曲线还都是双轴。合并成一张:柱是完成题目数和总提交数, 线是 AC 率,等级进 tooltip(S/A/B/C 四档离散值连成折线读不出东西,逐题等级表格里有)。 同期解题排名分布也砍了:五片扇形对十几道题做统计本来就是噪声,而排名逐题列在 SolvedTable 里,饼图没有增加任何信息。 换形式: - 标签雷达图 → 横向条形。雷达对比较大小是最差的形式之一,原来还把值归一化成"占最多 标签的百分比",第一名恒为 100%,等于只画了个排序。现在画真实题数。 - 难度掌握情况 → 难度分布。去掉叠在里面的 S/A/B/C 维度,3×4 十二个格子对一个两个月 做十来道题的学生大部分恒为 0。 热力图改成一格一周(53 格,周一起算,最后一格是本周)。按天切的话一年 365 格里三百多 格是空的,中职学生一年也就二三十天有提交,整张图看着像没用过。颜色阈值跟着按周重定。 新增三个维度: - 错在哪里:判完的失败提交按状态码分组。编译错误占大头说明语法不熟,答案错误占大头 说明是逻辑问题,两种情况老师该给的建议完全不同。 - 几次做对:到首次通过为止提交了几次,分一次过 / 2-3 / 4-6 / 7次以上。原来只有一个 "平均提交次数",看不出分布。 - 流程图得分:detailsData.flowcharts 早就在下发,但全页一张图都没有,只在解题表格的 第二个 tab 里列着。 顺带: - 时间活跃度从"只统计 AC 时间"改成"统计全部提交",星期和时段由后端按东八区聚合。 只看 AC 的话十来个点撒进 7×4 的格子几乎全是空的,和热力图的时区口径也对不上。 - 两两并排用弹性容器而不是固定两列网格:知识点分布在没有标签时整张卡不渲染, 固定两列会空掉一半。 - AI 卡片原来有三个条件挂载点(solved>10 在右列、≤10 在全宽行、=0 藏在 Overview 里面),单列之后收成最后一张无条件的卡。 - DurationChart 右轴的 S/A/B/C 一个刻度都没显示过:轴范围 -0.5~3.5,生成的刻度值是 -0.5/0.5/1.5/2.5/3.5,拿去索引 gradeOrder 全是 undefined。 契约新增 activity / errors / rankScope / solved[].attempts / durationData[].acceptedCount。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZuPwqDmLEiK9zgQ9z9sVn
This commit is contained in:
1 parent
69883bd016
commit
4d7be969d9
18 files changed
+759
-1212
No files matched your search
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<n-card :title="title" size="small">
|
||||
<n-card :title="title" size="small" v-if="show">
|
||||
<template #header-extra>
|
||||
<n-text depth="3" style="font-size: 12px"> 全面评估学习情况 </n-text>
|
||||
<n-text depth="3" style="font-size: 12px">
|
||||
做题量和提交质量的变化
|
||||
</n-text>
|
||||
</template>
|
||||
<div class="chart">
|
||||
<Chart type="bar" :key="chartKey" :data="data" :options="options" />
|
||||
@@ -18,11 +20,11 @@ import {
|
||||
BarElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Colors,
|
||||
LineController,
|
||||
BarController,
|
||||
} from "chart.js"
|
||||
import { useAIStore } from "oj/store/ai"
|
||||
import { useChartTheme } from "shared/composables/chartTheme"
|
||||
@@ -35,161 +37,143 @@ ChartJS.register(
|
||||
BarElement,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Colors,
|
||||
LineController,
|
||||
BarController,
|
||||
)
|
||||
|
||||
const aiStore = useAIStore()
|
||||
const { chartKey } = useChartTheme()
|
||||
|
||||
const gradeOrder = ["C", "B", "A", "S"] as const
|
||||
|
||||
const title = computed(() => {
|
||||
if (aiStore.duration === "months:2") {
|
||||
return "过去两个月的每周综合情况"
|
||||
return "过去两个月的每周情况"
|
||||
} else if (aiStore.duration === "months:6") {
|
||||
return "过去半年的每月综合情况"
|
||||
return "过去半年的每月情况"
|
||||
} else if (aiStore.duration === "years:1") {
|
||||
return "过去一年的每月综合情况"
|
||||
return "过去一年的每月情况"
|
||||
} else {
|
||||
return "过去四周的综合情况"
|
||||
return "过去四周的情况"
|
||||
}
|
||||
})
|
||||
|
||||
const data = computed<ChartData<"bar" | "line">>(() => {
|
||||
return {
|
||||
labels: aiStore.durationData.map((duration) => {
|
||||
return [
|
||||
parseTime(duration.start, "M月D日"),
|
||||
parseTime(duration.end, "M月D日"),
|
||||
].join("~")
|
||||
}),
|
||||
datasets: [
|
||||
{
|
||||
type: "bar",
|
||||
label: "完成题目数",
|
||||
data: aiStore.durationData.map((duration) => duration.problemCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
label: "总提交次数",
|
||||
data: aiStore.durationData.map((duration) => duration.submissionCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
label: "等级",
|
||||
data: aiStore.durationData.map((duration) =>
|
||||
duration.grade ? gradeOrder.indexOf(duration.grade) : null,
|
||||
),
|
||||
spanGaps: false,
|
||||
tension: 0.4,
|
||||
yAxisID: "y1",
|
||||
order: 1,
|
||||
borderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const show = computed(() => aiStore.durationData.length > 0)
|
||||
|
||||
const options = computed<ChartOptions<"bar" | "line">>(() => {
|
||||
return {
|
||||
interaction: {
|
||||
intersect: false,
|
||||
// 这一张顶掉了原来的三张(进步曲线 / 提交效率 / 周期综合)—— 它们读的是同一个
|
||||
// durationData,半年视图统共就 6 个桶、四个字段,没有必要摊成三张卡六条曲线。
|
||||
// 等级放进 tooltip 不再单独占一条轴:S/A/B/C 是四档离散值,连成折线读不出东西,
|
||||
// 而且逐题的等级在下面的解题表格里本来就有
|
||||
const data = computed<ChartData<"bar" | "line">>(() => ({
|
||||
labels: aiStore.durationData.map((duration) =>
|
||||
[
|
||||
parseTime(duration.start, "M月D日"),
|
||||
parseTime(duration.end, "M月D日"),
|
||||
].join("~"),
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
type: "bar",
|
||||
label: "完成题目数",
|
||||
data: aiStore.durationData.map((duration) => duration.problemCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
label: "总提交次数",
|
||||
data: aiStore.durationData.map((duration) => duration.submissionCount),
|
||||
yAxisID: "y",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
label: "AC率",
|
||||
// 没有提交的周期给 null 而不是 0,配合 spanGaps: false 断开,
|
||||
// 否则空档会被画成「AC率 0%」,看着像交了一堆全错
|
||||
data: aiStore.durationData.map((duration) =>
|
||||
duration.submissionCount > 0
|
||||
? (duration.acceptedCount / duration.submissionCount) * 100
|
||||
: null,
|
||||
),
|
||||
spanGaps: false,
|
||||
tension: 0.4,
|
||||
yAxisID: "y1",
|
||||
order: 1,
|
||||
borderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const options = computed<ChartOptions<"bar" | "line">>(() => ({
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: "index",
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
},
|
||||
y: {
|
||||
ticks: { stepSize: 1, precision: 0 },
|
||||
title: { display: true, text: "数量" },
|
||||
beginAtZero: true,
|
||||
},
|
||||
y1: {
|
||||
type: "linear",
|
||||
position: "right",
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: { callback: (v) => `${Number(v).toFixed(0)}%` },
|
||||
title: { display: true, text: "AC率" },
|
||||
grid: { display: false },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: "bottom",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 8,
|
||||
font: { size: 11 },
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
},
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: TooltipItem<"bar" | "line">) => {
|
||||
const dsLabel = ctx.dataset.label || ""
|
||||
if (ctx.dataset.label === "AC率") {
|
||||
return `${dsLabel}: ${Number(ctx.parsed.y).toFixed(1)}%`
|
||||
}
|
||||
return `${dsLabel}: ${ctx.formattedValue}`
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "数量",
|
||||
},
|
||||
beginAtZero: true,
|
||||
},
|
||||
y1: {
|
||||
type: "linear",
|
||||
position: "right",
|
||||
min: -0.5,
|
||||
max: gradeOrder.length - 0.5,
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
// 轴是 -0.5 ~ 3.5,chart.js 生成的刻度值就是 -0.5/0.5/1.5/2.5/3.5,
|
||||
// 直接拿去索引 gradeOrder 全是 undefined —— 右轴的 S/A/B/C 一个都不会显示。
|
||||
// 四舍五入到整数档再取,和 ProgressChart 的写法一致
|
||||
callback: (v) => {
|
||||
const idx = Math.round(Number(v))
|
||||
return gradeOrder[idx] || ""
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "等级",
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
footer: (items: TooltipItem<"bar" | "line">[]) => {
|
||||
const index = items[0]?.dataIndex
|
||||
const bucket =
|
||||
index === undefined ? undefined : aiStore.durationData[index]
|
||||
if (!bucket) return ""
|
||||
const lines = [`本期等级: ${bucket.grade || "无"}`]
|
||||
if (bucket.submissionCount > 0) {
|
||||
lines.push(
|
||||
`通过 ${bucket.acceptedCount} 次 / 共提交 ${bucket.submissionCount} 次`,
|
||||
)
|
||||
}
|
||||
return lines
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: "bottom" as const,
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 8,
|
||||
font: {
|
||||
size: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: TooltipItem<"bar" | "line">) => {
|
||||
const dsLabel = ctx.dataset.label || ""
|
||||
if ((ctx.dataset as any).yAxisID === "y1") {
|
||||
const idx = Number(ctx.parsed.y)
|
||||
return `${dsLabel}: ${gradeOrder[idx] || ""}`
|
||||
}
|
||||
return `${dsLabel}: ${ctx.formattedValue}`
|
||||
},
|
||||
// AC 率直接读该周期的 acceptedCount / submissionCount。原来拿柱子上的
|
||||
// 「完成题目数 / 总提交次数」当 AC 率,分子是去重后的题数,不是一回事
|
||||
footer: (items: TooltipItem<"bar" | "line">[]) => {
|
||||
const index = items[0]?.dataIndex
|
||||
const bucket =
|
||||
index === undefined ? undefined : aiStore.durationData[index]
|
||||
if (!bucket || bucket.submissionCount === 0) return ""
|
||||
const rate = (bucket.acceptedCount / bucket.submissionCount) * 100
|
||||
return `AC率: ${rate.toFixed(1)}%(通过 ${bucket.acceptedCount} / 共 ${bucket.submissionCount} 次)`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart {
|
||||
height: 300px;
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user