Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import api from "@/lib/api";
|
||||
import type { PlatformUser, PlatformOrg } from "@/lib/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { Building2 } from "lucide-react";
|
||||
import { Pagination } from "@/components/ui/pagination";
|
||||
|
||||
const roleLabels: Record<string, string> = {
|
||||
super_admin: "平台管理员",
|
||||
admin: "机构管理员",
|
||||
creator: "创作者",
|
||||
user: "普通用户",
|
||||
};
|
||||
|
||||
const roleColors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
super_admin: "destructive",
|
||||
admin: "default",
|
||||
creator: "secondary",
|
||||
user: "outline",
|
||||
};
|
||||
|
||||
export default function PlatformUsersPage() {
|
||||
const qc = useQueryClient();
|
||||
const [search, setSearch] = useState("");
|
||||
const [orgFilter, setOrgFilter] = useState("all");
|
||||
const [roleFilter, setRoleFilter] = useState("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [migrateTarget, setMigrateTarget] = useState<PlatformUser | null>(null);
|
||||
const [newOrgID, setNewOrgID] = useState("");
|
||||
|
||||
const { data: orgs } = useQuery({
|
||||
queryKey: ["platformOrgs"],
|
||||
queryFn: () => api.get<PlatformOrg[]>("/api/v1/platform/orgs"),
|
||||
});
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["platformUsers", search, orgFilter, roleFilter, page],
|
||||
queryFn: () => {
|
||||
const p = new URLSearchParams();
|
||||
p.set("page", String(page));
|
||||
if (search) p.set("q", search);
|
||||
if (orgFilter !== "all") p.set("org_id", orgFilter);
|
||||
if (roleFilter !== "all") p.set("role", roleFilter);
|
||||
return api.get<{
|
||||
items: PlatformUser[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}>(`/api/v1/platform/users?${p}`);
|
||||
},
|
||||
});
|
||||
|
||||
// 切换过滤条件时重置到第一页
|
||||
const resetSearch = (v: string) => {
|
||||
setSearch(v);
|
||||
setPage(1);
|
||||
};
|
||||
const resetOrg = (v: string | null) => {
|
||||
setOrgFilter(v ?? "all");
|
||||
setPage(1);
|
||||
};
|
||||
const resetRole = (v: string | null) => {
|
||||
setRoleFilter(v ?? "all");
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const updateRole = useMutation({
|
||||
mutationFn: ({ id, role }: { id: string; role: string }) =>
|
||||
api.put(`/api/v1/platform/users/${id}/role`, { role }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["platformUsers"] });
|
||||
toast.success("角色已更新");
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const updateStatus = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: string }) =>
|
||||
api.put(`/api/v1/platform/users/${id}/status`, { status }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["platformUsers"] });
|
||||
toast.success("状态已更新");
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const assignOrg = useMutation({
|
||||
mutationFn: ({ id, org_id }: { id: string; org_id: string }) =>
|
||||
api.put(`/api/v1/platform/users/${id}/org`, { org_id }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["platformUsers"] });
|
||||
toast.success("已迁移到目标机构");
|
||||
setMigrateTarget(null);
|
||||
setNewOrgID("");
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-bold">全局用户</h1>
|
||||
<span className="text-xs text-muted-foreground">共 {data?.total ?? 0} 条</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<Input
|
||||
placeholder="搜索姓名或邮箱..."
|
||||
value={search}
|
||||
onChange={(e) => resetSearch(e.target.value)}
|
||||
className="w-64"
|
||||
/>
|
||||
<Select value={orgFilter} onValueChange={resetOrg}>
|
||||
<SelectTrigger className="w-40">
|
||||
<span>
|
||||
{orgFilter === "all"
|
||||
? "全部机构"
|
||||
: orgs?.find((o) => o.id === orgFilter)?.short_name ||
|
||||
orgs?.find((o) => o.id === orgFilter)?.name ||
|
||||
orgFilter}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部机构</SelectItem>
|
||||
{orgs?.map((o) => (
|
||||
<SelectItem key={o.id} value={o.id}>
|
||||
{o.short_name || o.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={roleFilter} onValueChange={resetRole}>
|
||||
<SelectTrigger className="w-36">
|
||||
<span>{roleFilter === "all" ? "全部角色" : roleLabels[roleFilter] || roleFilter}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部角色</SelectItem>
|
||||
<SelectItem value="super_admin">平台管理员</SelectItem>
|
||||
<SelectItem value="admin">机构管理员</SelectItem>
|
||||
<SelectItem value="creator">创作者</SelectItem>
|
||||
<SelectItem value="user">普通用户</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
page={data?.page ?? 1}
|
||||
pageSize={data?.page_size ?? 20}
|
||||
total={data?.total ?? 0}
|
||||
onChange={setPage}
|
||||
/>
|
||||
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="text-left p-3">用户</th>
|
||||
<th className="text-left p-3">所属机构</th>
|
||||
<th className="text-left p-3">角色</th>
|
||||
<th className="text-left p-3">状态</th>
|
||||
<th className="text-left p-3">登录</th>
|
||||
<th className="text-left p-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.items?.map((u) => (
|
||||
<tr key={u.id} className="border-t">
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={u.avatar_url} />
|
||||
<AvatarFallback>{u.name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{u.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{u.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{u.org_name ? (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Building2 className="h-3 w-3" />
|
||||
{u.org_short || u.org_name}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground italic">—(平台级)</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge variant={roleColors[u.role]}>{roleLabels[u.role]}</Badge>
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Badge variant={u.status === "active" ? "default" : "destructive"}>
|
||||
{u.status === "active" ? "正常" : "禁用"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground text-xs">{u.login_count} 次</td>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Select
|
||||
defaultValue={u.role}
|
||||
onValueChange={(role) => role && updateRole.mutate({ id: u.id, role })}
|
||||
>
|
||||
<SelectTrigger className="w-28 h-7 text-xs">
|
||||
<span>{roleLabels[u.role] || u.role}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">普通用户</SelectItem>
|
||||
<SelectItem value="creator">创作者</SelectItem>
|
||||
<SelectItem value="admin">机构管理员</SelectItem>
|
||||
<SelectItem value="super_admin">平台管理员</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => {
|
||||
setMigrateTarget(u);
|
||||
setNewOrgID(u.org_id || "");
|
||||
}}
|
||||
>
|
||||
迁移
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={() =>
|
||||
updateStatus.mutate({
|
||||
id: u.id,
|
||||
status: u.status === "active" ? "disabled" : "active",
|
||||
})
|
||||
}
|
||||
>
|
||||
{u.status === "active" ? "禁用" : "启用"}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 迁移机构对话框 */}
|
||||
<Dialog open={!!migrateTarget} onOpenChange={(o) => !o && setMigrateTarget(null)}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>迁移用户机构</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
将用户「{migrateTarget?.name}」迁移到目标机构
|
||||
</p>
|
||||
<Select value={newOrgID} onValueChange={(v) => setNewOrgID(v ?? "")}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择目标机构" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{orgs?.map((o) => (
|
||||
<SelectItem key={o.id} value={o.id}>
|
||||
{o.name}({o.short_name})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setMigrateTarget(null)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!newOrgID || newOrgID === migrateTarget?.org_id}
|
||||
onClick={() =>
|
||||
migrateTarget && assignOrg.mutate({ id: migrateTarget.id, org_id: newOrgID })
|
||||
}
|
||||
>
|
||||
确认迁移
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user