From 69883bd0165eb4808d9cbd13557957ab1844545f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Thu, 3 Sep 2026 02:35:29 -0600 Subject: [PATCH] fix chart --- apps/api/src/routes/ai.ts | 6 +- .../oj/ai/components/DifficultyGradeChart.vue | 4 +- .../src/oj/ai/components/DurationChart.vue | 35 ++++---- .../src/oj/ai/components/EfficiencyChart.vue | 21 +++-- apps/web/src/oj/ai/components/Heatmap.vue | 84 ++++++++++++------- .../src/oj/ai/components/ProgressChart.vue | 15 ++-- .../ai/components/RankDistributionChart.vue | 22 +++-- apps/web/src/oj/ai/components/SolvedTable.vue | 5 +- .../src/oj/ai/components/TagsRadarChart.vue | 16 ++-- .../oj/ai/components/TimeActivityHeatmap.vue | 4 +- apps/web/src/oj/store/ai.ts | 2 + apps/web/src/shared/composables/chartTheme.ts | 40 +++++++++ packages/contract/src/ai.ts | 7 ++ 13 files changed, 180 insertions(+), 81 deletions(-) create mode 100644 apps/web/src/shared/composables/chartTheme.ts diff --git a/apps/api/src/routes/ai.ts b/apps/api/src/routes/ai.ts index a7c3a07..6db7e08 100644 --- a/apps/api/src/routes/ai.ts +++ b/apps/api/src/routes/ai.ts @@ -102,6 +102,7 @@ async function buildDetail(user: AuthUser, start: string, end: string) { const problemIds = firstAc.map((item) => item.problemId) if (!problemIds.length) return aiDetailSchema.parse({ user: user.username, className: user.className, start, end, solved: [], flowcharts: [], grade: "", tags: {}, difficulty: {}, contestCount: 0, + rankScope: "global", }) const classUsers = user.className ? await db.select({ id: schema.user.id }).from(schema.user).where(eq(schema.user.className, user.className)) : [] const scopeIds = classUsers.length > 1 ? classUsers.map((item) => item.id) : null @@ -166,6 +167,7 @@ async function buildDetail(user: AuthUser, start: string, end: string) { user: user.username, className: user.className, start, end, solved, flowcharts, grade: averageGrade(solved.map((item) => item.grade)), tags: topTags, difficulty, contestCount: new Set(solved.flatMap((item) => item.problem.contestId ?? [])).size, + rankScope: scopeIds ? "class" : "global", }) } @@ -258,7 +260,8 @@ async function buildDuration(user: AuthUser, endText: string, duration: string) const from = bucket.start.getTime() const to = bucket.end.getTime() const inRange = rows.filter((row) => row.time >= from && row.time <= to) - const solved = [...new Set(inRange.filter((row) => accepted.includes(row.result)).map((row) => row.problemId))] + const acceptedRows = inRange.filter((row) => accepted.includes(row.result)) + const solved = [...new Set(acceptedRows.map((row) => row.problemId))] return durationDataSchema.parse({ unit: config.unit, index: config.count - 1 - index, @@ -266,6 +269,7 @@ async function buildDuration(user: AuthUser, endText: string, duration: string) end: bucket.end.toISOString(), grade: solved.length ? bucketGrade(solved, from, to) : "", problemCount: solved.length, + acceptedCount: acceptedRows.length, submissionCount: inRange.length, }) }) diff --git a/apps/web/src/oj/ai/components/DifficultyGradeChart.vue b/apps/web/src/oj/ai/components/DifficultyGradeChart.vue index b21aa4b..5ebda95 100644 --- a/apps/web/src/oj/ai/components/DifficultyGradeChart.vue +++ b/apps/web/src/oj/ai/components/DifficultyGradeChart.vue @@ -6,7 +6,7 @@
- +
@@ -23,11 +23,13 @@ import { Legend, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" import type { Grade } from "utils/types" ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend) const aiStore = useAIStore() +const { chartKey } = useChartTheme() // 难度和等级的顺序(后端返回的是中文) const difficultyOrder = ["简单", "中等", "困难"] diff --git a/apps/web/src/oj/ai/components/DurationChart.vue b/apps/web/src/oj/ai/components/DurationChart.vue index 2039f12..f0fec77 100644 --- a/apps/web/src/oj/ai/components/DurationChart.vue +++ b/apps/web/src/oj/ai/components/DurationChart.vue @@ -4,7 +4,7 @@ 全面评估学习情况
- +
@@ -25,6 +25,7 @@ import { LineController, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" import { parseTime } from "utils/functions" // 注册混合图表(Bar + Line)所需的 Chart.js 组件 @@ -42,6 +43,7 @@ ChartJS.register( ) const aiStore = useAIStore() +const { chartKey } = useChartTheme() const gradeOrder = ["C", "B", "A", "S"] as const @@ -89,7 +91,6 @@ const data = computed>(() => { spanGaps: false, tension: 0.4, yAxisID: "y1", - barThickness: 10, order: 1, borderWidth: 2, pointRadius: 4, @@ -128,8 +129,11 @@ const options = computed>(() => { 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 = Number(v) + const idx = Math.round(Number(v)) return gradeOrder[idx] || "" }, }, @@ -167,24 +171,15 @@ const options = computed>(() => { } return `${dsLabel}: ${ctx.formattedValue}` }, + // AC 率直接读该周期的 acceptedCount / submissionCount。原来拿柱子上的 + // 「完成题目数 / 总提交次数」当 AC 率,分子是去重后的题数,不是一回事 footer: (items: TooltipItem<"bar" | "line">[]) => { - const barItems = items.filter( - (item) => (item.dataset as any).yAxisID === "y", - ) - if (barItems.length >= 2) { - const problemCount = - barItems.find((item) => item.dataset.label === "完成题目数") - ?.parsed.y || 0 - const submissionCount = - barItems.find((item) => item.dataset.label === "总提交次数") - ?.parsed.y || 0 - const efficiency = - submissionCount > 0 - ? ((problemCount / submissionCount) * 100).toFixed(1) - : "0" - return `AC率: ${efficiency}%` - } - return "" + 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} 次)` }, }, }, diff --git a/apps/web/src/oj/ai/components/EfficiencyChart.vue b/apps/web/src/oj/ai/components/EfficiencyChart.vue index 0e724e6..02f4c8b 100644 --- a/apps/web/src/oj/ai/components/EfficiencyChart.vue +++ b/apps/web/src/oj/ai/components/EfficiencyChart.vue @@ -4,7 +4,7 @@ 反映刷题质量提升
- +
@@ -23,6 +23,7 @@ import { Filler, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" import { parseTime } from "utils/functions" // 注册折线图所需的 Chart.js 组件 @@ -38,6 +39,7 @@ ChartJS.register( ) const aiStore = useAIStore() +const { chartKey } = useChartTheme() const title = computed(() => { if (aiStore.duration === "months:2") { @@ -60,15 +62,17 @@ const show = computed(() => { const efficiencyData = computed(() => { return aiStore.durationData.map((duration) => { const problemCount = duration.problemCount || 0 + const acceptedCount = duration.acceptedCount || 0 const submissionCount = duration.submissionCount || 0 // 计算效率:提交次数/完成题目数 // 值越接近1,说明一次AC率越高 const efficiency = problemCount > 0 ? submissionCount / problemCount : 0 - // AC率:AC题目数 / 总提交次数(越高说明提交质量越好) - const onePassRate = - submissionCount > 0 ? (problemCount / submissionCount) * 100 : 0 + // AC 率 = 判为通过的提交数 / 总提交数。原来分子用的是去重后的题目数, + // 那既不是 AC 率,算出来还正好是上面 efficiency 的倒数 —— 双轴画的是同一个数 + const acRate = + submissionCount > 0 ? (acceptedCount / submissionCount) * 100 : 0 return { label: [ @@ -76,8 +80,9 @@ const efficiencyData = computed(() => { parseTime(duration.end, "M月D日"), ].join("~"), efficiency: efficiency, - onePassRate: onePassRate, + acRate: acRate, problemCount: problemCount, + acceptedCount: acceptedCount, submissionCount: submissionCount, } }) @@ -107,7 +112,7 @@ const data = computed>(() => { }, { label: "提交AC率", - data: efficiency.map((e) => e.onePassRate), + data: efficiency.map((e) => e.acRate), borderColor: "rgb(34, 197, 94)", backgroundColor: "rgba(34, 197, 94, 0.1)", tension: 0.4, @@ -203,8 +208,8 @@ const options = computed(() => { } else { // 提交AC率 return [ - `${dsLabel}: ${item.onePassRate.toFixed(1)}%`, - `提示: AC题目数 / 总提交次数,越高表示提交质量越好`, + `${dsLabel}: ${item.acRate.toFixed(1)}%`, + `通过 ${item.acceptedCount} 次 / 共提交 ${item.submissionCount} 次`, ] } }, diff --git a/apps/web/src/oj/ai/components/Heatmap.vue b/apps/web/src/oj/ai/components/Heatmap.vue index 2c4f4a8..5aa8f19 100644 --- a/apps/web/src/oj/ai/components/Heatmap.vue +++ b/apps/web/src/oj/ai/components/Heatmap.vue @@ -4,7 +4,11 @@ 激励持续学习 -
+
import { useAIStore } from "oj/store/ai" import { parseTime } from "utils/functions" +import { useChartTheme } from "shared/composables/chartTheme" const aiStore = useAIStore() +const { isDark } = useChartTheme() const containerRef = useTemplateRef("containerRef") const CELL_SIZE = 12 @@ -68,30 +74,49 @@ const CELL_TOTAL = CELL_SIZE + CELL_GAP const DAY_WIDTH = 20 const MONTH_HEIGHT = 20 const RIGHT_PADDING = 5 -const COLORS = ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"] +// 深色下空格子不能再用接近白的 #ebedf0,整张图会变成一片发亮的方块 +const LIGHT_COLORS = ["#ebedf0", "#c6e48b", "#7bc96f", "#239a3b", "#196127"] +const DARK_COLORS = ["#22272e", "#0e4429", "#006d32", "#26a641", "#39d353"] const WEEK_DAYS = ["", "一", "", "三", "", "五", ""] -const getColor = (count: number) => - count === 0 - ? COLORS[0] - : count <= 2 - ? COLORS[1] - : count <= 4 - ? COLORS[2] - : count <= 7 - ? COLORS[3] - : COLORS[4] +const COLORS = computed(() => (isDark.value ? DARK_COLORS : LIGHT_COLORS)) +const cellStroke = computed(() => + isDark.value ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)", +) +const cellStrokeHover = computed(() => + isDark.value ? "rgba(255, 255, 255, 0.45)" : "rgba(0, 0, 0, 0.3)", +) + +const getColor = (count: number) => { + const palette = COLORS.value + if (count === 0) return palette[0] + if (count <= 2) return palette[1] + if (count <= 4) return palette[2] + if (count <= 7) return palette[3] + return palette[4] +} + +// 第一格未必是周日 —— 后端给的是「今天往前推 364 天」那天开始的 365 天。 +// 原来直接拿 i % 7 当行号,等于假设首日是周日,左边 一/三/五 的标签和月份标签 +// 会整体错位(今天是周四就错四行)。这里按首日真实的星期做偏移。 +const weekdayOffset = computed(() => { + const first = aiStore.heatmapData[0] + return first ? new Date(first.timestamp).getDay() : 0 +}) const cells = computed(() => - aiStore.heatmapData.map((item, i) => ({ - date: new Date(item.timestamp), - count: item.value, - color: getColor(item.value), - week: Math.floor(i / 7), - day: i % 7, - x: Math.floor(i / 7) * CELL_TOTAL, - y: (i % 7) * CELL_TOTAL, - })), + aiStore.heatmapData.map((item, i) => { + const slot = weekdayOffset.value + i + return { + date: new Date(item.timestamp), + count: item.value, + color: getColor(item.value), + week: Math.floor(slot / 7), + day: slot % 7, + x: Math.floor(slot / 7) * CELL_TOTAL, + y: (slot % 7) * CELL_TOTAL, + } + }), ) const monthLabels = computed(() => { @@ -100,9 +125,9 @@ const monthLabels = computed(() => { cells.value.forEach((cell, i) => { const month = cell.date.getMonth() - const isWeekStart = cell.date.getDay() === 0 || i === 0 + const isWeekStart = cell.day === 0 || i === 0 - if (month !== lastMonth && (isWeekStart || cell.date.getDay() <= 3)) { + if (month !== lastMonth && (isWeekStart || cell.day <= 3)) { labels.push({ text: `${month + 1}月`, x: DAY_WIDTH + cell.week * CELL_TOTAL, @@ -114,10 +139,11 @@ const monthLabels = computed(() => { return labels }) -const svgWidth = computed( - () => - DAY_WIDTH + Math.ceil(cells.value.length / 7) * CELL_TOTAL + RIGHT_PADDING, -) +const svgWidth = computed(() => { + const last = cells.value[cells.value.length - 1] + const weeks = last ? last.week + 1 : 0 + return DAY_WIDTH + weeks * CELL_TOTAL + RIGHT_PADDING +}) const svgHeight = computed(() => MONTH_HEIGHT + 7 * CELL_TOTAL) @@ -188,12 +214,12 @@ const hideTooltip = () => { .cell { cursor: pointer; transition: all 0.2s ease; - stroke: rgba(0, 0, 0, 0.05); + stroke: var(--cell-stroke); stroke-width: 0.5; } .cell:hover { - stroke: rgba(0, 0, 0, 0.3); + stroke: var(--cell-stroke-hover); stroke-width: 1.5; filter: brightness(0.9); } diff --git a/apps/web/src/oj/ai/components/ProgressChart.vue b/apps/web/src/oj/ai/components/ProgressChart.vue index e3a92f8..83d13de 100644 --- a/apps/web/src/oj/ai/components/ProgressChart.vue +++ b/apps/web/src/oj/ai/components/ProgressChart.vue @@ -4,7 +4,7 @@ 追踪学习成长轨迹
- +
@@ -24,6 +24,7 @@ import { Filler, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" import { parseTime } from "utils/functions" import type { Grade } from "utils/types" @@ -41,6 +42,7 @@ ChartJS.register( ) const aiStore = useAIStore() +const { chartKey } = useChartTheme() const gradeOrder = ["C", "B", "A", "S"] as const const gradeColors: Record = { @@ -78,8 +80,11 @@ const progressData = computed(() => { const problemCount = duration.problemCount || 0 cumulativeCount += problemCount - // 计算本期等级的权重值 - const currentGradeValue = gradeOrder.indexOf(duration.grade || "C") + // 契约里空串是「无评级」(该周期没有活动),不能当成 C —— 那会在 tooltip 上 + // 把一个没做题的周期写成「本期等级: C」。这里权重取 0,反正 problemCount 也是 0 + const currentGradeValue = duration.grade + ? gradeOrder.indexOf(duration.grade) + : 0 // 累加加权等级 totalWeightedGrade += currentGradeValue * problemCount @@ -97,7 +102,7 @@ const progressData = computed(() => { start: parseTime(duration.start, "YYYY-MM-DD"), end: parseTime(duration.end, "YYYY-MM-DD"), count: cumulativeCount, - grade: duration.grade || "C", + grade: duration.grade, gradeValue: currentGradeValue, avgGradeValue: avgGradeValue, // 累计平均等级 problemCount: problemCount, @@ -235,7 +240,7 @@ const options = computed>(() => { const avgIdx = Math.round(Number(ctx.parsed.y)) return [ `${dsLabel}: ${gradeOrder[avgIdx] || ""}`, - `本期等级: ${progress.grade}`, + `本期等级: ${progress.grade || "无"}`, `本期完成: ${progress.problemCount} 题`, ] } else { diff --git a/apps/web/src/oj/ai/components/RankDistributionChart.vue b/apps/web/src/oj/ai/components/RankDistributionChart.vue index 38d25a8..017e339 100644 --- a/apps/web/src/oj/ai/components/RankDistributionChart.vue +++ b/apps/web/src/oj/ai/components/RankDistributionChart.vue @@ -6,7 +6,7 @@
- +
@@ -15,10 +15,12 @@ import { Pie } from "vue-chartjs" import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" ChartJS.register(ArcElement, Title, Tooltip, Legend) const aiStore = useAIStore() +const { chartKey } = useChartTheme() // 排名区间定义 const RANK_RANGES = [ @@ -42,19 +44,21 @@ const rankDistribution = computed(() => { const acCount = item.periodAcCount if (rank && acCount && acCount > 0) { - const percentile = (rank / acCount) * 100 + // 口径和后端 grade() 一致:(rank - 1) / count。少减这个 1 会让每道题整体降一档, + // 而且 rank === acCount 时正好算出 100,落在 [70, 100) 之外被 findIndex 丢掉 —— + // 班里只有他一个人做出来的题(1 / 1)既被当成垫底又整个消失 + const percentile = ((rank - 1) / acCount) * 100 - // 找到对应的区间 + // 找到对应的区间;万一越界也归到最后一档,别让这道题凭空不见 const rangeIndex = RANK_RANGES.findIndex( (r) => percentile >= r.min && percentile < r.max, ) + const index = rangeIndex === -1 ? RANK_RANGES.length - 1 : rangeIndex - if (rangeIndex !== -1) { - distribution[rangeIndex].count++ - distribution[rangeIndex].problems.push( - `${item.problem.displayId}: ${item.problem.title}`, - ) - } + distribution[index].count++ + distribution[index].problems.push( + `${item.problem.displayId}: ${item.problem.title}`, + ) } }) diff --git a/apps/web/src/oj/ai/components/SolvedTable.vue b/apps/web/src/oj/ai/components/SolvedTable.vue index 8842be6..44ad4df 100644 --- a/apps/web/src/oj/ai/components/SolvedTable.vue +++ b/apps/web/src/oj/ai/components/SolvedTable.vue @@ -80,7 +80,10 @@ const columns: DataTableColumn[] = [ ), }, { - title: () => (aiStore.detailsData.className ? "班级排名" : "全服排名"), + // 用后端下发的 rankScope,不要看 className 有没有值:班里只有一个人时 + // 后端会回退到全服排名,那种学生原来看到的是「班级排名」配全服数据 + title: () => + aiStore.detailsData.rankScope === "class" ? "班级排名" : "全服排名", key: "rank", width: 100, align: "center", diff --git a/apps/web/src/oj/ai/components/TagsRadarChart.vue b/apps/web/src/oj/ai/components/TagsRadarChart.vue index 42b6e37..53998d9 100644 --- a/apps/web/src/oj/ai/components/TagsRadarChart.vue +++ b/apps/web/src/oj/ai/components/TagsRadarChart.vue @@ -4,7 +4,7 @@ 可视化知识点覆盖面
- +
@@ -20,6 +20,7 @@ import { Legend, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" // 注册雷达图所需的 Chart.js 组件 ChartJS.register( @@ -32,13 +33,15 @@ ChartJS.register( ) const aiStore = useAIStore() +const { chartKey, gridColor } = useChartTheme() const show = computed(() => { return Object.keys(aiStore.detailsData.tags).length > 0 }) -// 最多显示前10个标签,避免雷达图过于拥挤 -const MAX_TAGS = 10 +// 后端 /ai/detail 已经把 tags 截到前 5(routes/ai.ts 的 topTags),这里取同一个上限。 +// 原来写 10 是取不满的死数 +const MAX_TAGS = 5 const title = computed(() => { const totalTags = Object.keys(aiStore.detailsData.tags).length @@ -112,11 +115,11 @@ const options = computed(() => { }, }, grid: { - color: "rgba(0, 0, 0, 0.1)", + color: gridColor.value, circular: true, }, angleLines: { - color: "rgba(0, 0, 0, 0.1)", + color: gridColor.value, }, pointLabels: { font: { @@ -138,7 +141,8 @@ const options = computed(() => { const index = context.dataIndex const actualValue = tagData[index].value const percentage = Math.round(Number(context.parsed.r)) - return `完成 ${actualValue} 道题 (掌握度 ${percentage}%)` + // 这个百分比是「占最多的那个标签的比例」,不是掌握度 —— 第一名恒为 100% + return `完成 ${actualValue} 道题 (占最多标签的 ${percentage}%)` }, }, }, diff --git a/apps/web/src/oj/ai/components/TimeActivityHeatmap.vue b/apps/web/src/oj/ai/components/TimeActivityHeatmap.vue index daa85ad..60a205c 100644 --- a/apps/web/src/oj/ai/components/TimeActivityHeatmap.vue +++ b/apps/web/src/oj/ai/components/TimeActivityHeatmap.vue @@ -6,7 +6,7 @@
- +
@@ -23,10 +23,12 @@ import { Legend, } from "chart.js" import { useAIStore } from "oj/store/ai" +import { useChartTheme } from "shared/composables/chartTheme" ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend) const aiStore = useAIStore() +const { chartKey } = useChartTheme() const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"] const TIME_PERIODS = [ diff --git a/apps/web/src/oj/store/ai.ts b/apps/web/src/oj/store/ai.ts index daedef5..7d6ceb2 100644 --- a/apps/web/src/oj/store/ai.ts +++ b/apps/web/src/oj/store/ai.ts @@ -25,6 +25,7 @@ export const useAIStore = defineStore("ai", () => { contestCount: 0, solved: [], flowcharts: [], + rankScope: "global", }) const heatmapData = ref<{ timestamp: number; value: number }[]>([]) @@ -51,6 +52,7 @@ export const useAIStore = defineStore("ai", () => { detailsData.tags = res.tags detailsData.difficulty = res.difficulty detailsData.contestCount = res.contestCount + detailsData.rankScope = res.rankScope detailsData.flowcharts = res.flowcharts } diff --git a/apps/web/src/shared/composables/chartTheme.ts b/apps/web/src/shared/composables/chartTheme.ts new file mode 100644 index 0000000..15a2ea7 --- /dev/null +++ b/apps/web/src/shared/composables/chartTheme.ts @@ -0,0 +1,40 @@ +import { useDark } from "@vueuse/core" +import { Chart as ChartJS } from "chart.js" + +/** + * chart.js 的默认配色是写死的浅色(文字 #666、网格线 rgba(0,0,0,0.1)),深色主题下 + * 坐标轴刻度和图例几乎看不见。这里挂一次全局默认值,跟着 isDark 走。 + * + * 只改默认值还不够:options 不是 computed 的图表在主题切换后不会重绘, + * 所以另外给出 chartKey —— 图表绑到 :key 上,切换时整个重新挂载。 + */ +export function useChartTheme() { + const isDark = useDark() + + const textColor = computed(() => + isDark.value ? "rgba(255, 255, 255, 0.75)" : "#606266", + ) + const gridColor = computed(() => + isDark.value ? "rgba(255, 255, 255, 0.12)" : "rgba(0, 0, 0, 0.1)", + ) + const chartKey = computed(() => (isDark.value ? "dark" : "light")) + + watchEffect(() => { + ChartJS.defaults.color = textColor.value + // 网格线只能从 scale 这边改,**不要**去动 ChartJS.defaults.borderColor: + // Colors 插件的 containsDefaultColorsDefenitions() 一看到根上的 borderColor + // 不等于出厂值就整个罢工,靠它自动配色的图(DurationChart)会退成近黑的柱子和图例 + // 网格线走 defaults.set("scale", ...),所有轴类型都继承这一层。 + // **不要**去动 ChartJS.defaults.borderColor:Colors 插件的 + // containsDefaultColorsDefenitions() 一看到根上的 borderColor 不等于出厂值 + // 就整个罢工,靠它自动配色的图(DurationChart)会退成近黑的柱子和图例。 + // 也不要直接写 defaults.scales.linear.border —— 那要求对应的 scale 已经注册, + // 只注册了 radialLinear 的雷达图会在 setup 里抛错。 + ChartJS.defaults.set("scale", { + grid: { color: gridColor.value }, + border: { color: gridColor.value }, + }) + }) + + return { isDark, textColor, gridColor, chartKey } +} diff --git a/packages/contract/src/ai.ts b/packages/contract/src/ai.ts index 3c8b6cc..02662f2 100644 --- a/packages/contract/src/ai.ts +++ b/packages/contract/src/ai.ts @@ -13,6 +13,8 @@ export const durationDataSchema = z.object({ end: z.string(), grade: gradeSchema, problemCount: z.number().int(), + /** 该周期内判为通过的提交数。problemCount 是去重后的题数,两者不能互相代替 */ + acceptedCount: z.number().int(), submissionCount: z.number().int(), }) @@ -53,6 +55,11 @@ export const aiDetailSchema = z.object({ tags: z.record(z.string(), z.number().int()), difficulty: z.record(z.string(), z.number().int()), contestCount: z.number().int(), + /** + * solved 里的 rank/acCount 是在哪个范围里排的。班里只有一个人时后端会回退到全服, + * 前端不能只看 className 有没有值就写「班级排名」。 + */ + rankScope: z.enum(["class", "global"]), }) /**