接上 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>
200 lines
5.9 KiB
Vue
200 lines
5.9 KiB
Vue
<script setup lang="ts">
|
||
import { getClassUsernames, login } from "../api"
|
||
import { storeToRefs } from "pinia"
|
||
import { useAuthModalStore } from "../store/authModal"
|
||
import { useConfigStore } from "../store/config"
|
||
import { useUserStore } from "../store/user"
|
||
|
||
const userStore = useUserStore()
|
||
const configStore = useConfigStore()
|
||
const authStore = useAuthModalStore()
|
||
|
||
const {
|
||
loginModalOpen,
|
||
loginForm: form,
|
||
loginLoading: isLoading,
|
||
loginError: msg,
|
||
} = storeToRefs(authStore)
|
||
const loginRef = useTemplateRef("loginRef")
|
||
const classUserOptions = ref<SelectOption[]>([])
|
||
const classUserLoading = ref(false)
|
||
const isClassLogin = computed(() => Boolean(form.value.class))
|
||
const classList = computed<SelectOption[]>(() => {
|
||
const defaults = [{ label: "没有我所在的班级", value: "" }]
|
||
const configs =
|
||
configStore.config?.classList.map((item) => ({
|
||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||
value: `ks${item}`,
|
||
})) ?? []
|
||
return [...defaults, ...configs]
|
||
})
|
||
const rules: FormRules = {
|
||
username: [
|
||
{ required: true, message: "用户名必填", trigger: ["blur", "change"] },
|
||
],
|
||
password: [
|
||
{ required: true, message: "密码必填", trigger: "blur" },
|
||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
||
],
|
||
}
|
||
|
||
async function submit() {
|
||
loginRef.value!.validate(async (errors?: unknown) => {
|
||
if (!errors) {
|
||
try {
|
||
authStore.clearLoginError()
|
||
authStore.setLoginLoading(true)
|
||
const merged = {
|
||
username: form.value.username,
|
||
password: form.value.password,
|
||
}
|
||
if (form.value.class) {
|
||
merged.username = form.value.class + form.value.username
|
||
}
|
||
await login(merged)
|
||
} catch (err: any) {
|
||
// 判错误码而不是错误文案:文案在后端,改一个字这里就静默掉进「无法登录」
|
||
if (err.error === "account-disabled") {
|
||
authStore.setLoginError("此账号已被封禁")
|
||
} else if (err.error === "invalid-credentials") {
|
||
authStore.setLoginError("用户名或密码不正确")
|
||
} else {
|
||
authStore.setLoginError("无法登录")
|
||
}
|
||
} finally {
|
||
authStore.setLoginLoading(false)
|
||
}
|
||
if (!msg.value) {
|
||
authStore.closeLoginModal()
|
||
await userStore.getMyProfile()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function goSignup() {
|
||
authStore.switchToSignup()
|
||
}
|
||
|
||
async function loadClassUsernames(selectedClass: string) {
|
||
classUserLoading.value = true
|
||
try {
|
||
const res = await getClassUsernames(selectedClass)
|
||
classUserOptions.value = res.data.map((name: string) => ({
|
||
label: name,
|
||
value: name,
|
||
}))
|
||
} catch {
|
||
classUserOptions.value = []
|
||
} finally {
|
||
classUserLoading.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => form.value.class,
|
||
(selectedClass) => {
|
||
classUserOptions.value = []
|
||
form.value.username = ""
|
||
if (!selectedClass) {
|
||
classUserLoading.value = false
|
||
return
|
||
}
|
||
loadClassUsernames(selectedClass.slice(2))
|
||
},
|
||
)
|
||
|
||
onMounted(() => {
|
||
authStore.clearLoginError()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<n-modal
|
||
:mask-closable="false"
|
||
v-model:show="loginModalOpen"
|
||
preset="card"
|
||
title="登录"
|
||
style="width: 400px"
|
||
:auto-focus="false"
|
||
>
|
||
<n-form ref="loginRef" :model="form" :rules="rules" show-require-mark>
|
||
<n-alert :show-icon="false" class="tip">
|
||
关于【选择班级】的提醒:<br />
|
||
1. 如果是上课统一生成的账号,选择【相应班级】,用户名直接写自己的名字
|
||
<br />
|
||
2.
|
||
同样是上课用的号,但是没有你的班级。选择【没有我所在的班级】,用户名要写:ks班级+姓名,比如23计算机1班张三,就写ks231张三
|
||
<br />
|
||
3. 如果是自己注册的号,选择【没有我所在的班级】 <br />
|
||
</n-alert>
|
||
<n-form-item label="选择班级" path="class" :show-require-mark="false">
|
||
<n-select
|
||
v-model:value="form.class"
|
||
:options="classList"
|
||
clearable
|
||
name="class"
|
||
id="login-class"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="用户名" path="username">
|
||
<n-select
|
||
v-if="form.class"
|
||
v-model:value="form.username"
|
||
:options="classUserOptions"
|
||
:loading="classUserLoading"
|
||
clearable
|
||
filterable
|
||
:name="isClassLogin ? 'class-username' : 'username'"
|
||
:id="isClassLogin ? 'login-class-username' : 'login-username'"
|
||
placeholder="请选择姓名"
|
||
/>
|
||
<n-input
|
||
v-else
|
||
v-model:value="form.username"
|
||
autofocus
|
||
clearable
|
||
:name="isClassLogin ? 'class-username' : 'username'"
|
||
:id="isClassLogin ? 'login-class-username' : 'login-username'"
|
||
:autocomplete="isClassLogin ? 'off' : 'username'"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item label="密码" path="password">
|
||
<n-input
|
||
v-model:value="form.password"
|
||
clearable
|
||
type="password"
|
||
:name="isClassLogin ? 'class-password' : 'password'"
|
||
:id="isClassLogin ? 'login-class-password' : 'login-password'"
|
||
:autocomplete="isClassLogin ? 'new-password' : 'current-password'"
|
||
@keyup.enter="submit"
|
||
/>
|
||
</n-form-item>
|
||
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
||
<n-form-item>
|
||
<n-flex style="width: 100%">
|
||
<n-button
|
||
type="primary"
|
||
:loading="isLoading"
|
||
@click="submit"
|
||
:style="{
|
||
flex: configStore.config?.allowRegister ? '0 0 auto' : '1',
|
||
}"
|
||
>
|
||
登录
|
||
</n-button>
|
||
<n-button v-if="configStore.config?.allowRegister" @click="goSignup">
|
||
没有账号?立即注册
|
||
</n-button>
|
||
</n-flex>
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-modal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.tip {
|
||
margin-bottom: 20px;
|
||
}
|
||
</style>
|