feat(平台端): 完整用户管理 CRUD — 新增/编辑/删除/启禁用 + 端口角色选择
UsersPage 增 新增/编辑 对话框(账号/昵称/端口角色/子角色/初始密码,编辑锁账号)、删除、启禁用; usersApi 增 update(PUT)/remove(DELETE)。后端 /admin/users 增 PUT/DELETE。npm run build 通过。
This commit is contained in:
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user