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:
@@ -60,10 +60,6 @@ const title = computed(() => {
|
||||
const data = computed<ChartData<"bar" | "line">>(() => {
|
||||
return {
|
||||
labels: aiStore.durationData.map((duration) => {
|
||||
let prefix = "周"
|
||||
if (duration.unit === "months") {
|
||||
prefix = "月"
|
||||
}
|
||||
return [
|
||||
parseTime(duration.start, "M月D日"),
|
||||
parseTime(duration.end, "M月D日"),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</n-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
||||
import type { ChartData, TooltipItem } from "chart.js"
|
||||
import { Chart } from "vue-chartjs"
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
|
||||
@@ -99,17 +99,6 @@ const streakData = computed(() => {
|
||||
let weekCount = 0
|
||||
let monthCount = 0
|
||||
|
||||
// 检查今天是否有做题
|
||||
const todayData = sortedData.find((item) => {
|
||||
const itemDate = new Date(item.timestamp)
|
||||
return (
|
||||
itemDate.getFullYear() === today.getFullYear() &&
|
||||
itemDate.getMonth() === today.getMonth() &&
|
||||
itemDate.getDate() === today.getDate()
|
||||
)
|
||||
})
|
||||
const hasToday = todayData && todayData.value > 0
|
||||
|
||||
// 遍历数据计算连续天数
|
||||
for (const item of sortedData) {
|
||||
if (item.value > 0) {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
type AiAnalysisRecord,
|
||||
type Contest as OjContest,
|
||||
type ContestAccess,
|
||||
type ContestList,
|
||||
type ActivityRankItem,
|
||||
type FormatCodeResponse,
|
||||
type Metrics,
|
||||
type TutorialSummary,
|
||||
type ClassComparisonResponse,
|
||||
type ClassRankItem,
|
||||
type ClassUserRank,
|
||||
type ContestRank,
|
||||
type UserRank,
|
||||
type ProblemRank,
|
||||
type CreateSubmissionResponse,
|
||||
@@ -38,6 +41,7 @@ import api2 from "utils/api2"
|
||||
import { filterResult } from "oj/transforms"
|
||||
import type {
|
||||
Announcement,
|
||||
ContestRank,
|
||||
Profile,
|
||||
Message,
|
||||
SubmissionListItem,
|
||||
@@ -84,10 +88,6 @@ export function getAuthors(all = false) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getRandomProblemID() {
|
||||
return api2.get("problems/random")
|
||||
}
|
||||
|
||||
export async function getProblem(problemID: string, contestID: string) {
|
||||
const endpoint = contestID
|
||||
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
|
||||
@@ -96,8 +96,9 @@ export async function getProblem(problemID: string, contestID: string) {
|
||||
return { error: null, data: detailProblem(response.data) }
|
||||
}
|
||||
|
||||
// 未登录返回 "0",登录后返回百分比字符串
|
||||
export function getProblemBeatRate(problemID: number) {
|
||||
return api2.get(`problems/${problemID}/beat-count`)
|
||||
return api2.get<string>(`problems/${problemID}/beat-count`)
|
||||
}
|
||||
|
||||
export async function getSubmission(id: string) {
|
||||
@@ -121,7 +122,7 @@ export function formatCode(data: { code: string; language: string }) {
|
||||
"C++": "cpp",
|
||||
SQL: "sql",
|
||||
}
|
||||
return api2.post("code/format", {
|
||||
return api2.post<FormatCodeResponse>("code/format", {
|
||||
code: data.code,
|
||||
language: languages[data.language] ?? data.language.toLowerCase(),
|
||||
})
|
||||
@@ -148,7 +149,9 @@ export function getTodaySubmissionCount(language?: string) {
|
||||
}
|
||||
|
||||
export function adminRejudge(id: string) {
|
||||
return api2.post(`submissions/${encodeURIComponent(id)}/rejudge`)
|
||||
return api2.post<{ ok: boolean }>(
|
||||
`submissions/${encodeURIComponent(id)}/rejudge`,
|
||||
)
|
||||
}
|
||||
|
||||
export function getSubmissionStatistics(
|
||||
@@ -219,13 +222,17 @@ export function getContest(id: string) {
|
||||
}
|
||||
|
||||
export function getContestAccess(id: string) {
|
||||
return api2.get(`contests/${encodeURIComponent(id)}/access`)
|
||||
return api2.get<ContestAccess>(`contests/${encodeURIComponent(id)}/access`)
|
||||
}
|
||||
|
||||
// 注意和 GET /access 不一样:这个返回裸 true,密码错是 403 走 catch
|
||||
export function checkContestPassword(contestID: string, password: string) {
|
||||
return api2.post(`contests/${encodeURIComponent(contestID)}/access`, {
|
||||
password,
|
||||
})
|
||||
return api2.post<boolean>(
|
||||
`contests/${encodeURIComponent(contestID)}/access`,
|
||||
{
|
||||
password,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function getContestProblems(contestID: string) {
|
||||
@@ -239,7 +246,9 @@ export function getContestRank(
|
||||
contestID: string,
|
||||
query: { limit: number; offset: number },
|
||||
) {
|
||||
return api2.get<ContestRank>(
|
||||
// submissionInfo 在契约里是 Record<string, unknown>(JSONB 原文),
|
||||
// 前端在这里收窄成 SubmissionInfo,见 utils/types 的 ContestRank
|
||||
return api2.get<{ results: ContestRank[]; total: number }>(
|
||||
`contests/${encodeURIComponent(contestID)}/rank`,
|
||||
{ params: query },
|
||||
)
|
||||
@@ -300,7 +309,7 @@ export function refreshUserProblemDisplayIds() {
|
||||
}
|
||||
|
||||
export function getMetrics(userid: number) {
|
||||
return api2.get(`users/${userid}/metrics`)
|
||||
return api2.get<Metrics>(`users/${userid}/metrics`)
|
||||
}
|
||||
|
||||
export function getTutorial(id: number) {
|
||||
@@ -308,7 +317,7 @@ export function getTutorial(id: number) {
|
||||
}
|
||||
|
||||
export function getTutorials(type: "python" | "c") {
|
||||
return api2.get("tutorials", { params: { type } })
|
||||
return api2.get<TutorialSummary[]>("tutorials", { params: { type } })
|
||||
}
|
||||
|
||||
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||
|
||||
@@ -88,7 +88,7 @@ function reset() {
|
||||
|
||||
<n-space vertical :size="6">
|
||||
<div
|
||||
v-for="(line, idx) in data.lines"
|
||||
v-for="(_line, idx) in data.lines"
|
||||
:key="idx"
|
||||
:style="lineStyle(idx)"
|
||||
@click="toggle(idx)"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Icon } from "@iconify/vue"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
|
||||
import type { SUBMISSION_RESULT } from "utils/types"
|
||||
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
|
||||
import { Pie } from "vue-chartjs"
|
||||
import {
|
||||
@@ -30,19 +31,19 @@ const beatRate = ref("0")
|
||||
const yearlyACData = ref<YearlyACData[]>([])
|
||||
|
||||
const data = computed(() => {
|
||||
const status = problem.value!.statisticInfo
|
||||
const labels = []
|
||||
for (let i in status) {
|
||||
if (status[i] !== 0) {
|
||||
// @ts-ignore
|
||||
labels.push(JUDGE_STATUS[i]["name"])
|
||||
// statisticInfo 是 { 判题状态码: 次数 } 的 JSONB,契约里是 Record<string, unknown>
|
||||
const status = problem.value!.statisticInfo as Record<string, number>
|
||||
const labels: string[] = []
|
||||
const values: number[] = []
|
||||
for (const code in status) {
|
||||
if (status[code] !== 0) {
|
||||
labels.push(JUDGE_STATUS[Number(code) as SUBMISSION_RESULT].name)
|
||||
values.push(status[code]!)
|
||||
}
|
||||
}
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{ data: Object.values(status), hoverOffset: 5, borderRadius: 10 },
|
||||
],
|
||||
datasets: [{ data: values, hoverOffset: 5, borderRadius: 10 }],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { NFlex, NTag } from "naive-ui"
|
||||
import { useRouteQuery } from "@vueuse/router"
|
||||
import { getProblemList, getRandomProblemID } from "oj/api"
|
||||
import { getProblemList } from "oj/api"
|
||||
import { getTagColor } from "utils/functions"
|
||||
import type { ProblemFiltered } from "utils/types"
|
||||
import { getProblemTagList } from "shared/api"
|
||||
@@ -102,11 +102,6 @@ function chooseTag(tag: Tag) {
|
||||
})
|
||||
}
|
||||
|
||||
async function getRandom() {
|
||||
const res = await getRandomProblemID()
|
||||
router.push("/problem/" + res.data)
|
||||
}
|
||||
|
||||
// 监听搜索关键词变化(防抖)
|
||||
watchDebounced(() => query.keyword, listProblems, {
|
||||
debounce: 500,
|
||||
@@ -263,9 +258,6 @@ function rowProps(row: ProblemFiltered) {
|
||||
<n-form-item>
|
||||
<n-button @click="clearQuery" quaternary>重置</n-button>
|
||||
</n-form-item>
|
||||
<!-- <n-form-item>
|
||||
<n-button @click="getRandom" quaternary>随机</n-button>
|
||||
</n-form-item> -->
|
||||
<n-form-item>
|
||||
<n-button
|
||||
@click="toggleShowTag()"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { formatISO, getTime, parseISO } from "date-fns"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { ContestStatus, ContestType } from "utils/constants"
|
||||
import { duration } from "utils/functions"
|
||||
import type { Contest, Problem } from "utils/types"
|
||||
import type { OjContest, ProblemFiltered } from "utils/types"
|
||||
import {
|
||||
checkContestPassword,
|
||||
getContest,
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
export const useContestStore = defineStore("contest", () => {
|
||||
const userStore = useUserStore()
|
||||
const [access, toggleAccess] = useToggle(false)
|
||||
const contest = ref<Contest | null>(null)
|
||||
const problems = ref<Problem[]>([])
|
||||
const contest = ref<OjContest | null>(null)
|
||||
const problems = ref<ProblemFiltered[]>([])
|
||||
const now = ref(0)
|
||||
|
||||
let timer = 0
|
||||
@@ -59,7 +59,8 @@ export const useContestStore = defineStore("contest", () => {
|
||||
problems.value = []
|
||||
const res = await getContest(contestID)
|
||||
contest.value = res.data
|
||||
now.value = getTime(parseISO(res.data.now))
|
||||
// now 是学生侧比赛专有的服务器时间,用来对齐倒计时
|
||||
now.value = getTime(parseISO(res.data.now ?? res.data.createTime))
|
||||
if (contestStatus.value !== ContestStatus.finished) {
|
||||
timer = setInterval(() => {
|
||||
now.value = now.value + 1000
|
||||
|
||||
@@ -64,7 +64,9 @@ async function init() {
|
||||
try {
|
||||
const res = await getProfile(route.query.name as string)
|
||||
profile.value = res.data
|
||||
const acm = res.data!.acmProblemsStatus.problems || {}
|
||||
// 用户不存在时后端返回 null,后面的统计全都无从算起
|
||||
if (!res.data) return
|
||||
const acm = res.data.acmProblemsStatus.problems || {}
|
||||
const ac: string[] = []
|
||||
Object.keys(acm).forEach((id) => {
|
||||
if (acm[id]["status"] === 0) {
|
||||
@@ -74,8 +76,8 @@ async function init() {
|
||||
ac.sort()
|
||||
problems.value = ac
|
||||
|
||||
if (profile.value.submissionNumber > 0) {
|
||||
const metricsRes = await getMetrics(profile.value.user.id)
|
||||
if (res.data.submissionNumber > 0) {
|
||||
const metricsRes = await getMetrics(res.data.user.id)
|
||||
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
||||
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
||||
toLatestAt.value = durationToDays(
|
||||
|
||||
Reference in New Issue
Block a user