接上 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>
131 lines
3.6 KiB
Vue
131 lines
3.6 KiB
Vue
<script setup lang="ts">
|
||
import { signup } from "../api"
|
||
import { storeToRefs } from "pinia"
|
||
import { useAuthModalStore } from "../store/authModal"
|
||
|
||
const authStore = useAuthModalStore()
|
||
|
||
const {
|
||
signupModalOpen,
|
||
signupForm: form,
|
||
signupLoading: isLoading,
|
||
signupError: msg,
|
||
} = storeToRefs(authStore)
|
||
const signupRef = useTemplateRef("signupRef")
|
||
|
||
const rules: FormRules = {
|
||
username: [{ required: true, message: "用户名必填", trigger: "blur" }],
|
||
email: [{ required: true, message: "邮箱必填", trigger: "blur" }],
|
||
password: [
|
||
{ required: true, message: "密码必填", trigger: "blur" },
|
||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
||
],
|
||
passwordAgain: [
|
||
{ required: true, message: "密码必填", trigger: "blur" },
|
||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
||
{
|
||
validator: (_: FormItemRule, value: string) =>
|
||
value === form.value.password,
|
||
message: "两次密码输入不一致",
|
||
trigger: "blur",
|
||
},
|
||
],
|
||
}
|
||
|
||
function goLogin() {
|
||
authStore.switchToLogin()
|
||
}
|
||
|
||
function submit() {
|
||
signupRef.value!.validate(async (errors?: unknown) => {
|
||
if (!errors) {
|
||
try {
|
||
authStore.clearSignupError()
|
||
authStore.setSignupLoading(true)
|
||
await signup({
|
||
username: form.value.username,
|
||
email: form.value.email,
|
||
password: form.value.password,
|
||
})
|
||
} catch (err: any) {
|
||
if (err.error === "username-exists") {
|
||
authStore.setSignupError("用户名已存在")
|
||
} else if (err.error === "email-exists") {
|
||
authStore.setSignupError("邮箱已存在")
|
||
} else {
|
||
authStore.setSignupError("无法注册")
|
||
}
|
||
} finally {
|
||
authStore.setSignupLoading(false)
|
||
}
|
||
if (!msg.value) {
|
||
authStore.closeSignupModal()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<n-modal
|
||
:mask-closable="false"
|
||
v-model:show="signupModalOpen"
|
||
preset="card"
|
||
title="注册"
|
||
style="width: 400px"
|
||
:auto-focus="false"
|
||
>
|
||
<n-form ref="signupRef" :model="form" :rules="rules" show-require-mark>
|
||
<n-form-item label="用户名" path="username">
|
||
<n-input
|
||
v-model:value="form.username"
|
||
autofocus
|
||
clearable
|
||
name="username"
|
||
id="signup-username"
|
||
autocomplete="username"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="邮箱" path="email">
|
||
<n-input
|
||
v-model:value="form.email"
|
||
clearable
|
||
name="email"
|
||
id="signup-email"
|
||
autocomplete="email"
|
||
@change="submit"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="密码" path="password">
|
||
<n-input
|
||
v-model:value="form.password"
|
||
clearable
|
||
type="password"
|
||
name="password"
|
||
id="signup-password"
|
||
autocomplete="new-password"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="确认密码" path="passwordAgain">
|
||
<n-input
|
||
v-model:value="form.passwordAgain"
|
||
clearable
|
||
type="password"
|
||
name="passwordAgain"
|
||
id="signup-password-again"
|
||
autocomplete="new-password"
|
||
/>
|
||
</n-form-item>
|
||
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
||
<n-form-item>
|
||
<n-space>
|
||
<n-button type="primary" :loading="isLoading" @click="submit">
|
||
注册
|
||
</n-button>
|
||
<n-button @click="goLogin">已经注册?现在登录</n-button>
|
||
</n-space>
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-modal>
|
||
</template>
|