import { CheckCircleOutlined, CloseCircleOutlined, FileSearchOutlined, FlagOutlined, LoginOutlined, PlusCircleOutlined, ReloadOutlined, } from "@ant-design/icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import { communityClient, type ServerCommunityCase } from "../../api/communityClient"; import { reportClient, type AdminReportItem } from "../../api/reportClient"; import WorkspacePageShell from "../../components/WorkspacePageShell"; import "../../styles/pages/compliance.css"; import type { WebUserSession } from "../../types"; import { canManageCommunityCases, canReviewCommunity } from "./communityPermissions"; interface CommunityReviewPageProps { session: WebUserSession | null; onOpenLogin: () => void; onOpenReport: () => void; onOpenCaseAdd: () => void; } type ReviewStatus = "" | ServerCommunityCase["status"]; type ModerationTab = "cases" | "reports"; const REVIEW_STATUS_OPTIONS: Array<{ value: ReviewStatus; label: string }> = [ { value: "pending", label: "待审核" }, { value: "approved", label: "已通过" }, { value: "rejected", label: "已驳回" }, { value: "", label: "全部" }, ]; const STATUS_LABEL: Record = { pending: "待审核", approved: "已通过", rejected: "已驳回", }; function getCaseCover(item: ServerCommunityCase): string { return ( item.coverUrl || item.assets.find((asset) => asset.assetType === "cover" && asset.url)?.url || item.assets.find((asset) => (asset.assetType === "image" || asset.assetType === "asset") && asset.url)?.url || "" ); } function getWorkflowStats(item: ServerCommunityCase): { nodes: number; edges: number } { const workflow = item.metadata.workflow || item.metadata.workflowSnapshot || (item.metadata.workflowData && typeof item.metadata.workflowData === "object" ? (item.metadata.workflowData as Record) : null); if (!workflow || typeof workflow !== "object" || Array.isArray(workflow)) { return { nodes: 0, edges: 0 }; } const workflowRecord = workflow as Record; const nodes = Array.isArray(workflowRecord.nodes) ? workflowRecord.nodes.length : 0; const edges = Array.isArray(workflowRecord.edges) ? workflowRecord.edges.length : 0; return { nodes, edges }; } function formatDate(value?: string | null): string { if (!value) return "暂无时间"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } function compactType(value?: string | null): string { return value?.trim() || "未填写"; } export default function CommunityReviewPage({ session, onOpenLogin, onOpenReport, onOpenCaseAdd }: CommunityReviewPageProps) { const allowed = canReviewCommunity(session); const canAddCase = canManageCommunityCases(session); const [activeTab, setActiveTab] = useState("cases"); const [status, setStatus] = useState("pending"); const [cases, setCases] = useState([]); const [selectedCaseId, setSelectedCaseId] = useState(null); const [reports, setReports] = useState([]); const [selectedReportId, setSelectedReportId] = useState(null); const [reviewNote, setReviewNote] = useState(""); const [loadingCases, setLoadingCases] = useState(false); const [loadingReports, setLoadingReports] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const selectedCase = useMemo( () => cases.find((item) => String(item.id) === selectedCaseId) ?? cases[0] ?? null, [cases, selectedCaseId], ); const selectedReport = useMemo( () => reports.find((item) => String(item.id) === selectedReportId) ?? reports[0] ?? null, [reports, selectedReportId], ); const loadCases = useCallback(async () => { if (!allowed) return; setLoadingCases(true); setError(null); try { const items = await communityClient.listCasesForReview(status); setCases(items); setSelectedCaseId((current) => current && items.some((item) => String(item.id) === current) ? current : String(items[0]?.id || ""), ); } catch (loadError) { setCases([]); setError(loadError instanceof Error ? loadError.message : "审核列表加载失败"); } finally { setLoadingCases(false); } }, [allowed, status]); const loadReports = useCallback(async () => { if (!allowed) return; setLoadingReports(true); setError(null); try { const items = await reportClient.listAdminReports(); setReports(items); setSelectedReportId((current) => current && items.some((item) => String(item.id) === current) ? current : String(items[0]?.id || ""), ); } catch (loadError) { setReports([]); setError(loadError instanceof Error ? loadError.message : "举报列表加载失败"); } finally { setLoadingReports(false); } }, [allowed]); useEffect(() => { if (activeTab === "cases") { void loadCases(); } else { void loadReports(); } }, [activeTab, loadCases, loadReports]); const handleDecision = async (nextStatus: "approved" | "rejected") => { if (!selectedCase || submitting) return; setSubmitting(true); setError(null); try { await communityClient.updateReviewStatus(selectedCase.id, nextStatus, reviewNote.trim()); setReviewNote(""); await loadCases(); } catch (submitError) { setError(submitError instanceof Error ? submitError.message : "审核操作失败"); } finally { setSubmitting(false); } }; if (!session) { return (

请登录工作人员账号

社区作品审核和举报查看需要登录后访问。

); } if (!allowed) { return (

当前账号没有审核权限

请切换到 admin、staff、reviewer 或 moderator 角色账号后再进入审核台。

); } const caseStats = selectedCase ? getWorkflowStats(selectedCase) : { nodes: 0, edges: 0 }; return (
内部审核台

社区审核与举报

读取现有服务器审核接口,处理待审作品并查看用户举报。

{canAddCase ? ( ) : null}
{error ?

{error}

: null} {activeTab === "cases" ? ( <>
{REVIEW_STATUS_OPTIONS.map((option) => ( ))}
{selectedCase ? (
{getCaseCover(selectedCase) ? {selectedCase.title} : null} {STATUS_LABEL[selectedCase.status]}
{selectedCase.tags[0] || "社区作品"}

{selectedCase.title}

{selectedCase.description || "作者未填写作品简介。"}

作者:{selectedCase.username || `UID ${selectedCase.userId || "-"}`} 状态:{STATUS_LABEL[selectedCase.status]} 节点:{caseStats.nodes} 连线:{caseStats.edges} 收藏:{selectedCase.favoriteCount} 点赞:{selectedCase.likeCount}
{selectedCase.reviewNote ? (

上次备注:{selectedCase.reviewNote}

) : null}