From 1f1887c86001933ad1c76372fb889e9acced8591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=AB=E5=87=A4=E8=90=8D?= Date: Fri, 7 Aug 2026 17:08:56 +0800 Subject: [PATCH] feat: optimize user management and add account deletion --- app/api/users/route.ts | 36 ++++++++-- app/globals.css | 135 ++++++++++++++++++++++++++++++++--- app/users-page.tsx | 114 +++++++++++++++++++++++++++-- design-qa.md | 50 +++++++++++++ tests/rendered-html.test.mjs | 17 +++++ 5 files changed, 329 insertions(+), 23 deletions(-) create mode 100644 design-qa.md diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 6c8d323..1e4539c 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -57,14 +57,14 @@ export async function POST(request: Request) { role?: UserRole; userId?: string; }; - const password = String(body.password ?? ""); - if (!validatePassword(password)) { - return Response.json({ error: "密码需为8—72位" }, { status: 400 }); - } const db = getRawDb(); - const passwordRecord = await createPasswordRecord(password); if (body.action === "create") { + const password = String(body.password ?? ""); + if (!validatePassword(password)) { + return Response.json({ error: "密码需为8—72位" }, { status: 400 }); + } + const passwordRecord = await createPasswordRecord(password); const username = normalizeUsername(body.username); const role = body.role === "admin" ? "admin" : "user"; if (!validateUsername(username)) { @@ -92,6 +92,11 @@ export async function POST(request: Request) { ) .run(); } else if (body.action === "reset_password") { + const password = String(body.password ?? ""); + if (!validatePassword(password)) { + return Response.json({ error: "密码需为8—72位" }, { status: 400 }); + } + const passwordRecord = await createPasswordRecord(password); const target = await db .prepare("SELECT id, role FROM users WHERE id = ?") .bind(String(body.userId ?? "")) @@ -124,6 +129,27 @@ export async function POST(request: Request) { ), db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").bind(target.id), ]); + } else if (body.action === "delete") { + const target = await db + .prepare("SELECT id, role FROM users WHERE id = ?") + .bind(String(body.userId ?? "")) + .first<{ id: string; role: UserRole }>(); + if (!target) { + return Response.json({ error: "没有找到这个账号" }, { status: 404 }); + } + if (target.id === currentUser.id) { + return Response.json({ error: "不能删除当前登录账号" }, { status: 400 }); + } + if (target.role === "super_admin") { + return Response.json({ error: "不能删除超级管理员账号" }, { status: 403 }); + } + if (currentUser.role !== "super_admin" && target.role !== "user") { + return managerForbidden(); + } + await db.batch([ + db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").bind(target.id), + db.prepare("DELETE FROM users WHERE id = ? AND role <> 'super_admin'").bind(target.id), + ]); } else { return Response.json({ error: "不支持的操作" }, { status: 400 }); } diff --git a/app/globals.css b/app/globals.css index 37f8886..d68ae4c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2877,25 +2877,42 @@ label small { .user-management-layout { display: grid; - grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.28fr); - gap: 18px; + grid-template-columns: 1fr; + gap: 16px; + min-width: 0; } .user-create-panel, .user-list-panel { align-self: start; + min-width: 0; + padding: 22px; } .user-create-form { - display: flex; - flex-direction: column; - margin-top: 24px; + display: grid; + gap: 12px; + align-items: end; + margin-top: 18px; +} + +.user-create-form.with-role { + grid-template-columns: minmax(190px, 1.15fr) minmax(190px, 1fr) minmax(150px, 0.72fr) 132px; +} + +.user-create-form.without-role { + grid-template-columns: minmax(190px, 1fr) minmax(190px, 1fr) 132px; +} + +.user-create-form label { + min-width: 0; + margin-bottom: 0; } .user-create-form .primary-button { width: 100%; - height: 45px; - margin-top: 3px; + height: 40px; + min-height: 40px; } .user-management-message { @@ -2908,7 +2925,9 @@ label small { } .user-table { - margin-top: 18px; + width: 100%; + max-width: 100%; + margin-top: 16px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; @@ -2917,10 +2936,10 @@ label small { .user-table-head, .user-table-row { display: grid; - grid-template-columns: minmax(130px, 1.4fr) minmax(90px, 0.8fr) minmax(90px, 0.8fr) 82px; + grid-template-columns: minmax(160px, 1.4fr) minmax(100px, 0.7fr) minmax(110px, 0.75fr) minmax(180px, auto); gap: 12px; align-items: center; - padding: 12px 14px; + padding: 14px 16px; } .user-table-head { @@ -2934,6 +2953,10 @@ label small { font-size: 10px; } +.user-table-row:hover { + background: #fbfcfa; +} + .user-table-row > span:not(.role-pill) { color: #84908c; } @@ -2958,6 +2981,77 @@ label small { background: #e5f5ed; } +.user-row-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.user-action-button { + min-height: 30px; + padding: 0 10px; + border: 1px solid #dce5e1; + border-radius: 8px; + color: var(--green-deep); + background: white; + font-size: 9px; + font-weight: 650; +} + +.user-action-button:hover { + border-color: #a8cbbd; + background: #f4faf7; +} + +.user-action-button.danger { + border-color: #efd7d3; + color: #a8473b; +} + +.user-action-button.danger:hover { + border-color: #dfb5ae; + background: #fff7f5; +} + +.danger-button { + display: inline-flex; + min-height: 38px; + align-items: center; + justify-content: center; + padding: 0 15px; + border: 1px solid #a83f34; + border-radius: 9px; + color: white; + background: #b94d41; + font-size: 11px; + font-weight: 650; +} + +.danger-button:hover { + background: #9f3e34; +} + +.danger-button:disabled { + cursor: wait; + opacity: 0.62; +} + +.eyebrow.danger { + color: #b04b40; +} + +.user-delete-warning { + margin: 0; + padding: 13px 14px; + border: 1px solid #f0dedb; + border-radius: 10px; + color: #7f5049; + background: #fff8f6; + font-size: 10px; + line-height: 1.65; +} + .toast { position: fixed; right: 24px; @@ -3101,6 +3195,15 @@ label small { .schedule-grid { grid-template-columns: 1fr; } + + .user-create-form.with-role, + .user-create-form.without-role { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .user-create-form .primary-button { + align-self: end; + } } @media (max-width: 900px) { @@ -3216,7 +3319,17 @@ label small { .user-table-head, .user-table-row { - min-width: 560px; + min-width: 660px; + } + + .user-create-panel, + .user-list-panel { + padding: 18px; + } + + .user-create-form.with-role, + .user-create-form.without-role { + grid-template-columns: 1fr; } .topbar { diff --git a/app/users-page.tsx b/app/users-page.tsx index 79e66fe..5417653 100644 --- a/app/users-page.tsx +++ b/app/users-page.tsx @@ -30,6 +30,7 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) { const [role, setRole] = useState<"admin" | "user">("user"); const [resetTarget, setResetTarget] = useState(null); const [resetPassword, setResetPassword] = useState(""); + const [deleteTarget, setDeleteTarget] = useState(null); const [message, setMessage] = useState(""); const [working, setWorking] = useState(false); @@ -100,6 +101,31 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) { } }; + const deleteUser = async () => { + if (!deleteTarget) return; + setWorking(true); + setMessage(""); + try { + const next = await userApi( + await fetch("/api/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + userId: deleteTarget.id, + }), + }), + ); + setUsers(next); + setDeleteTarget(null); + setMessage("账号已删除,该账号所有登录设备均已退出"); + } catch (reason) { + setMessage(reason instanceof Error ? reason.message : "删除失败"); + } finally { + setWorking(false); + } + }; + const canReset = (item: ManagedUser) => { if (currentUser.role === "super_admin") { return item.role !== "super_admin" || item.id === currentUser.id; @@ -107,6 +133,11 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) { return item.role === "user"; }; + const canDelete = (item: ManagedUser) => { + if (item.id === currentUser.id || item.role === "super_admin") return false; + return currentUser.role === "super_admin" || item.role === "user"; + }; + return (
@@ -116,7 +147,10 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {

账号密码由后台统一分配,不开放注册和个人改密。

-
+ {currentUser.role === "super_admin" && ( -