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

@@ -2,11 +2,13 @@ import api2 from "utils/api2"
import { toProblemListItem } from "admin/transforms"
import type {
AcTrend,
AdminProblemSetProgress,
BatchProblemTagResponse,
GenerateSqlTestCaseResponse,
RenameTagResponse,
SqlTestCaseScript,
AcmHelperItem,
SubmissionInfo,
AdminAiReport,
AdminAiReportList,
StuckProblem,
@@ -36,7 +38,6 @@ import type {
ProblemSetBadge,
ProblemSetList,
ProblemSetProblem,
ProblemSetProgressList,
TutorialListItem,
} from "utils/types"
@@ -476,7 +477,11 @@ export function makeProblemPublic(id: number, displayId: string) {
// 比赛辅助检查
export function getACMHelperList(contestId: number) {
return api2.get<AcmHelperItem[]>(`admin/contests/${contestId}/acm-helper`)
// acInfo 在契约里是 Record<string, unknown>acm_contest_rank 的 JSONB 原文),
// 组件侧按 SubmissionInfo 读,收窄放在这里
return api2.get<
Array<Omit<AcmHelperItem, "acInfo"> & { acInfo: SubmissionInfo }>
>(`admin/contests/${contestId}/acm-helper`)
}
export function updateACMHelperChecked(
@@ -652,8 +657,9 @@ export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
}
// 题单进度管理 API
// 注意:返回的是裸数组,不是分页信封 —— 和 oj 侧的 /user-progress 不同
export function getProblemSetProgress(problemSetId: number) {
return api2.get<ProblemSetProgressList>(
return api2.get<AdminProblemSetProgress[]>(
`admin/problem-sets/${problemSetId}/progress`,
)
}

View File

@@ -4,7 +4,7 @@ import type {
ProblemSet,
ProblemSetProblem,
ProblemSetBadge,
ProblemSetProgress,
AdminProblemSetProgress,
} from "utils/types"
import {
getProblemSetDetail,
@@ -37,7 +37,7 @@ const problemSetId = computed(() => Number(route.params.problemSetId))
const problemSet = ref<ProblemSet | null>(null)
const problems = ref<ProblemSetProblem[]>([])
const badges = ref<ProblemSetBadge[]>([])
const progress = ref<ProblemSetProgress[]>([])
const progress = ref<AdminProblemSetProgress[]>([])
// 模态框状态
const showAddProblemModal = ref(false)

View File

@@ -6,7 +6,7 @@ import {
CLASS_NAME_RE,
} from "utils/constants"
import { parseTime } from "utils/functions"
import type { Server } from "utils/types"
import type { OrphanTestCase, Server, WebsiteConfig } from "utils/types"
import { useConfigStore } from "shared/store/config"
import { useConfigWebSocket } from "shared/composables/websocket"
import {
@@ -98,8 +98,8 @@ const serverColumns: DataTableColumn<Server>[] = [
width: 100,
},
{ title: "IP", key: "ip", width: 140 },
{ title: "判题机版本", key: "judger_version", width: 100 },
{ title: "服务器 URL", key: "service_url", width: 200 },
{ title: "判题机版本", key: "judgerVersion", width: 100 },
{ title: "服务器 URL", key: "serviceUrl", width: 200 },
{
title: "上一次心跳",
key: "last_heartbeat",
@@ -114,14 +114,14 @@ const serverColumns: DataTableColumn<Server>[] = [
},
]
const testcases = ref<Testcase[]>([])
const testcases = ref<OrphanTestCase[]>([])
const token = ref("")
const servers = ref<Server[]>([])
const abnormalServers = computed(() =>
servers.value.filter((item) => item.status === "abnormal"),
)
const websiteConfig = reactive({
const websiteConfig = reactive<WebsiteConfig>({
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL,
websiteName: "判题狗",
websiteNameShortcut: "判题狗",

View File

@@ -427,7 +427,7 @@ function typeTagType(type: ExerciseType) {
<n-form-item label="选项(勾选所有正确答案)">
<n-space vertical style="width: 100%">
<n-flex
v-for="(opt, i) in mcqOptions"
v-for="(_opt, i) in mcqOptions"
:key="i"
align="center"
:size="8"

View File

@@ -10,7 +10,6 @@ interface Props {
}
const route = useRoute()
const router = useRouter()
const message = useMessage()
const props = defineProps<Props>()