refactor(前端): vue-tsc 清零,type-check 可以当 CI 门禁了

接上 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>
This commit is contained in:
2026-08-25 14:04:10 -06:00
parent 3d501e8b69
commit 1198895b54
33 changed files with 119 additions and 99 deletions

View File

@@ -1,4 +1,4 @@
import axios, { type AxiosRequestConfig } from "axios"
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"
import { createDiscreteApi } from "naive-ui"
import { useAuthModalStore } from "shared/store/authModal"
import { STORAGE_KEY } from "./constants"
@@ -52,7 +52,14 @@ instance.interceptors.request.use((config) => {
})
instance.interceptors.response.use(
(response) => Promise.resolve({ error: null, data: response.data.data }),
// 这里**故意**不返回 AxiosResponse把 { data } 信封剥掉,让调用方直接拿到
// ApiResponse。类型上和 axios 的拦截器签名对不上(它期望原样返回响应),
// 文件末尾的 `as unknown as Api2Client` 就是为了把这个真实形状交出去。
((response: AxiosResponse) =>
Promise.resolve({
error: null,
data: response.data.data,
})) as unknown as (response: AxiosResponse) => AxiosResponse,
(error) => {
const payload = error.response?.data as Api2Error | undefined
const code = payload?.error?.code ?? "network-error"

View File

@@ -13,7 +13,9 @@ async function download(url: string) {
const headers = res.headers
const link = document.createElement("a")
link.href = window.URL.createObjectURL(
new window.Blob([res.data], { type: headers["content-type"] }),
new window.Blob([res.data], {
type: String(headers["content-type"] ?? ""),
}),
)
link.download = (headers["content-disposition"] || "").split("filename=")[1]
document.body.appendChild(link)

View File

@@ -400,11 +400,6 @@ export function trickOrTreat() {
return
}
// 获取点击位置相对于视口的位置
const rect = target.getBoundingClientRect()
const clickX = e.clientX - rect.left - rect.width / 2
const clickY = e.clientY - rect.top - rect.height / 2
// 计算飞出方向(随机方向)
const angle = Math.random() * Math.PI * 2
const distance = 1000 + Math.random() * 500

View File

@@ -436,6 +436,7 @@ export type {
AdminUserList,
AcmHelperItem,
AdminContestList,
AdminProblemSetProgress,
AdminAiReport,
AdminAiReportListItem,
AdminAiReportList,