feat(后台用户): 管理员密码默认打码,点「显示」才展开
Some checks failed
Deploy / deploy (push) Has been cancelled

后台用户列表把所有账号的明文密码直接铺在表格里。学生密码是有意保留的运营需求
(老师要能查),但管理员账号的密码跟着一起明文显示,投屏或者旁人路过就看到了。

现在学生管理员/教师管理员/超管三类账号的密码默认渲染成 •••••• 加一个「显示」
按钮,点一下该行展开成明文(仍然是 TextCopy,点击复制照旧)。普通用户不变。
展开状态按 user id 记在 list.vue 里,listUsers() 里清空,所以翻页、搜索、换筛选、
换排序之后一律回到打码;不做定时自动隐藏,也不记进 localStorage。

只改前端。rawPassword 照旧随列表下发——/users* 每条路由本来就是 requireSuperAdmin,
非超管连列表都拿不到,要防的是屏幕被看到,不是拿到响应的人。改成「按需取单个密码」
反而要多开一个下发明文的端点,把面扩大。

rawPassword 为空时不打码:给一个点了什么也不显示的按钮没意义,直接走原来的 TextCopy。

顺带把角色标签缩短(学生管理员→生管、教师管理员→师管)。getUserRole 只有同一张表的
用户名列在用,密码列变宽之后那一列需要让出横向空间。

实跑验证(本机 dev 栈 + 超管登录):管理员行打码、学生行明文照旧、点「显示」后展开
且仍是可复制的按钮、搜索触发重新拉列表后回到打码。vue-tsc 与 build 均通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 10:08:30 -06:00
parent bb4b5825c3
commit 1d9686b929
3 changed files with 61 additions and 5 deletions

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import TextCopy from "shared/components/TextCopy.vue"
import { USER_TYPE } from "utils/constants"
import type { User } from "utils/types"
interface Props {
user: User
revealed: boolean
}
const props = defineProps<Props>()
defineEmits<{
(e: "reveal", value: number): void
}>()
/**
* 只有**管理员账号**的密码默认打码。
*
* 学生的密码照旧直接显示 —— 老师查学生密码是日常动作(明文列 `raw_password` 就是为它
* 保留的),挡一道只是添乱。要防的是管理员密码在投屏、或者旁人路过时被看到。
*
* `rawPassword` 为空时不打码:给一个点了什么也不显示的按钮没有意义,直接走原来的
* TextCopy渲染成空
*/
const masked = computed(
() =>
!props.revealed &&
props.user.adminType !== USER_TYPE.REGULAR_USER &&
!!props.user.rawPassword,
)
</script>
<template>
<n-flex v-if="masked" align="center" :size="6" :wrap="false">
<span class="dots"></span>
<n-button size="tiny" secondary @click="$emit('reveal', props.user.id)">
显示
</n-button>
</n-flex>
<TextCopy v-else>{{ props.user.rawPassword }}</TextCopy>
</template>
<style scoped>
/* 不加 nowrap 的话六个点会在窄列里折行,把整行撑高 */
.dots {
white-space: nowrap;
letter-spacing: 1px;
}
</style>

View File

@@ -13,6 +13,7 @@ import {
} from "../api"
import Actions from "./components/Actions.vue"
import Name from "./components/Name.vue"
import Password from "./components/Password.vue"
import { PROBLEM_PERMISSION, USER_TYPE } from "utils/constants"
import { useRouteQuery } from "@vueuse/router"
import TextCopy from "shared/components/TextCopy.vue"
@@ -49,6 +50,9 @@ const sortOptions = [
]
const [create, toggleCreate] = useToggle(false)
const password = ref("")
// 已经点开的管理员密码,按 user id 记。listUsers() 里清空,所以翻页/搜索/换筛选之后
// 一律回到打码状态 —— 不做定时自动隐藏,也不记进 localStorage。
const revealedPasswords = ref(new Set<number>())
const userIDs = ref<DataTableRowKey[]>([])
const rowKey = (row: User) => row.id
@@ -65,8 +69,13 @@ const columns: DataTableColumn<User>[] = [
{
title: "密码",
key: "raw_password",
width: 100,
render: (row) => h(TextCopy, () => row.rawPassword),
width: 150,
render: (row) =>
h(Password, {
user: row,
revealed: revealedPasswords.value.has(row.id),
onReveal: (id: number) => revealedPasswords.value.add(id),
}),
},
{
title: "创建时间",
@@ -131,6 +140,7 @@ async function listUsers() {
)
total.value = res.total
users.value = res.results
revealedPasswords.value.clear()
}
function chooseUsers(rowKeys: DataTableRowKey[]) {

View File

@@ -141,7 +141,7 @@ export function debounce<T extends (...args: any[]) => any>(
export function getUserRole(role: User["adminType"]): {
type: "default" | "info" | "warning" | "error"
label: "普通" | "生管理员" | "师管理员" | "超管"
label: "普通" | "生管" | "师管" | "超管"
} {
const roleMap = {
[USER_TYPE.REGULAR_USER]: {
@@ -150,11 +150,11 @@ export function getUserRole(role: User["adminType"]): {
},
[USER_TYPE.STUDENT_ADMIN]: {
type: "info" as const,
label: "生管理员" as const,
label: "生管" as const,
},
[USER_TYPE.TEACHER_ADMIN]: {
type: "warning" as const,
label: "师管理员" as const,
label: "师管" as const,
},
[USER_TYPE.SUPER_ADMIN]: {
type: "error" as const,