Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/lib/api";
import { AppIcon } from "@/lib/app-icon";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "sonner";
import { Archive, RotateCcw } from "lucide-react";
interface AdminApp {
id: string;
name: string;
description: string;
icon_url?: string;
dify_app_type?: string;
status: string;
visibility: string;
usage_count: number;
avg_rating: number;
creator_name: string;
created_at: string;
}
const statusLabels: Record<string, string> = {
draft: "草稿",
pending_review: "审核中",
approved: "已上架",
rejected: "已驳回",
archived: "已归档",
};
const statusColors: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
draft: "outline",
pending_review: "secondary",
approved: "default",
rejected: "destructive",
archived: "outline",
};
const appTypeLabels: Record<string, string> = {
chatbot: "对话型",
completion: "文本生成",
workflow: "工作流",
agent: "智能体",
};
const visibilityLabels: Record<string, string> = {
public: "全单位",
department: "部门",
private: "私有",
};
export default function AdminAppsPage() {
const queryClient = useQueryClient();
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [actionTarget, setActionTarget] = useState<{
type: "delist" | "relist";
id: string;
name: string;
} | null>(null);
const { data } = useQuery({
queryKey: ["adminApps", search, statusFilter],
queryFn: () => {
const params = new URLSearchParams();
if (search) params.set("q", search);
if (statusFilter !== "all") params.set("status", statusFilter);
return api.get<{ items: AdminApp[] }>(`/api/v1/admin/apps?${params}`);
},
});
const delistApp = useMutation({
mutationFn: (id: string) => api.post(`/api/v1/admin/apps/${id}/delist`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["adminApps"] });
toast.success("已撤架");
setActionTarget(null);
},
onError: (err: Error) => toast.error(err.message),
});
const relistApp = useMutation({
mutationFn: (id: string) => api.post(`/api/v1/admin/apps/${id}/relist`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["adminApps"] });
toast.success("已重新上架");
setActionTarget(null);
},
onError: (err: Error) => toast.error(err.message),
});
return (
<div>
{/* 操作确认弹窗 */}
<AlertDialog
open={!!actionTarget}
onOpenChange={(open) => !open && setActionTarget(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{actionTarget?.type === "delist" ? "确认撤架" : "确认重新上架"}
</AlertDialogTitle>
<AlertDialogDescription>
{actionTarget?.type === "delist"
? `确定要将应用「${actionTarget?.name}」从应用商店撤架吗?撤架后用户将无法使用。`
: `确定要将应用「${actionTarget?.name}」重新上架到应用商店吗?`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
className={
actionTarget?.type === "delist"
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
: ""
}
onClick={() => {
if (actionTarget?.type === "delist") {
delistApp.mutate(actionTarget.id);
} else if (actionTarget?.type === "relist") {
relistApp.mutate(actionTarget!.id);
}
}}
>
{actionTarget?.type === "delist" ? "确认撤架" : "确认上架"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<h1 className="text-2xl font-bold mb-6"></h1>
<div className="flex items-center gap-3 mb-4">
<Input
placeholder="搜索应用..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-64"
/>
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v ?? "all")}>
<SelectTrigger className="w-32">
<span>
{statusFilter === "all" ? "全部" : statusLabels[statusFilter] || statusFilter}
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="pending_review"></SelectItem>
<SelectItem value="approved"></SelectItem>
<SelectItem value="rejected"></SelectItem>
<SelectItem value="archived"></SelectItem>
</SelectContent>
</Select>
</div>
<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>
<th className="text-left p-3"></th>
<th className="text-left p-3"></th>
</tr>
</thead>
<tbody>
{data?.items?.map((app) => (
<tr key={app.id} className="border-t hover:bg-muted/30 transition-colors">
<td className="p-3">
<div className="flex items-center gap-2">
<AppIcon iconUrl={app.icon_url} size={20} className="shrink-0 text-muted-foreground" />
<div>
<div className="font-medium">{app.name}</div>
<div className="text-xs text-muted-foreground line-clamp-1">
{app.description}
</div>
</div>
</div>
</td>
<td className="p-3">
<span className="text-xs text-muted-foreground">
{appTypeLabels[app.dify_app_type || "chatbot"] || "对话型"}
</span>
</td>
<td className="p-3 text-muted-foreground">{app.creator_name}</td>
<td className="p-3">
<Badge variant={statusColors[app.status]}>
{statusLabels[app.status]}
</Badge>
</td>
<td className="p-3 text-muted-foreground">
{visibilityLabels[app.visibility] || app.visibility}
</td>
<td className="p-3 text-muted-foreground">{app.usage_count}</td>
<td className="p-3">
{app.avg_rating > 0 ? (
<span className="text-yellow-500"> {app.avg_rating.toFixed(1)}</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
<td className="p-3">
<div className="flex gap-1.5">
{app.status === "approved" && (
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 text-xs text-destructive hover:text-destructive"
onClick={() =>
setActionTarget({ type: "delist", id: app.id, name: app.name })
}
>
<Archive className="h-3 w-3" />
</Button>
)}
{app.status === "archived" && (
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 text-xs text-primary"
onClick={() =>
setActionTarget({ type: "relist", id: app.id, name: app.name })
}
>
<RotateCcw className="h-3 w-3" />
</Button>
)}
{app.status !== "approved" && app.status !== "archived" && (
<span className="text-xs text-muted-foreground">-</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}