feat(登录): 姓名支持拼音和拼音缩写搜索
Some checks failed
Deploy / deploy (push) Has been cancelled

机房电脑打中文要切输入法,选姓名前先切一次法太别扭。现在 zhangsan、zs、
xiaoxian 都能搜到张三/曾小贤,中文照旧能打。

用 pinyin-pro 的 match 而不是自己拼一份拼音串比对:多音字它两个读音都认,
单田芳搜 stf 和 dtf 都出得来,曾小贤搜 zxx 和 cengxiaoxian 也都出得来 ——
自己拼只会留下默认读音那一个,姓氏恰恰是多音字重灾区。

词典有 85 KB(gzip),静态引入会压进首屏,所以改成打开登录框才 import。
已登录的人一次都不会下载(构建产物里它是独立 chunk,只被 default 布局
动态引用;实测页面加载 0 个请求,打开登录框才有 1 个)。拉不下来时退回
纯中文匹配,不至于让人登不上。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQCLjAmz6pPXMNGoZmvAmC
This commit is contained in:
2026-09-01 07:25:21 -06:00
parent 047e6dd7d3
commit 4654615f89
3 changed files with 30 additions and 1 deletions

View File

@@ -48,6 +48,7 @@
"nanoid": "^6.0.1", "nanoid": "^6.0.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^4.0.3", "pinia": "^4.0.3",
"pinyin-pro": "^3.29.3",
"skulpt": "^1.2.0", "skulpt": "^1.2.0",
"vue": "^3.5.41", "vue": "^3.5.41",
"vue-chartjs": "^5.3.4", "vue-chartjs": "^5.3.4",

View File

@@ -26,6 +26,28 @@ const adminRef = useTemplateRef("adminRef")
const studentPasswordRef = useTemplateRef("studentPasswordRef") const studentPasswordRef = useTemplateRef("studentPasswordRef")
const classUserOptions = ref<SelectOption[]>([]) const classUserOptions = ref<SelectOption[]>([])
const classUserLoading = ref(false) const classUserLoading = ref(false)
// 姓名支持拼音和拼音缩写搜索(张三 ← zhangsan / zs。pinyin-pro 带着一份几百 K 的
// 词典,静态引入会压进首屏 —— 改成打开登录框才拉,已登录的人一次都不会下载。
// 用 match 而不是自己拼拼音串:多音字它两个读音都认(单田芳 ← stf 和 dtf 都能搜到)。
type MatchFn = typeof import("pinyin-pro").match
const pinyinMatch = shallowRef<MatchFn | null>(null)
async function ensurePinyin() {
if (pinyinMatch.value) return
try {
pinyinMatch.value = (await import("pinyin-pro")).match
} catch {
// 拉不下来就只剩中文匹配,不至于让人登不上
}
}
function filterClassUser(pattern: string, option: SelectOption) {
const keyword = pattern.trim()
if (!keyword) return true
const label = String(option.label ?? "")
if (label.includes(keyword)) return true
return pinyinMatch.value
? pinyinMatch.value(label, keyword, { continuous: true }) !== null
: false
}
// 空串是「没有我所在的班级」null 才是没选 —— 往届的班级毕业后会从后台配置里删掉, // 空串是「没有我所在的班级」null 才是没选 —— 往届的班级毕业后会从后台配置里删掉,
// 这一项是他们唯一的入口,写完整用户名登录 // 这一项是他们唯一的入口,写完整用户名登录
const OTHER_CLASS = "" const OTHER_CLASS = ""
@@ -179,6 +201,7 @@ watch(classList, restoreLastClass)
// 每次打开都从干净的表单开始,别把上一个人的姓名和密码留在框里 // 每次打开都从干净的表单开始,别把上一个人的姓名和密码留在框里
watch(loginModalOpen, (open) => { watch(loginModalOpen, (open) => {
if (!open) return if (!open) return
ensurePinyin()
resetForm() resetForm()
studentRef.value?.restoreValidation() studentRef.value?.restoreValidation()
adminRef.value?.restoreValidation() adminRef.value?.restoreValidation()
@@ -188,6 +211,7 @@ watch(loginModalOpen, (open) => {
onMounted(() => { onMounted(() => {
authStore.clearLoginError() authStore.clearLoginError()
restoreLastClass() restoreLastClass()
if (loginModalOpen.value) ensurePinyin()
}) })
</script> </script>
@@ -228,9 +252,10 @@ onMounted(() => {
:options="classUserOptions" :options="classUserOptions"
:loading="classUserLoading" :loading="classUserLoading"
filterable filterable
:filter="filterClassUser"
name="class-username" name="class-username"
id="login-class-username" id="login-class-username"
placeholder="选择姓名" placeholder="选择姓名,也可以打拼音"
@update:value="onUsernamePicked" @update:value="onUsernamePicked"
/> />
<n-input <n-input

View File

@@ -75,6 +75,7 @@
"nanoid": "^6.0.1", "nanoid": "^6.0.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"pinia": "^4.0.3", "pinia": "^4.0.3",
"pinyin-pro": "^3.29.3",
"skulpt": "^1.2.0", "skulpt": "^1.2.0",
"vue": "^3.5.41", "vue": "^3.5.41",
"vue-chartjs": "^5.3.4", "vue-chartjs": "^5.3.4",
@@ -1241,6 +1242,8 @@
"pinia": ["pinia@4.0.3", "https://registry.npmjs.com/pinia/-/pinia-4.0.3.tgz", { "dependencies": { "nostics": "^1.1.4" }, "peerDependencies": { "@vue/devtools-api": "^8.1.5", "typescript": ">=5.6.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA=="], "pinia": ["pinia@4.0.3", "https://registry.npmjs.com/pinia/-/pinia-4.0.3.tgz", { "dependencies": { "nostics": "^1.1.4" }, "peerDependencies": { "@vue/devtools-api": "^8.1.5", "typescript": ">=5.6.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA=="],
"pinyin-pro": ["pinyin-pro@3.29.3", "", {}, "sha512-+UU9bx6vfDw8amOJGHm0TE0rdQl8VPylsDWviQ5OOQ3e+on1xRP4OqDbiDuMT5OISgvfl/Y6ez1BBRaIP80GLQ=="],
"pkg-types": ["pkg-types@2.3.1", "https://registry.npmjs.com/pkg-types/-/pkg-types-2.3.1.tgz", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], "pkg-types": ["pkg-types@2.3.1", "https://registry.npmjs.com/pkg-types/-/pkg-types-2.3.1.tgz", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
"points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmjs.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmjs.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],