feat(用户): 选一个班就能下载全班的用户名和密码(表格)
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 11:22:10 +08:00
1 parent 02e2fa7d9b
commit 859a537804
3 files changed
+63 -8

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
/**
* 账号名单下成表格:用户名、密码两列。批量建号建完下名单、用户列表按班下名单共用这一份。
* 开头带 BOM,Excel 打开中文才不乱码
*/
export function downloadAccountsCsv(
rows: { username: string; password: string }[],
filename: string,
) {
const cell = (text: string) => (/[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text)
const csv =
"用户名,密码\n" + rows.map((it) => `${cell(it.username)},${cell(it.password)}`).join("\n")
const link = document.createElement("a")
link.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }))
link.download = `${filename}.csv`
link.click()
URL.revokeObjectURL(link.href)
}
+2 -7
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { downloadAccountsCsv } from "./accountsCsv"
import PageHeader from "admin/components/PageHeader.vue"
import { classLabel } from "oj/submission/utils"
import UserName from "shared/components/UserName.vue"
@@ -151,15 +152,9 @@ function again() {
passwords.clear()
}
/** 下载名单:用户名、密码两列。开头带 BOM,Excel 打开中文才不乱码 */
function download() {
if (!done.value) return
const csv = "用户名,密码\n" + done.value.map((it) => `${it.username},${it.password}`).join("\n")
const link = document.createElement("a")
link.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }))
link.download = `${prefix.value || "账号"}.csv`
link.click()
URL.revokeObjectURL(link.href)
downloadAccountsCsv(done.value, prefix.value || "账号")
}
/** 打印账号条:一人一张,一页四列,打出来剪开发下去 */
+44 -1
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import PageHeader from "admin/components/PageHeader.vue"
import { errorMessage } from "utils/api"
import { DataTableRowKey, SelectOption, useThemeVars } from "naive-ui"
@@ -12,7 +13,8 @@ import UserNameCell from "./components/UserNameCell.vue"
import PasswordCell from "./components/PasswordCell.vue"
import { PROBLEM_PERMISSION, USER_TYPE } from "utils/constants"
import { useRouteQuery } from "@vueuse/router"
import { NO_CLASS } from "@oj2/contract"
import { NO_CLASS, STUDENT_ROLES } from "@oj2/contract"
import { downloadAccountsCsv } from "./accountsCsv"
import { classLabel, groupClassOptions } from "oj/submission/utils"
import { useConfigStore } from "shared/store/config"
import { useBreakpoints } from "shared/composables/breakpoints"
@@ -218,6 +220,39 @@ async function listUsers() {
revealedPasswords.value.clear()
}
/**
* 选了一个班就能把全班的用户名、密码下成表格(发给班主任、打印)。只下学生,不管搜索和角色筛选;
* 一页最多 250 人,一个班 60 人以内,循环只是保险。老班级(23x 及更早)很多人没存明文密码,那格空着
*/
const downloadingClass = ref(false)
async function downloadClassAccounts() {
const className = query.className
downloadingClass.value = true
try {
const rows: AdminUserForm[] = []
for (let offset = 0; ; offset += 250) {
const res = await getUserList(offset, 250, "", "", "username", className)
rows.push(...res.results)
if (!res.results.length || rows.length >= res.total) break
}
const students = rows.filter((it) =>
(STUDENT_ROLES as readonly string[]).includes(it.adminType),
)
if (!students.length) {
message.warning("这个班没有学生")
return
}
downloadAccountsCsv(
students.map((it) => ({ username: it.username, password: it.rawPassword ?? "" })),
`${classLabel(className)}账号`,
)
} catch (err) {
message.error(errorMessage(err))
} finally {
downloadingClass.value = false
}
}
function chooseUsers(rowKeys: DataTableRowKey[]) {
userIDs.value = rowKeys
}
@@ -366,6 +401,14 @@ watch(() => [query.page, query.limit, query.type, query.orderBy, query.className
aria-label="班级"
style="width: 150px"
/>
<n-button
v-if="query.className && query.className !== NO_CLASS"
:loading="downloadingClass"
@click="downloadClassAccounts"
>
<template #icon><Icon icon="ph:download-simple" /></template>
下载这个班的账号
</n-button>
<n-select
v-model:value="query.type"
:options="adminOptions"