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