Initial commit: GovAI 政务AI平台
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user