接上 vue-tsc 时有 143 条既有错误,上一批迁移带走 89 条,这里把剩下的清完。
大部分是噪音(未使用的回调参数、死变量、少数 unknown),但里面躺着两个真问题:
- 个人主页查不存在的用户会抛:getProfile 返回 null 时后面照样取
.acmProblemsStatus,靠 `!` 压着。加了早返回。
- getProblemSetProgress 我上一批标错了类型:后台这个接口返回的是裸数组,
不是分页信封(和 oj 侧的 /user-progress 不一样)。已改正并加注释区分。
其余处理:
- 未使用的回调参数按 TS 约定加 `_` 前缀(v-for 的项、供子类重写的空钩子、
路由守卫的 from);确认无用的局部变量直接删(clickX/clickY 算了点击位置
但飞出方向用的是随机角度,hasToday 下面已经用 lastDateOnly 判过,
prefix 算了周/月但标签里没用上)。
- App.vue 的 highlight.js 注册:Promise.all([...]).then(m => m.map(x => x.default))
会把元组塌成 (HLJSApi | LanguageFn)[],hljs 上就找不到 registerLanguage。
改成逐个取 .default。
- 给一批 api 补上契约类型(getMetrics / getHitokoto / getTutorials /
formatCode / getProblemBeatRate / getClassUsernames 等),契约相应补了
8 个 z.infer 导出。
- ContestRank.submissionInfo 和 AcmHelperItem.acInfo 在 api 边界窄化成
SubmissionInfo —— 契约里是 Record<string, unknown>(JSONB 原文)。
- FlowchartEditor 的 TS2589:vue-flow 的 Node 嵌套太深,ref<Node[]>([]) 的
UnwrapRef 推导撞上实例化层级上限,改成 ref([]) as Ref<Node[]>。
- api2.ts 的响应拦截器**故意**不返回 AxiosResponse(要把 { data } 信封剥掉),
类型上确实说不通,没硬掰,写注释说明为什么用断言。
- 题目列表的「随机」按钮在模板里一直是注释状态,对应的 getRandom 和
getRandomProblemID 一并删除(后端 /problems/random 保留不动)。
验证:vue-tsc 0 条,apps/api tsc、check:routes、web build 全通过;
修过 key 的列都打接口确认过字段真实存在。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
235 lines
5.7 KiB
Vue
235 lines
5.7 KiB
Vue
<template>
|
||
<n-card :title="title" size="small" v-if="show">
|
||
<template #header-extra>
|
||
<n-text depth="3" style="font-size: 12px">反映刷题质量提升</n-text>
|
||
</template>
|
||
<div class="chart">
|
||
<Chart type="line" :data="data" :options="options" />
|
||
</div>
|
||
</n-card>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import type { ChartData, TooltipItem } from "chart.js"
|
||
import { Chart } from "vue-chartjs"
|
||
import {
|
||
Chart as ChartJS,
|
||
CategoryScale,
|
||
LinearScale,
|
||
PointElement,
|
||
LineElement,
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
Filler,
|
||
} from "chart.js"
|
||
import { useAIStore } from "oj/store/ai"
|
||
import { parseTime } from "utils/functions"
|
||
|
||
// 注册折线图所需的 Chart.js 组件
|
||
ChartJS.register(
|
||
CategoryScale,
|
||
LinearScale,
|
||
PointElement,
|
||
LineElement,
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
Filler,
|
||
)
|
||
|
||
const aiStore = useAIStore()
|
||
|
||
const title = computed(() => {
|
||
if (aiStore.duration === "months:2") {
|
||
return "过去两个月的每周提交效率"
|
||
} else if (aiStore.duration === "months:6") {
|
||
return "过去半年的每月提交效率"
|
||
} else if (aiStore.duration === "years:1") {
|
||
return "过去一年的每月提交效率"
|
||
} else {
|
||
return "过去四周的提交效率"
|
||
}
|
||
})
|
||
|
||
// 判断是否有数据
|
||
const show = computed(() => {
|
||
return aiStore.durationData.length > 0
|
||
})
|
||
|
||
// 计算提交效率数据
|
||
const efficiencyData = computed(() => {
|
||
return aiStore.durationData.map((duration) => {
|
||
const problemCount = duration.problemCount || 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
|
||
|
||
return {
|
||
label: [
|
||
parseTime(duration.start, "M月D日"),
|
||
parseTime(duration.end, "M月D日"),
|
||
].join("~"),
|
||
efficiency: efficiency,
|
||
onePassRate: onePassRate,
|
||
problemCount: problemCount,
|
||
submissionCount: submissionCount,
|
||
}
|
||
})
|
||
})
|
||
|
||
// 图表数据
|
||
const data = computed<ChartData<"line">>(() => {
|
||
const efficiency = efficiencyData.value
|
||
|
||
return {
|
||
labels: efficiency.map((e) => e.label),
|
||
datasets: [
|
||
{
|
||
label: "平均提交次数",
|
||
data: efficiency.map((e) => e.efficiency),
|
||
borderColor: "rgb(99, 102, 241)",
|
||
backgroundColor: "rgba(99, 102, 241, 0.1)",
|
||
tension: 0.4,
|
||
fill: true,
|
||
pointRadius: 5,
|
||
pointHoverRadius: 7,
|
||
borderWidth: 2.5,
|
||
pointBackgroundColor: "rgb(99, 102, 241)",
|
||
pointBorderColor: "#fff",
|
||
pointBorderWidth: 2,
|
||
yAxisID: "y",
|
||
},
|
||
{
|
||
label: "提交AC率",
|
||
data: efficiency.map((e) => e.onePassRate),
|
||
borderColor: "rgb(34, 197, 94)",
|
||
backgroundColor: "rgba(34, 197, 94, 0.1)",
|
||
tension: 0.4,
|
||
fill: true,
|
||
pointRadius: 5,
|
||
pointHoverRadius: 7,
|
||
borderWidth: 2.5,
|
||
pointBackgroundColor: "rgb(34, 197, 94)",
|
||
pointBorderColor: "#fff",
|
||
pointBorderWidth: 2,
|
||
yAxisID: "y1",
|
||
},
|
||
],
|
||
}
|
||
})
|
||
|
||
// 图表配置
|
||
const options = computed(() => {
|
||
return {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
mode: "index" as const,
|
||
intersect: false,
|
||
},
|
||
scales: {
|
||
x: {
|
||
ticks: {
|
||
maxRotation: 0,
|
||
minRotation: 0,
|
||
autoSkip: true,
|
||
},
|
||
},
|
||
y: {
|
||
type: "linear" as const,
|
||
position: "left" as const,
|
||
title: {
|
||
display: true,
|
||
text: "平均提交次数(次/题)",
|
||
font: {
|
||
size: 13,
|
||
},
|
||
},
|
||
beginAtZero: true,
|
||
ticks: {
|
||
callback: function (value: string | number) {
|
||
return Number(value).toFixed(1)
|
||
},
|
||
},
|
||
},
|
||
y1: {
|
||
type: "linear" as const,
|
||
position: "right" as const,
|
||
min: 0,
|
||
max: 100,
|
||
title: {
|
||
display: true,
|
||
text: "提交AC率(%)",
|
||
font: {
|
||
size: 13,
|
||
},
|
||
},
|
||
ticks: {
|
||
callback: function (value: string | number) {
|
||
return Number(value).toFixed(0) + "%"
|
||
},
|
||
},
|
||
grid: {
|
||
drawOnChartArea: false,
|
||
},
|
||
},
|
||
},
|
||
plugins: {
|
||
title: {
|
||
display: false,
|
||
},
|
||
tooltip: {
|
||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||
padding: 12,
|
||
callbacks: {
|
||
label: function (ctx: TooltipItem<"line">) {
|
||
const index = ctx.dataIndex
|
||
const item = efficiencyData.value[index]
|
||
const dsLabel = ctx.dataset.label || ""
|
||
|
||
if (ctx.datasetIndex === 0) {
|
||
// 平均提交次数
|
||
return [
|
||
`${dsLabel}: ${item.efficiency.toFixed(2)} 次/题`,
|
||
`完成题目: ${item.problemCount} 题`,
|
||
`总提交: ${item.submissionCount} 次`,
|
||
]
|
||
} else {
|
||
// 提交AC率
|
||
return [
|
||
`${dsLabel}: ${item.onePassRate.toFixed(1)}%`,
|
||
`提示: AC题目数 / 总提交次数,越高表示提交质量越好`,
|
||
]
|
||
}
|
||
},
|
||
},
|
||
},
|
||
legend: {
|
||
display: true,
|
||
position: "bottom" as const,
|
||
labels: {
|
||
boxWidth: 12,
|
||
boxHeight: 12,
|
||
padding: 8,
|
||
font: {
|
||
size: 12,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
})
|
||
</script>
|
||
<style scoped>
|
||
.chart {
|
||
height: 300px;
|
||
width: 100%;
|
||
}
|
||
</style>
|