feat(账号): 新建的号不再加 ks + 班号前缀,班级单独选;学生改不了真名了
- 批量建号先选班再贴名单,用户名就是名字;被别的班或注册的号占了自动加数字(xuyue2)。 查重改成查「这个班里有没有同名的」,老号按剥掉 ks 前缀的名字比 - 后台编辑、新建用户能直接选班级;以前班级只能从用户名的 ks 前缀推,改不了 - 登录点名字时名单接口一并给出用户名,新号老号都登得上;本机记的班级兼容旧的 ks257 写法 - 筛选、抽人、「看看他们的写法」都按班级认人,不再拼 ks 前缀(新号拼出来永远圈不进去) - 接口 PUT /me/profile 不再收真名:界面早没入口,但早几届有 44 人把真名改成了外号 - 0042:两个被推成非班号班级的老号(xrm、王仁杰)改成没填班级 - 老账号不改名,只认 ks 前缀的地方收在契约 username.ts 一处 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48590eee60
commit
221d264701
25 files changed
+4234
-156
No files matched your search
@@ -103,6 +103,7 @@ export function getBaseInfo() {
|
||||
return api.get<DashboardInfo>('admin/dashboard')
|
||||
}
|
||||
|
||||
/** 后台首页「抽一个人」:从这个班(班号)随机拿 10 个显示用的名字 */
|
||||
export function randomUser10(classroom: string) {
|
||||
return api.get<string[]>('admin/random-usernames', {
|
||||
params: { classroom },
|
||||
@@ -245,9 +246,11 @@ export function getUserList(
|
||||
}
|
||||
|
||||
/** 批量建号之前查重:这些用户名哪些已经有了 */
|
||||
export function checkUsernames(usernames: string[]) {
|
||||
/** 批量建号查重:这个班里已经有的同名账号,和每个名字会建成的用户名 */
|
||||
export function checkUsernames(className: string, names: string[]) {
|
||||
return api.post<CheckUsernamesResponse>('admin/users/check', {
|
||||
usernames,
|
||||
className,
|
||||
names,
|
||||
} satisfies CheckUsernamesRequest)
|
||||
}
|
||||
|
||||
@@ -260,6 +263,7 @@ export function editUser(user: AdminUserForm) {
|
||||
adminType: toAdminType(user.adminType),
|
||||
problemPermission: toProblemPermission(user.problemPermission),
|
||||
realName: user.realName ?? null,
|
||||
className: user.className ?? null,
|
||||
isDisabled: user.isDisabled,
|
||||
password: user.password ?? '',
|
||||
} satisfies UpdateUserRequest)
|
||||
|
||||
@@ -118,8 +118,8 @@ function stopRolling() {
|
||||
/** 名字滚两秒停下:接口一次给 10 个随机的,最后一个就是抽中的 */
|
||||
async function draw() {
|
||||
if (!luckyClass.value) return
|
||||
const prefix = `ks${luckyClass.value}`
|
||||
const names = (await randomUser10(prefix)).map((name) => name.slice(prefix.length) || name)
|
||||
// 后端按班级随机拿 10 个,回的就是显示的名字(老号已经剥掉 ks 前缀)
|
||||
const names = await randomUser10(luckyClass.value)
|
||||
if (!names.length) {
|
||||
luckyGuy.value = '这个班没有人'
|
||||
return
|
||||
|
||||
@@ -34,7 +34,8 @@ const classError = computed(() => {
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const prefix = computed(() => (classNo.value.trim() ? `ks${classNo.value.trim()}` : ''))
|
||||
/** 填好了、格式也对的班号;没填或填错是空串 */
|
||||
const klass = computed(() => (classNo.value.trim() && !classError.value ? classNo.value.trim() : ''))
|
||||
|
||||
/** 名单:一行一个,前后和中间的空格都去掉(从表格里复制常带空格),空行不算 */
|
||||
const names = computed(() =>
|
||||
@@ -56,11 +57,16 @@ function passwordOf(name: string) {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名就是名字(2026-11 起不再加 ks + 班号)。名字被别的班、注册的号占了,后端给的是加了数字的(杨涵宇2);
|
||||
* 查重还没回来之前先按名字摆着
|
||||
*/
|
||||
const suggested = ref<Map<string, string>>(new Map())
|
||||
const rows = computed(() =>
|
||||
names.value.map((name) => ({
|
||||
name,
|
||||
username: prefix.value + name,
|
||||
password: passwordOf(prefix.value + name),
|
||||
username: suggested.value.get(name) ?? name,
|
||||
password: passwordOf(name),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -69,7 +75,7 @@ const duplicates = computed(() => {
|
||||
const first = new Map<string, number>()
|
||||
const result: { line: number; firstLine: number; name: string }[] = []
|
||||
rows.value.forEach((row, i) => {
|
||||
const key = row.username.toLowerCase()
|
||||
const key = row.name.toLowerCase()
|
||||
const seen = first.get(key)
|
||||
if (seen === undefined) first.set(key, i + 1)
|
||||
else result.push({ line: i + 1, firstLine: seen, name: row.name })
|
||||
@@ -77,22 +83,26 @@ const duplicates = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
// 已经有的账号:停手半秒再去查
|
||||
const existing = ref<{ username: string; createTime: string | null }[]>([])
|
||||
// 这个班已经有的同名账号(老号按剥掉 ks 前缀的名字比),顺带拿回每个名字会建成的用户名:停手半秒再去查
|
||||
const existing = ref<{ name: string; username: string; createTime: string | null }[]>([])
|
||||
const checking = ref(false)
|
||||
watchDebounced(
|
||||
() => rows.value.map((row) => row.username).join('\n'),
|
||||
() => `${klass.value}\n${names.value.join('\n')}`,
|
||||
async () => {
|
||||
const usernames = [...new Set(rows.value.map((row) => row.username))]
|
||||
if (!usernames.length || classError.value || usernames.length > 500) {
|
||||
const list = [...new Set(names.value)]
|
||||
if (!list.length || !klass.value || list.length > 500) {
|
||||
existing.value = []
|
||||
suggested.value = new Map()
|
||||
return
|
||||
}
|
||||
checking.value = true
|
||||
try {
|
||||
existing.value = (await checkUsernames(usernames)).existing
|
||||
const res = await checkUsernames(klass.value, list)
|
||||
existing.value = res.existing
|
||||
suggested.value = new Map(list.map((name, i) => [name, res.usernames[i] ?? name]))
|
||||
} catch {
|
||||
existing.value = []
|
||||
suggested.value = new Map()
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
@@ -102,7 +112,7 @@ watchDebounced(
|
||||
|
||||
const tooMany = computed(() => rows.value.length > 500)
|
||||
const issueCount = computed(() => (classError.value ? 1 : 0) + duplicates.value.length + existing.value.length + (tooMany.value ? 1 : 0))
|
||||
const canSubmit = computed(() => rows.value.length > 0 && !issueCount.value && !checking.value && !!prefix.value)
|
||||
const canSubmit = computed(() => rows.value.length > 0 && !issueCount.value && !checking.value && !!klass.value)
|
||||
|
||||
// ---------------------------------------------------------------- 建号
|
||||
|
||||
@@ -118,8 +128,9 @@ async function submit() {
|
||||
const payload: ImportUsersRequest['users'] = rows.value.map((row, i) => [
|
||||
row.username,
|
||||
row.password,
|
||||
`${prefix.value || 'user'}.${batch}.${i + 1}@example.com`,
|
||||
`c${klass.value}.${batch}.${i + 1}@example.com`,
|
||||
row.name,
|
||||
klass.value,
|
||||
])
|
||||
saving.value = true
|
||||
try {
|
||||
@@ -142,7 +153,7 @@ function again() {
|
||||
|
||||
function download() {
|
||||
if (!done.value) return
|
||||
downloadAccountsCsv(done.value, prefix.value || '账号')
|
||||
downloadAccountsCsv(done.value, klass.value ? classLabel(klass.value) : '账号')
|
||||
}
|
||||
|
||||
/** 打印账号条:一人一张,一页四列,打出来剪开发下去 */
|
||||
@@ -196,7 +207,6 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
<div class="left">
|
||||
<label class="label" for="class-no">班级号</label>
|
||||
<div class="class-row">
|
||||
<span class="ks">ks</span>
|
||||
<n-input v-model:value="classNo" :input-props="{ id: 'class-no', inputmode: 'numeric' }" placeholder="如 2612" style="width: 120px" />
|
||||
<span v-if="classNo.trim() && !classError" class="class-name">
|
||||
{{ classLabel(classNo.trim()) }}
|
||||
@@ -210,7 +220,7 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
<template v-if="rows.length">
|
||||
<div class="head">
|
||||
<b>会建这 {{ rows.length }} 个账号</b>
|
||||
<n-tag v-if="!issueCount && !checking && prefix" type="success" :bordered="false" round> 没有重名 </n-tag>
|
||||
<n-tag v-if="!issueCount && !checking && klass" type="success" :bordered="false" round> 没有重名 </n-tag>
|
||||
<n-tag v-if="issueCount" type="warning" :bordered="false" round> 有 {{ issueCount }} 处要先改 </n-tag>
|
||||
</div>
|
||||
|
||||
@@ -221,7 +231,7 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
第 {{ dup.line }} 行「{{ dup.name }}」和第 {{ dup.firstLine }} 行重名 —— 同班同名的,名字后面加个 1、2 区分
|
||||
</div>
|
||||
<div v-for="it in existing" :key="it.username">
|
||||
{{ it.username }} 已经有这个账号了<template v-if="it.createTime">({{ parseTime(it.createTime, 'YYYY-MM-DD') }} 建的)</template>
|
||||
{{ classLabel(klass) }}已经有「{{ it.name }}」了<template v-if="it.createTime">({{ parseTime(it.createTime, 'YYYY-MM-DD') }} 建的)</template>
|
||||
—— 删掉这一行,或者换个名字
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +259,7 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
<n-button type="primary" size="large" :disabled="!canSubmit" :loading="saving || checking" @click="submit">
|
||||
建好这 {{ rows.length }} 个账号
|
||||
</n-button>
|
||||
<n-text v-if="!prefix" depth="3" class="hint" style="margin-left: 12px"> 先填班级号 </n-text>
|
||||
<n-text v-if="!klass" depth="3" class="hint" style="margin-left: 12px"> 先填班级号 </n-text>
|
||||
</div>
|
||||
</template>
|
||||
<n-empty v-else :description="`${isMobile ? '上面' : '左边'}粘进名单,这里就列出会建成的账号`" :style="{ marginTop: isMobile ? '24px' : '80px' }" />
|
||||
@@ -288,11 +298,6 @@ body{margin:12mm;font-family:"Microsoft YaHei","PingFang SC",sans-serif}
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ks {
|
||||
font-size: 18px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ const classOptions = computed(() => [
|
||||
...groupClassOptions(configStore.config?.classList ?? []),
|
||||
{ label: classLabel(NO_CLASS), value: NO_CLASS },
|
||||
])
|
||||
/** 编辑框的班级:配置里的班,外加这个人现在的班(老班不在配置里也要显示得出来) */
|
||||
const editClassOptions = computed(() =>
|
||||
groupClassOptions([...(configStore.config?.classList ?? []), ...(userEditing.value?.className ? [userEditing.value.className] : [])]),
|
||||
)
|
||||
const neverLoggedIn = ref(0)
|
||||
|
||||
const total = ref(0)
|
||||
@@ -314,7 +318,7 @@ async function handleEditUser() {
|
||||
try {
|
||||
if (create.value) {
|
||||
const newUser: ImportUsersRequest['users'] = [
|
||||
[userEditing.value.username, password.value, userEditing.value.email ?? '', userEditing.value.realName ?? ''],
|
||||
[userEditing.value.username, password.value, userEditing.value.email ?? '', userEditing.value.realName ?? '', userEditing.value.className],
|
||||
]
|
||||
await importUsers(newUser)
|
||||
listUsers()
|
||||
@@ -388,10 +392,9 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className
|
||||
<n-form-item-gi :span="1" label="真名">
|
||||
<n-input v-model:value="userEditing.realName" />
|
||||
</n-form-item-gi>
|
||||
<!-- 班级是后端从用户名的 ks 数字前缀推出来的(classNameOf),
|
||||
editUser 也不发这个字段,所以这里只读 —— 以前是个能改、改了没用的输入框 -->
|
||||
<n-form-item-gi v-if="!create" :span="1" label="班级">
|
||||
<n-input :value="userEditing.className" disabled placeholder="由用户名自动推导" />
|
||||
<!-- 班级单独存一列,2026-11 起不再从用户名的 ks 前缀推(新号不带前缀)。老师、管理员不填 -->
|
||||
<n-form-item-gi :span="1" label="班级">
|
||||
<n-select v-model:value="userEditing.className" :options="editClassOptions" filterable clearable placeholder="没填班级" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="1" label="邮箱">
|
||||
<n-input v-model:value="userEditing.email" />
|
||||
|
||||
Reference in new issue
Block a user