Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -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>
);
}