f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
149 lines
5.1 KiB
TypeScript
149 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import api from "@/lib/api";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { toast } from "sonner";
|
|
|
|
interface Review {
|
|
id: string;
|
|
app_id: string;
|
|
version: string;
|
|
submit_comment?: string;
|
|
submitted_at: string;
|
|
app_name?: string;
|
|
app_description?: string;
|
|
app_icon?: string;
|
|
submitter_name: string;
|
|
}
|
|
|
|
export default function ReviewsPage() {
|
|
const queryClient = useQueryClient();
|
|
const [rejectDialog, setRejectDialog] = useState<string | null>(null);
|
|
const [rejectComment, setRejectComment] = useState("");
|
|
|
|
const { data: reviews } = useQuery({
|
|
queryKey: ["pendingReviews"],
|
|
queryFn: () => api.get<Review[]>("/api/v1/admin/reviews"),
|
|
});
|
|
|
|
const approve = useMutation({
|
|
mutationFn: (id: string) => api.post(`/api/v1/admin/reviews/${id}/approve`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["pendingReviews"] });
|
|
toast.success("已通过审核");
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const reject = useMutation({
|
|
mutationFn: ({ id, comment }: { id: string; comment: string }) =>
|
|
api.post(`/api/v1/admin/reviews/${id}/reject`, { comment }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["pendingReviews"] });
|
|
setRejectDialog(null);
|
|
setRejectComment("");
|
|
toast.success("已驳回");
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold">审核队列</h1>
|
|
<Badge variant="secondary">{reviews?.length || 0} 待审核</Badge>
|
|
</div>
|
|
|
|
{reviews?.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-12 text-center text-muted-foreground">
|
|
暂无待审核应用
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{reviews?.map((review) => (
|
|
<Card key={review.id}>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-3xl">{review.app_icon || "🤖"}</span>
|
|
<div>
|
|
<CardTitle className="text-base">{review.app_name}</CardTitle>
|
|
<p className="text-sm text-muted-foreground">{review.app_description}</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="outline">v{review.version}</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-sm text-muted-foreground mb-4">
|
|
<span>提交者: {review.submitter_name}</span>
|
|
<span className="mx-2">|</span>
|
|
<span>提交时间: {new Date(review.submitted_at).toLocaleString("zh-CN")}</span>
|
|
{review.submit_comment && (
|
|
<>
|
|
<span className="mx-2">|</span>
|
|
<span>说明: {review.submit_comment}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={() => approve.mutate(review.id)}
|
|
disabled={approve.isPending}
|
|
>
|
|
通过
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => setRejectDialog(review.id)}
|
|
>
|
|
驳回
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<Dialog open={!!rejectDialog} onOpenChange={() => setRejectDialog(null)}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>驳回审核</DialogTitle>
|
|
</DialogHeader>
|
|
<Textarea
|
|
placeholder="请填写驳回原因..."
|
|
value={rejectComment}
|
|
onChange={(e) => setRejectComment(e.target.value)}
|
|
rows={4}
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setRejectDialog(null)}>
|
|
取消
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() =>
|
|
rejectDialog && reject.mutate({ id: rejectDialog, comment: rejectComment })
|
|
}
|
|
disabled={!rejectComment.trim() || reject.isPending}
|
|
>
|
|
确认驳回
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|