feat: 重新设计登录框

- 记住上次选的班级(机房一节课一个班),打开就带出这个班的名单;不记姓名
- 姓名可以打字筛选,选好后光标自动跳到密码框
- 三个报错框合成一处;“管理员登录”改成“老师”;密码可以点眼睛看;加“忘了密码找老师重置”

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 12:23:56 +08:00
1 parent 1ba31844a9
commit 81e1b233ee
1 file changed
+63 -49
+63 -49
View File
@@ -2,12 +2,13 @@
<n-modal <n-modal
preset="card" preset="card"
title="登录" title="登录"
style="width: 420px" style="width: min(400px, 92vw)"
:bordered="false"
v-model:show="loginModal" v-model:show="loginModal"
> >
<n-tabs v-model:value="activeTab" @update:value="onTabChange"> <n-tabs v-model:value="activeTab" @update:value="onTabChange">
<n-tab-pane name="student" tab="学生登录"> <n-tab-pane name="student" tab="学生">
<n-form> <n-form :show-feedback="false" class="form">
<n-form-item label="班级"> <n-form-item label="班级">
<n-select <n-select
v-model:value="selectedClass" v-model:value="selectedClass"
@@ -21,37 +22,25 @@
<n-select <n-select
v-model:value="selectedUsername" v-model:value="selectedUsername"
:options="nameOptions" :options="nameOptions"
placeholder="选择姓名" filterable
:placeholder="selectedClass ? '选择或输入姓名' : '先选班级'"
:loading="namesLoading" :loading="namesLoading"
:disabled="!selectedClass || namesLoading" :disabled="!selectedClass || namesLoading"
@update:value="focusPassword"
/> />
</n-form-item> </n-form-item>
<n-form-item label="密码"> <n-form-item label="密码">
<n-input <n-input
ref="studentPasswordRef"
type="password" type="password"
show-password-on="click"
v-model:value="studentPassword" v-model:value="studentPassword"
name="password" name="password"
placeholder="输入密码,按 Enter 登录"
@keyup.enter="submitStudent" @keyup.enter="submitStudent"
/> />
</n-form-item> </n-form-item>
<n-alert <div v-if="studentError" class="error">{{ studentError }}</div>
type="error"
v-if="classesError"
class="message"
title="加载班级列表失败,请刷新重试"
/>
<n-alert
type="error"
v-if="namesError"
class="message"
title="加载姓名列表失败,请重新选择班级"
/>
<n-alert
type="error"
v-if="studentError"
class="message"
:title="studentError"
/>
<n-button <n-button
block block
type="primary" type="primary"
@@ -61,28 +50,26 @@
> >
登录 登录
</n-button> </n-button>
<div class="note">忘了密码找老师重置</div>
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="admin" tab="管理员登录"> <n-tab-pane name="admin" tab="老师">
<n-form> <n-form :show-feedback="false" class="form">
<n-form-item label="用户名"> <n-form-item label="用户名">
<n-input v-model:value="adminName" name="username" /> <n-input v-model:value="adminName" name="username" placeholder="用户名" />
</n-form-item> </n-form-item>
<n-form-item label="密码"> <n-form-item label="密码">
<n-input <n-input
type="password" type="password"
show-password-on="click"
v-model:value="adminPassword" v-model:value="adminPassword"
name="password" name="password"
placeholder="输入密码,按 Enter 登录"
@keyup.enter="submitAdmin" @keyup.enter="submitAdmin"
/> />
</n-form-item> </n-form-item>
<n-alert <div v-if="adminError" class="error">{{ adminError }}</div>
type="error"
v-if="adminError"
class="message"
:title="adminError"
/>
<n-button <n-button
block block
type="primary" type="primary"
@@ -99,14 +86,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted } from "vue" import { ref, computed, nextTick, onMounted } from "vue"
import { Account } from "../api" import { Account } from "../api"
import { loginModal } from "../store/modal" import { loginModal } from "../store/modal"
import { user } from "../store/user" import { user } from "../store/user"
import { router } from "../router" import { router } from "../router"
// Tab state // 机房的电脑一节课一个班轮流用:记住班级,不记姓名
const CLASS_KEY = "login-class"
const activeTab = ref("student") const activeTab = ref("student")
const studentPasswordRef = ref<{ focus: () => void } | null>(null)
// Student tab state // Student tab state
const selectedClass = ref<string | null>(null) const selectedClass = ref<string | null>(null)
@@ -115,10 +105,8 @@ const studentPassword = ref("")
const studentLoading = ref(false) const studentLoading = ref(false)
const studentError = ref("") const studentError = ref("")
// Classes data
const classes = ref<string[]>([]) const classes = ref<string[]>([])
const classesLoading = ref(false) const classesLoading = ref(false)
const classesError = ref(false)
const classOptions = computed(() => const classOptions = computed(() =>
classes.value.map((c) => ({ label: c, value: c })), classes.value.map((c) => ({ label: c, value: c })),
) )
@@ -126,7 +114,6 @@ const classOptions = computed(() =>
// Names data // Names data
const names = ref<{ name: string; username: string }[]>([]) const names = ref<{ name: string; username: string }[]>([])
const namesLoading = ref(false) const namesLoading = ref(false)
const namesError = ref(false)
const nameOptions = computed(() => const nameOptions = computed(() =>
names.value.map((n) => ({ label: n.name, value: n.username })), names.value.map((n) => ({ label: n.name, value: n.username })),
) )
@@ -137,28 +124,31 @@ const adminPassword = ref("")
const adminLoading = ref(false) const adminLoading = ref(false)
const adminError = ref("") const adminError = ref("")
// Load classes on mount (cached — not reloaded on tab switch) // 班级列表只在打开页面时取一次
onMounted(async () => { onMounted(async () => {
classesLoading.value = true classesLoading.value = true
classesError.value = false
try { try {
classes.value = await Account.listClasses() classes.value = await Account.listClasses()
} catch { } catch {
classesError.value = true studentError.value = "班级列表没加载出来,请刷新页面重试"
return
} finally { } finally {
classesLoading.value = false classesLoading.value = false
} }
let saved: string | null = null
try {
saved = localStorage.getItem(CLASS_KEY)
} catch {}
if (saved && classes.value.includes(saved)) {
selectedClass.value = saved
onClassChange(saved)
}
}) })
// 换标签页时清掉输入的密码和错误;选好的班级和姓名留着
function onTabChange() { function onTabChange() {
// Reset student tab state
selectedClass.value = null
selectedUsername.value = null
studentPassword.value = "" studentPassword.value = ""
studentError.value = "" studentError.value = ""
namesError.value = false
names.value = []
// Reset admin tab state
adminName.value = "" adminName.value = ""
adminPassword.value = "" adminPassword.value = ""
adminError.value = "" adminError.value = ""
@@ -167,17 +157,24 @@ function onTabChange() {
async function onClassChange(classname: string) { async function onClassChange(classname: string) {
selectedUsername.value = null selectedUsername.value = null
names.value = [] names.value = []
namesError.value = false studentError.value = ""
try {
localStorage.setItem(CLASS_KEY, classname)
} catch {}
namesLoading.value = true namesLoading.value = true
try { try {
names.value = await Account.listNamesByClass(classname) names.value = await Account.listNamesByClass(classname)
} catch { } catch {
namesError.value = true studentError.value = "姓名列表没加载出来,请重新选一次班级"
} finally { } finally {
namesLoading.value = false namesLoading.value = false
} }
} }
function focusPassword() {
nextTick(() => studentPasswordRef.value?.focus())
}
// 失败次数太多被暂时禁止登录时,显示服务端给的提示 // 失败次数太多被暂时禁止登录时,显示服务端给的提示
function loginError(err: any, fallback: string) { function loginError(err: any, fallback: string) {
return err.response?.status === 429 ? err.response.data.detail : fallback return err.response?.status === 429 ? err.response.data.detail : fallback
@@ -228,7 +225,24 @@ async function submitAdmin() {
</script> </script>
<style scoped> <style scoped>
.message { .form {
margin-bottom: 12px; display: flex;
flex-direction: column;
gap: 12px;
padding-top: 4px;
}
.error {
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--wp-bad);
background: var(--wp-bad-soft);
}
.note {
font-size: 12px;
text-align: center;
color: var(--wp-faint);
} }
</style> </style>