feat: 重新设计登录框
- 记住上次选的班级(机房一节课一个班),打开就带出这个班的名单;不记姓名 - 姓名可以打字筛选,选好后光标自动跳到密码框 - 三个报错框合成一处;“管理员登录”改成“老师”;密码可以点眼睛看;加“忘了密码找老师重置” Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ba31844a9
commit
81e1b233ee
1 file changed
+63
-49
@@ -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>
|
||||||
Reference in new issue
Block a user