登录框拆成「学生登录 / 管理员登录」两个页签。学生页签选完班级选姓名, 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">
|
<script setup lang="ts">
|
||||||
|
import { STORAGE_KEY } from "utils/constants"
|
||||||
|
import storage from "utils/storage"
|
||||||
import { getClassUsernames, login } from "../api"
|
import { getClassUsernames, login } from "../api"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { useAuthModalStore } from "../store/authModal"
|
import { useAuthModalStore } from "../store/authModal"
|
||||||
@@ -15,41 +17,87 @@ const {
|
|||||||
loginLoading: isLoading,
|
loginLoading: isLoading,
|
||||||
loginError: msg,
|
loginError: msg,
|
||||||
} = storeToRefs(authStore)
|
} = 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 classUserOptions = ref<SelectOption[]>([])
|
||||||
const classUserLoading = ref(false)
|
const classUserLoading = ref(false)
|
||||||
const isClassLogin = computed(() => Boolean(form.value.class))
|
// 空串是「没有我所在的班级」,null 才是没选 —— 往届的班级毕业后会从后台配置里删掉,
|
||||||
|
// 这一项是他们唯一的入口,写完整用户名登录
|
||||||
|
const OTHER_CLASS = ""
|
||||||
const classList = computed<SelectOption[]>(() => {
|
const classList = computed<SelectOption[]>(() => {
|
||||||
const defaults = [{ label: "没有我所在的班级", value: "" }]
|
|
||||||
const configs =
|
const configs =
|
||||||
configStore.config?.classList.map((item) => ({
|
configStore.config?.classList.map((item) => ({
|
||||||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||||||
value: `ks${item}`,
|
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: [
|
username: [
|
||||||
{ required: true, message: "用户名必填", trigger: ["blur", "change"] },
|
{ required: true, message: "用户名必填", trigger: ["blur", "change"] },
|
||||||
],
|
],
|
||||||
password: [
|
password: passwordRule,
|
||||||
{ required: true, message: "密码必填", trigger: "blur" },
|
|
||||||
{ min: 6, max: 20, message: "长度在 6 到 20 位之间", trigger: "input" },
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
function resetForm() {
|
||||||
loginRef.value!.validate(async (errors?: unknown) => {
|
form.value.class = null
|
||||||
if (!errors) {
|
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 {
|
try {
|
||||||
authStore.clearLoginError()
|
authStore.clearLoginError()
|
||||||
authStore.setLoginLoading(true)
|
authStore.setLoginLoading(true)
|
||||||
const merged = {
|
const merged = {
|
||||||
username: form.value.username,
|
username: form.value.username ?? "",
|
||||||
password: form.value.password,
|
password: form.value.password,
|
||||||
}
|
}
|
||||||
if (form.value.class) {
|
// 选了班级的只填了姓名,ks + 班级号的前缀这里补
|
||||||
merged.username = form.value.class + form.value.username
|
if (activeTab.value === "student" && form.value.class) {
|
||||||
|
merged.username = form.value.class + merged.username
|
||||||
}
|
}
|
||||||
await login(merged)
|
await login(merged)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -65,10 +113,12 @@ async function submit() {
|
|||||||
authStore.setLoginLoading(false)
|
authStore.setLoginLoading(false)
|
||||||
}
|
}
|
||||||
if (!msg.value) {
|
if (!msg.value) {
|
||||||
|
if (activeTab.value === "student") {
|
||||||
|
storage.set(STORAGE_KEY.LOGIN_CLASS, form.value.class)
|
||||||
|
}
|
||||||
authStore.closeLoginModal()
|
authStore.closeLoginModal()
|
||||||
await userStore.getMyProfile()
|
await userStore.getMyProfile()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +145,7 @@ watch(
|
|||||||
() => form.value.class,
|
() => form.value.class,
|
||||||
(selectedClass) => {
|
(selectedClass) => {
|
||||||
classUserOptions.value = []
|
classUserOptions.value = []
|
||||||
form.value.username = ""
|
form.value.username = null
|
||||||
if (!selectedClass) {
|
if (!selectedClass) {
|
||||||
classUserLoading.value = false
|
classUserLoading.value = false
|
||||||
return
|
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(() => {
|
onMounted(() => {
|
||||||
authStore.clearLoginError()
|
authStore.clearLoginError()
|
||||||
|
restoreLastClass()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -115,52 +197,56 @@ onMounted(() => {
|
|||||||
v-model:show="loginModalOpen"
|
v-model:show="loginModalOpen"
|
||||||
preset="card"
|
preset="card"
|
||||||
title="登录"
|
title="登录"
|
||||||
style="width: 400px"
|
style="width: 420px"
|
||||||
:auto-focus="false"
|
:auto-focus="false"
|
||||||
>
|
>
|
||||||
<n-form ref="loginRef" :model="form" :rules="rules" show-require-mark>
|
<n-tabs v-model:value="activeTab" @update:value="onTabChange">
|
||||||
<n-alert :show-icon="false" class="tip">
|
<n-tab-pane name="student" tab="学生登录">
|
||||||
关于【选择班级】的提醒:<br />
|
<n-form
|
||||||
1. 如果是上课统一生成的账号,选择【相应班级】,用户名直接写自己的名字
|
ref="studentRef"
|
||||||
<br />
|
:model="form"
|
||||||
2.
|
:rules="studentRules"
|
||||||
同样是上课用的号,但是没有你的班级。选择【没有我所在的班级】,用户名要写:ks班级+姓名,比如23计算机1班张三,就写ks231张三
|
show-require-mark
|
||||||
<br />
|
>
|
||||||
3. 如果是自己注册的号,选择【没有我所在的班级】 <br />
|
<n-form-item label="班级" path="class">
|
||||||
</n-alert>
|
|
||||||
<n-form-item label="选择班级" path="class" :show-require-mark="false">
|
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="form.class"
|
v-model:value="form.class"
|
||||||
:options="classList"
|
:options="classList"
|
||||||
clearable
|
filterable
|
||||||
name="class"
|
name="class"
|
||||||
id="login-class"
|
id="login-class"
|
||||||
|
placeholder="选择班级"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="用户名" path="username">
|
<n-form-item
|
||||||
|
:label="isClassLogin ? '姓名' : '用户名'"
|
||||||
|
path="username"
|
||||||
|
>
|
||||||
<n-select
|
<n-select
|
||||||
v-if="form.class"
|
v-if="isClassLogin"
|
||||||
v-model:value="form.username"
|
v-model:value="form.username"
|
||||||
:options="classUserOptions"
|
:options="classUserOptions"
|
||||||
:loading="classUserLoading"
|
:loading="classUserLoading"
|
||||||
clearable
|
|
||||||
filterable
|
filterable
|
||||||
:name="isClassLogin ? 'class-username' : 'username'"
|
name="class-username"
|
||||||
:id="isClassLogin ? 'login-class-username' : 'login-username'"
|
id="login-class-username"
|
||||||
placeholder="请选择姓名"
|
placeholder="选择姓名"
|
||||||
|
@update:value="onUsernamePicked"
|
||||||
/>
|
/>
|
||||||
<n-input
|
<n-input
|
||||||
v-else
|
v-else
|
||||||
v-model:value="form.username"
|
v-model:value="form.username"
|
||||||
autofocus
|
|
||||||
clearable
|
clearable
|
||||||
:name="isClassLogin ? 'class-username' : 'username'"
|
:disabled="form.class === null"
|
||||||
:id="isClassLogin ? 'login-class-username' : 'login-username'"
|
name="username"
|
||||||
:autocomplete="isClassLogin ? 'off' : 'username'"
|
id="login-username"
|
||||||
|
autocomplete="username"
|
||||||
|
placeholder="完整用户名,如 ks231张三"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="密码" path="password">
|
<n-form-item label="密码" path="password">
|
||||||
<n-input
|
<n-input
|
||||||
|
ref="studentPasswordRef"
|
||||||
v-model:value="form.password"
|
v-model:value="form.password"
|
||||||
clearable
|
clearable
|
||||||
type="password"
|
type="password"
|
||||||
@@ -170,7 +256,9 @@ onMounted(() => {
|
|||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-alert v-if="msg" type="error" :show-icon="false"> {{ msg }}</n-alert>
|
<n-alert v-if="msg" type="error" :show-icon="false">{{
|
||||||
|
msg
|
||||||
|
}}</n-alert>
|
||||||
<n-form-item>
|
<n-form-item>
|
||||||
<n-flex style="width: 100%">
|
<n-flex style="width: 100%">
|
||||||
<n-button
|
<n-button
|
||||||
@@ -183,17 +271,68 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="configStore.config?.allowRegister" @click="goSignup">
|
<n-button
|
||||||
|
v-if="configStore.config?.allowRegister"
|
||||||
|
@click="goSignup"
|
||||||
|
>
|
||||||
没有账号?立即注册
|
没有账号?立即注册
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-alert :show-icon="false" class="tip">
|
||||||
|
往届的班级、列表里找不到的班级、以及自己注册的账号,选【没有我所在的班级】,用户名要写完整,比如
|
||||||
|
23 计算机 1 班张三写 ks231张三。
|
||||||
|
</n-alert>
|
||||||
</n-form>
|
</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>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tip {
|
.tip {
|
||||||
margin-bottom: 20px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ export const useAuthModalStore = defineStore("authModal", () => {
|
|||||||
const signupModalOpen = ref(false)
|
const signupModalOpen = ref(false)
|
||||||
|
|
||||||
// ==================== 登录表单状态 ====================
|
// ==================== 登录表单状态 ====================
|
||||||
|
// class 为 null 表示还没选,别用空串 —— 空串是「没有我所在的班级」那一项的值。
|
||||||
|
// username 同理用 null:空串在 n-select 里是「选了一个空值」,占位符不显示。
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
class: "",
|
class: null as string | null,
|
||||||
username: "",
|
username: null as string | null,
|
||||||
password: "",
|
password: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
function clearProfile() {
|
function clearProfile() {
|
||||||
profile.value = null
|
profile.value = null
|
||||||
demoMode.value = false
|
demoMode.value = false
|
||||||
|
// 登录框记的班级要跨退登活下来:机房一台机器对一个班,下课登出、下节课再来
|
||||||
|
// 还是同一个班,清掉的话每个人都得重新选一遍。它是机器的属性,不是谁的隐私。
|
||||||
|
const loginClass = storage.get(STORAGE_KEY.LOGIN_CLASS)
|
||||||
storage.clear()
|
storage.clear()
|
||||||
|
if (loginClass !== null) storage.set(STORAGE_KEY.LOGIN_CLASS, loginClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退登的两步(吊销服务端会话、清本地状态)绑在一起:只清本地会留一个还活着
|
// 退登的两步(吊销服务端会话、清本地状态)绑在一起:只清本地会留一个还活着
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const STORAGE_KEY = {
|
|||||||
ADMIN_PROBLEM: "adminProblem",
|
ADMIN_PROBLEM: "adminProblem",
|
||||||
ADMIN_PROBLEM_TAGS: "adminProblemTags",
|
ADMIN_PROBLEM_TAGS: "adminProblemTags",
|
||||||
DEMO_MODE: "demoMode",
|
DEMO_MODE: "demoMode",
|
||||||
|
LOGIN_CLASS: "loginClass",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DIFFICULTY = {
|
export const DIFFICULTY = {
|
||||||
|
|||||||
Reference in New Issue
Block a user