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 { userProfileSchema } from "@oj2/contract"
import { userProfileSchema, type Quote } from "@oj2/contract"
import api2 from "utils/api2"
import type { ApiResponse } from "utils/api2"
import type { Profile, Tag } from "utils/types"
@@ -38,9 +38,11 @@ export function getProblemTagList() {
}
export function getHitokoto() {
return api2.get("quotes/random")
return api2.get<Quote>("quotes/random")
}
export function getClassUsernames(classroom: string) {
return api2.get(`classes/${encodeURIComponent(classroom)}/usernames`)
return api2.get<string[]>(
`classes/${encodeURIComponent(classroom)}/usernames`,
)
}

View File

@@ -47,7 +47,7 @@
</template>
<script lang="ts" setup>
import { ref, onUnmounted, nextTick, computed, watch } from "vue"
import { ref, onUnmounted, nextTick, computed } from "vue"
import { getNodeTypeConfig } from "./useNodeStyles"
import NodeHandles from "./NodeHandles.vue"
import NodeActions from "./NodeActions.vue"

View File

@@ -32,8 +32,10 @@ const { height = "calc(100vh - 133px)" } = defineProps<Props>()
const { addEdges, removeNodes, removeEdges } = useVueFlow()
// 节点和边的响应式数据
const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([])
// 显式标注成 Ref<Node[]>,不走 ref<T>() 的 UnwrapRef 推导 —— vue-flow 的
// Node/Edge 嵌套很深,让 TS 去调和两种形态会直接撞上「实例化层级过深」
const nodes = ref([]) as Ref<Node[]>
const edges = ref([]) as Ref<Edge[]>
// 历史记录管理
const { canUndo, canRedo, saveState, undo, redo } = useHistory()

View File

@@ -9,8 +9,15 @@ const hitokoto = reactive({
async function receive() {
try {
const res = await getHitokoto()
hitokoto.sentence = res.data.hitokoto
hitokoto.from = res.data.from
// 契约是 string | Record —— 一言数据集不在仓库里,形状留了余地
const quote = res.data
if (typeof quote === "string") {
hitokoto.sentence = quote
hitokoto.from = ""
} else {
hitokoto.sentence = String(quote.hitokoto ?? "")
hitokoto.from = String(quote.from ?? "")
}
} catch (error) {
hitokoto.sentence = "获取一言失败,请点击重试"
hitokoto.from = "DEV"

View File

@@ -39,7 +39,7 @@ const rules: FormRules = {
}
async function submit() {
loginRef.value!.validate(async (errors: FormRules | undefined) => {
loginRef.value!.validate(async (errors?: unknown) => {
if (!errors) {
try {
authStore.clearLoginError()

View File

@@ -37,7 +37,7 @@ function goLogin() {
}
function submit() {
signupRef.value!.validate(async (errors: FormRules | undefined) => {
signupRef.value!.validate(async (errors?: unknown) => {
if (!errors) {
try {
authStore.clearSignupError()

View File

@@ -258,14 +258,14 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
/**
* 断开连接钩子(子类可重写)
*/
protected onDisconnected(event: CloseEvent) {
protected onDisconnected(_event: CloseEvent) {
// 子类实现
}
/**
* 错误钩子(子类可重写)
*/
protected onError(error: Event) {
protected onError(_error: Event) {
// 子类实现
}