登录框拆成「学生登录 / 管理员登录」两个页签。学生页签选完班级选姓名, ks 前缀提交时自动拼,选完姓名焦点直接跳到密码框,回车登录;上次选的班级 记在 localStorage 下次自动填好(姓名不记 —— 同一台机器换个人坐就是别人)。 管理员页签只有用户名+密码,不拼前缀。 「没有我所在的班级」保留在班级列表末尾,往届的班级从后台 class_list 里删掉后 学生仍能从这里写完整用户名登录。为了把「还没选」和「没有我所在的班级」分开, loginForm.class 的初值从空串改成 null —— 原来两者都是空串,一打开就默认落在 「没有我所在的班级」那一档、显示文本框,正好和「方便学生」相反。username 同样 改成 nullable:空串在 n-select 里是「选了一个空值」,占位符不显示。 clearProfile 的 storage.clear() 给 LOGIN_CLASS 开了个口子:机房一台机器对一个班, 下课登出、下节课再来还是同一个班,清掉的话每个人都得重新选一遍。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQCLjAmz6pPXMNGoZmvAmC
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { STORAGE_KEY } from "utils/constants"
|
||||
import storage from "utils/storage"
|
||||
import { getClassUsernames, login } from "../api"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useAuthModalStore } from "../store/authModal"
|
||||
@@ -15,59 +17,107 @@ const {
|
||||
loginLoading: isLoading,
|
||||
loginError: msg,
|
||||
} = storeToRefs(authStore)
|
||||
const loginRef = useTemplateRef("loginRef")
|
||||
|
||||
// 学生页签走「班级 + 姓名」两个下拉,一个字都不用打;
|
||||
// 管理员页签只有用户名 + 密码,不拼班级前缀。
|
||||
const activeTab = ref<"student" | "admin">("student")
|
||||
const studentRef = useTemplateRef("studentRef")
|
||||
const adminRef = useTemplateRef("adminRef")
|
||||
const studentPasswordRef = useTemplateRef("studentPasswordRef")
|
||||
const classUserOptions = ref<SelectOption[]>([])
|
||||
const classUserLoading = ref(false)
|
||||
const isClassLogin = computed(() => Boolean(form.value.class))
|
||||
// 空串是「没有我所在的班级」,null 才是没选 —— 往届的班级毕业后会从后台配置里删掉,
|
||||
// 这一项是他们唯一的入口,写完整用户名登录
|
||||
const OTHER_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]
|
||||
return [...configs, { label: "没有我所在的班级", value: OTHER_CLASS }]
|
||||
})
|
||||
const rules: FormRules = {
|
||||
// 选了具体班级就是「选姓名」,选了「没有我所在的班级」就是「写用户名」
|
||||
const isClassLogin = computed(() => Boolean(form.value.class))
|
||||
const passwordRule: FormItemRule[] = [
|
||||
{ required: true, message: "密码必填", trigger: "blur" },
|
||||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
||||
]
|
||||
const studentRules = computed<FormRules>(() => ({
|
||||
class: [
|
||||
{
|
||||
validator: () => form.value.class !== null,
|
||||
message: "班级必选",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
],
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
message: isClassLogin.value ? "姓名必选" : "用户名必填",
|
||||
trigger: ["blur", "change"],
|
||||
},
|
||||
],
|
||||
password: passwordRule,
|
||||
}))
|
||||
const adminRules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: "用户名必填", trigger: ["blur", "change"] },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "密码必填", trigger: "blur" },
|
||||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
||||
],
|
||||
password: passwordRule,
|
||||
}
|
||||
|
||||
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)
|
||||
function resetForm() {
|
||||
form.value.class = null
|
||||
form.value.username = null
|
||||
form.value.password = ""
|
||||
classUserOptions.value = []
|
||||
classUserLoading.value = false
|
||||
authStore.clearLoginError()
|
||||
}
|
||||
|
||||
function onTabChange(tab: "student" | "admin") {
|
||||
resetForm()
|
||||
studentRef.value?.restoreValidation()
|
||||
adminRef.value?.restoreValidation()
|
||||
// 切回学生页签把记住的班级填回去,别因为误点了一下管理员就得重选
|
||||
if (tab === "student") restoreLastClass()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const formRef = activeTab.value === "student" ? studentRef : adminRef
|
||||
formRef.value!.validate(async (errors?: unknown) => {
|
||||
if (errors) return
|
||||
try {
|
||||
authStore.clearLoginError()
|
||||
authStore.setLoginLoading(true)
|
||||
const merged = {
|
||||
username: form.value.username ?? "",
|
||||
password: form.value.password,
|
||||
}
|
||||
if (!msg.value) {
|
||||
authStore.closeLoginModal()
|
||||
await userStore.getMyProfile()
|
||||
// 选了班级的只填了姓名,ks + 班级号的前缀这里补
|
||||
if (activeTab.value === "student" && form.value.class) {
|
||||
merged.username = form.value.class + merged.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) {
|
||||
if (activeTab.value === "student") {
|
||||
storage.set(STORAGE_KEY.LOGIN_CLASS, form.value.class)
|
||||
}
|
||||
authStore.closeLoginModal()
|
||||
await userStore.getMyProfile()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -95,7 +145,7 @@ watch(
|
||||
() => form.value.class,
|
||||
(selectedClass) => {
|
||||
classUserOptions.value = []
|
||||
form.value.username = ""
|
||||
form.value.username = null
|
||||
if (!selectedClass) {
|
||||
classUserLoading.value = false
|
||||
return
|
||||
@@ -104,8 +154,40 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 选完姓名直接跳到密码框,学生的手不用离开这一列
|
||||
function onUsernamePicked() {
|
||||
nextTick(() => studentPasswordRef.value?.focus())
|
||||
}
|
||||
|
||||
/**
|
||||
* 机房电脑一台对一个班,班级记在本地下次自动填上;
|
||||
* 姓名不记 —— 同一台机器换个人坐就是别人的名字了。
|
||||
*/
|
||||
function restoreLastClass() {
|
||||
if (form.value.class !== null) return
|
||||
const last = storage.get(STORAGE_KEY.LOGIN_CLASS)
|
||||
if (
|
||||
typeof last === "string" &&
|
||||
classList.value.some((item) => item.value === last)
|
||||
) {
|
||||
form.value.class = last
|
||||
}
|
||||
}
|
||||
|
||||
watch(classList, restoreLastClass)
|
||||
|
||||
// 每次打开都从干净的表单开始,别把上一个人的姓名和密码留在框里
|
||||
watch(loginModalOpen, (open) => {
|
||||
if (!open) return
|
||||
resetForm()
|
||||
studentRef.value?.restoreValidation()
|
||||
adminRef.value?.restoreValidation()
|
||||
restoreLastClass()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
authStore.clearLoginError()
|
||||
restoreLastClass()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -115,85 +197,142 @@ onMounted(() => {
|
||||
v-model:show="loginModalOpen"
|
||||
preset="card"
|
||||
title="登录"
|
||||
style="width: 400px"
|
||||
style="width: 420px"
|
||||
: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-tabs v-model:value="activeTab" @update:value="onTabChange">
|
||||
<n-tab-pane name="student" tab="学生登录">
|
||||
<n-form
|
||||
ref="studentRef"
|
||||
:model="form"
|
||||
:rules="studentRules"
|
||||
show-require-mark
|
||||
>
|
||||
<n-form-item label="班级" path="class">
|
||||
<n-select
|
||||
v-model:value="form.class"
|
||||
:options="classList"
|
||||
filterable
|
||||
name="class"
|
||||
id="login-class"
|
||||
placeholder="选择班级"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
:label="isClassLogin ? '姓名' : '用户名'"
|
||||
path="username"
|
||||
>
|
||||
登录
|
||||
</n-button>
|
||||
<n-button v-if="configStore.config?.allowRegister" @click="goSignup">
|
||||
没有账号?立即注册
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-select
|
||||
v-if="isClassLogin"
|
||||
v-model:value="form.username"
|
||||
:options="classUserOptions"
|
||||
:loading="classUserLoading"
|
||||
filterable
|
||||
name="class-username"
|
||||
id="login-class-username"
|
||||
placeholder="选择姓名"
|
||||
@update:value="onUsernamePicked"
|
||||
/>
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="form.username"
|
||||
clearable
|
||||
:disabled="form.class === null"
|
||||
name="username"
|
||||
id="login-username"
|
||||
autocomplete="username"
|
||||
placeholder="完整用户名,如 ks231张三"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input
|
||||
ref="studentPasswordRef"
|
||||
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-alert :show-icon="false" class="tip">
|
||||
往届的班级、列表里找不到的班级、以及自己注册的账号,选【没有我所在的班级】,用户名要写完整,比如
|
||||
23 计算机 1 班张三写 ks231张三。
|
||||
</n-alert>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="admin" tab="管理员登录">
|
||||
<n-form
|
||||
ref="adminRef"
|
||||
:model="form"
|
||||
:rules="adminRules"
|
||||
show-require-mark
|
||||
>
|
||||
<n-form-item label="用户名" path="username">
|
||||
<n-input
|
||||
v-model:value="form.username"
|
||||
clearable
|
||||
name="username"
|
||||
id="login-admin-username"
|
||||
autocomplete="username"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input
|
||||
v-model:value="form.password"
|
||||
clearable
|
||||
type="password"
|
||||
name="password"
|
||||
id="login-admin-password"
|
||||
autocomplete="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-button block type="primary" :loading="isLoading" @click="submit">
|
||||
登录
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-alert :show-icon="false" class="tip">
|
||||
管理员和老师从这里登录,学生请走【学生登录】页签。
|
||||
</n-alert>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tip {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,9 +10,11 @@ export const useAuthModalStore = defineStore("authModal", () => {
|
||||
const signupModalOpen = ref(false)
|
||||
|
||||
// ==================== 登录表单状态 ====================
|
||||
// class 为 null 表示还没选,别用空串 —— 空串是「没有我所在的班级」那一项的值。
|
||||
// username 同理用 null:空串在 n-select 里是「选了一个空值」,占位符不显示。
|
||||
const loginForm = reactive({
|
||||
class: "",
|
||||
username: "",
|
||||
class: null as string | null,
|
||||
username: null as string | null,
|
||||
password: "",
|
||||
})
|
||||
|
||||
|
||||
@@ -82,7 +82,11 @@ export const useUserStore = defineStore("user", () => {
|
||||
function clearProfile() {
|
||||
profile.value = null
|
||||
demoMode.value = false
|
||||
// 登录框记的班级要跨退登活下来:机房一台机器对一个班,下课登出、下节课再来
|
||||
// 还是同一个班,清掉的话每个人都得重新选一遍。它是机器的属性,不是谁的隐私。
|
||||
const loginClass = storage.get(STORAGE_KEY.LOGIN_CLASS)
|
||||
storage.clear()
|
||||
if (loginClass !== null) storage.set(STORAGE_KEY.LOGIN_CLASS, loginClass)
|
||||
}
|
||||
|
||||
// 退登的两步(吊销服务端会话、清本地状态)绑在一起:只清本地会留一个还活着
|
||||
|
||||
Reference in New Issue
Block a user