@@ -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: 40 0px"
style = "width: 42 0px"
: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 - bot tom : 20 px ;
margin - top : 4 px ;
}
< / style >