feat(平台端): 完整用户管理 CRUD — 新增/编辑/删除/启禁用 + 端口角色选择

UsersPage 增 新增/编辑 对话框(账号/昵称/端口角色/子角色/初始密码,编辑锁账号)、删除、启禁用;
usersApi 增 update(PUT)/remove(DELETE)。后端 /admin/users 增 PUT/DELETE。npm run build 通过。
This commit is contained in:
Pine
2026-08-24 18:42:57 +08:00
parent d8ef5b94eb
commit c07297ed45
2 changed files with 76 additions and 19 deletions
+2
View File
@@ -21,6 +21,8 @@ import type {
export const usersApi = {
list: () => api.get<UserRow[]>("/admin/users"),
create: (body: Partial<UserRow>) => api.post<UserRow>("/admin/users", body),
update: (id: string, body: Partial<UserRow>) => api.put<any>(`/admin/users/${id}`, body),
remove: (id: string) => api.del<{ ok: boolean }>(`/admin/users/${id}`),
setRole: (id: string, body: { role: string; sub_role?: string }) =>
api.post<UserRow>(`/admin/users/${id}/role`, body),
setStatus: (id: string, status: string) =>
+74 -19
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import { PageHeader } from "@/components/generic/PageHeader";
import { DataTable, type Column } from "@/components/generic/DataTable";
import { StatusBadge } from "@/components/generic/StatusBadge";
@@ -6,31 +7,85 @@ import { usersApi } from "@/api/resources";
import type { UserRow } from "@/types";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { formatDate } from "@/lib/utils";
const columns: Column<UserRow>[] = [
{ key: "username", header: "账号", cell: (r) => <span className="font-medium">{r.username}</span> },
{ key: "nickname", header: "昵称", cell: (r) => r.nickname || "-" },
{ key: "role", header: "端口角色", cell: (r) => r.sub_role || r.role || "-" },
{ key: "status", header: "状态", cell: (r) => <StatusBadge status={r.status} /> },
{ key: "created_at", header: "创建时间", cell: (r) => formatDate(r.created_at) },
];
const PORT_ROLES = ["operator", "government", "enterprise", "provider", "carrier", "opc_member", "investor"];
const EMPTY = { username: "", nickname: "", role: "opc_member", sub_role: "" };
export default function UsersPage() {
const { data, loading, error, reload } = useResource(() => usersApi.list());
const res = useResource(() => usersApi.list());
const [open, setOpen] = React.useState(false);
const [editing, setEditing] = React.useState<UserRow | null>(null);
const [form, setForm] = React.useState<Record<string, string>>(EMPTY);
const [busy, setBusy] = React.useState(false);
const openCreate = () => { setEditing(null); setForm(EMPTY); setOpen(true); };
const openEdit = (r: UserRow) => { setEditing(r); setForm({ username: r.username || "", nickname: r.nickname || "", role: r.role || "opc_member", sub_role: r.sub_role || "" }); setOpen(true); };
const save = async () => {
setBusy(true);
try {
if (editing) await usersApi.update(editing.id, { nickname: form.nickname, role: form.role, sub_role: form.sub_role || undefined });
else await usersApi.create({ username: form.username, nickname: form.nickname, password: form.password || "123456", role: form.role, sub_role: form.sub_role || undefined });
setOpen(false); res.reload();
} finally { setBusy(false); }
};
const remove = async (id: string) => {
if (!window.confirm("确认删除该用户?")) return;
await usersApi.remove(id); res.reload();
};
const toggle = async (r: UserRow) => {
await usersApi.setStatus(r.id, r.status === "disabled" ? "active" : "disabled"); res.reload();
};
const columns: Column<UserRow>[] = [
{ key: "username", header: "账号", cell: (r) => <span className="font-medium">{r.username}</span> },
{ key: "nickname", header: "昵称", cell: (r) => r.nickname || "-" },
{ key: "role", header: "端口角色", cell: (r) => r.sub_role || r.role || "-" },
{ key: "status", header: "状态", cell: (r) => <StatusBadge status={r.status} /> },
{ key: "created_at", header: "创建时间", cell: (r) => formatDate(r.created_at) },
{
key: "actions", header: "操作", cell: (r) => (
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => openEdit(r)}></Button>
<Button size="sm" variant="ghost" onClick={() => toggle(r)}>{r.status === "disabled" ? "启用" : "禁用"}</Button>
<Button size="sm" variant="ghost" className="text-destructive" onClick={() => remove(r.id)}></Button>
</div>
),
},
];
return (
<div>
<PageHeader
title="用户管理"
description="平台账号、端口身份与启用状态"
actions={<Button variant="outline" onClick={reload}></Button>}
/>
<Card>
<CardContent className="pt-2">
{error ? <p className="py-4 text-sm text-destructive">{error}</p> : null}
<DataTable columns={columns} data={data} loading={loading} empty="暂无用户" />
</CardContent>
</Card>
<PageHeader title="用户管理" description="平台账号增删改查、端口角色与启禁用"
actions={<div className="flex gap-2"><Button variant="outline" onClick={res.reload}></Button><Button onClick={openCreate}></Button></div>} />
<Card><CardContent className="pt-2">
{res.error ? <p className="py-4 text-sm text-destructive">{res.error}</p> : null}
<DataTable columns={columns} data={res.data} loading={res.loading} empty="暂无用户" />
</CardContent></Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader><DialogTitle>{editing ? "编辑用户" : "新增用户"}</DialogTitle></DialogHeader>
<div className="grid grid-cols-2 gap-4 py-2">
<div className="space-y-1.5"><Label></Label><Input disabled={!!editing} value={form.username} onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))} /></div>
{!editing && <div className="space-y-1.5"><Label></Label><Input value={form.password ?? ""} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} /></div>}
<div className="space-y-1.5"><Label></Label><Input value={form.nickname} onChange={(e) => setForm((f) => ({ ...f, nickname: e.target.value }))} /></div>
<div className="space-y-1.5"><Label></Label>
<Select value={form.role} onValueChange={(v) => setForm((f) => ({ ...f, role: v }))}>
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
<SelectContent>{PORT_ROLES.map((r) => <SelectItem key={r} value={r}>{r}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1.5"><Label></Label><Input value={form.sub_role} onChange={(e) => setForm((f) => ({ ...f, sub_role: e.target.value }))} /></div>
</div>
<DialogFooter><Button variant="outline" onClick={() => setOpen(false)}></Button><Button onClick={save} disabled={busy}></Button></DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}