import React, { useEffect, useMemo, useRef, useState } from "react"; import { ChevronRight, LoaderCircle, MessageCircleMore, RefreshCw, X } from "lucide-react"; import { formatDateTime } from "./time.js"; const REASONS = [ "内容不清晰", "数量不足", "问题不匹配", "回答不符合要求", "商品不一致", "其他", ]; const STATUSES = ["全部", "待审核"]; const formatTime = formatDateTime; const productName = (record) => [ ...new Set( record.questions.filter((q) => q.selected).map((q) => q.product_title), ), ].join(" / ") || "待识别商品"; const StatusBadge = ({ status }) => ( {status} ); function RefundRecipientInfo({ record }) { return (
支付宝收款信息 支付宝账号 {record.alipay_account || "未填写(历史任务)"} 收款人姓名 {record.payee_name || "未填写(历史任务)"}
); } const reviewKind = (record) => record?.batch_id ? "batch" : "assignment"; const reviewId = (record) => record?.batch_id || record?.assignment_id || record?.id; const reviewKey = (record) => `${reviewKind(record)}:${reviewId(record)}`; const reviewEndpoint = (record) => reviewKind(record) === "batch" ? `/api/admin/ask-batch-reviews/${reviewId(record)}` : `/api/admin/ask-assignment-reviews/${reviewId(record)}`; function DetailLoadingModal({ title }) { return (
{title} 请求已收到,图片将在弹窗内按需加载…
); } function NextReviewButton({ hasNext, onNext, busy, label = "下一个审核任务" }) { return ( ); } function RefundPrompt({ onConfirm, onCancel, onNext, hasNext }) { return (
✓

审核已通过

是否前往任务看板的“待返款”模块进行返款?

{hasNext && ( )}
); } function ReviewDetail({ record, close, decide, busy, hasNext, onNext, nextBusy }) { const [reason, setReason] = useState(REASONS[0]); const [customReason, setCustomReason] = useState(""); const selected = record.questions.filter((q) => q.selected); const pending = record.status === "待审核"; return (
event.target === event.currentTarget && close()} >

回答审核 · 用户任务列表

任务ID:{record.submission_no}

{record.user_id} {record.submission_no} 商品:{productName(record)} 提交时间:{formatTime(record.updated_at)}

① 问答记录(发布问题 ↔ 用户回答)

{selected.map((question, index) => (
Q{index + 1}:{question.question}

A: {question.answer}

))}

② 用户上传的任务截图

{record.completion_screenshots.map((image, index) => (
{`任务截图${index
任务截图 {index + 1}
))}
{!!record.payment_screenshots?.length &&

③ 返款二维码截图

{record.payment_screenshots.map((image) => (
收款码
审核通过后用于后续人工处理
))}
}
{record.latest_review && !pending && (
{record.latest_review.approved ? "审核已通过" : "审核未通过"} {!record.latest_review.approved && (

原因:{record.latest_review.reason_code} {record.latest_review.reason_text ? `;${record.latest_review.reason_text}` : ""}

)}
)}
{pending ? (
审核不通过原因: setCustomReason(event.target.value)} placeholder={ reason === "其他" ? "请输入自定义原因(必填)" : "补充说明(选填)" } />
) : ( )}
); } function AskReviewDetail({ record, close, api, onDecision, hasNext, onNext, nextBusy }) { const [reason, setReason] = useState("截图不清晰"); const [reasonText, setReasonText] = useState(""); const [busy, setBusy] = useState(false); const selected = record.questions.filter((item) => item.selected); async function decide(approved) { setBusy(true); try { const result = await api( `${reviewEndpoint(record)}/decision`, { method: "POST", body: JSON.stringify({ approved, reason_code: approved ? "" : reason, reason_text: reasonText.trim(), }), }, ); onDecision({ ...record, ...result.data, question_screenshots: record.question_screenshots || [], payment_screenshots: record.payment_screenshots || [], }, approved); } catch (error) { window.alert(error.message); } finally { setBusy(false); } } return (
event.target === event.currentTarget && close()} >

提问审核 · 用户任务详情

任务ID:{record.task_id}

{record.user_id} {record.task_id} 商品:{record.product_name} 创建人:{record.creator_name || "运营后台"} 提交时间:{formatTime(record.updated_at)}

① 用户认领并完成的提问

{selected.map((item, index) => (
Q{index + 1}:{item.question}
))}

② 用户上传的提问截图

{record.question_screenshots.map((image, index) => (
{`提问截图${index
提问截图 {index + 1}
))}
{!!record.payment_screenshots?.length &&

③ 用户上传的收款码

{record.payment_screenshots.map((image) => (
收款码
收款码截图
))}
}
{record.latest_review && record.status !== "待审核" && (
{record.latest_review.approved ? "审核已通过" : "审核未通过"} {!record.latest_review.approved && (

原因:{record.latest_review.reason_code} {record.latest_review.reason_text ? `;${record.latest_review.reason_text}` : ""}

)}
)}
{record.status === "待审核" ? (
审核不通过原因: setReasonText(event.target.value)} placeholder="补充说明(选填)" />
) : ( )}
); } function AskReviewQueue({ api, onApproved }) { const [records, setRecords] = useState([]); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [detailLoading, setDetailLoading] = useState(null); const [nextRecord, setNextRecord] = useState(null); const detailCache = useRef(new Map()); async function openDetail(record) { if (!record || detailLoading) return; const key = reviewKey(record); const index = records.findIndex((item) => reviewKey(item) === key); const nextCandidate = index >= 0 ? records[index + 1] || null : null; setNextRecord(nextCandidate); const cached = detailCache.current.get(key); if (cached) { setDetail(cached); return; } setDetailLoading(key); try { const result = await api( reviewEndpoint(record), ); detailCache.current.set(key, result.data); setDetail(result.data); if (nextCandidate && !detailCache.current.has(reviewKey(nextCandidate))) { void api(reviewEndpoint(nextCandidate)) .then((nextResult) => detailCache.current.set(reviewKey(nextCandidate), nextResult.data)) .catch(() => {}); } } catch (error) { window.alert(error.message); } finally { setDetailLoading(null); } } async function load(showLoading = true) { if (showLoading) setLoading(true); try { const result = await api("/api/admin/ask-completion-reviews"); setRecords(result.data); } catch (error) { window.alert(error.message); } finally { if (showLoading) setLoading(false); } } function handleDecision(updated, approved) { const key = reviewKey(updated); detailCache.current.set(key, updated); setDetail(updated); setRecords((current) => current.filter((item) => reviewKey(item) !== key)); window.dispatchEvent(new Event("review-count-changed")); void load(false); if (approved) { onApproved({ hasNext: Boolean(nextRecord), onNext: () => openDetail(nextRecord), }); } } useEffect(() => { load(); }, []); return ( <>
用户 / 任务 商品 提交时间 提问数 审核状态 操作
{loading ? (
正在加载提问审核任务…
) : records.length === 0 ? (
暂无提问审核任务
) : ( records.map((record) => (
{record.user_id} {record.task_id} {(record.taobao_id || record.phone) !== record.user_id && {record.taobao_id || record.phone}} {record.product_name} {formatTime(record.updated_at)} {record.questions.filter((item) => item.selected).length} {" "} 条提问
)) )}
{detailLoading && !detail && } {detail && ( setDetail(null)} api={api} onDecision={handleDecision} hasNext={Boolean(nextRecord)} nextBusy={Boolean(detailLoading)} onNext={() => openDetail(nextRecord)} /> )} ); } export default function ReviewCenterPage({ api }) { const [queue, setQueue] = useState("ask"); const [statusFilter, setStatusFilter] = useState("待审核"); const [records, setRecords] = useState([]); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [detailLoading, setDetailLoading] = useState(null); const [nextRecord, setNextRecord] = useState(null); const [refundPrompt, setRefundPrompt] = useState(null); const loadVersion = useRef(0); const detailCache = useRef(new Map()); async function openAnswerDetail(record) { if (!record || detailLoading) return; const index = allRows.findIndex((item) => item.id === record.id); const nextCandidate = index >= 0 ? allRows[index + 1] || null : null; setNextRecord(nextCandidate); const cached = detailCache.current.get(record.id); if (cached) { setDetail(cached); return; } setDetailLoading(record.id); try { const result = await api( `/api/admin/answer-completion-reviews/${record.id}`, ); detailCache.current.set(record.id, result.data); setDetail(result.data); if (nextCandidate && !detailCache.current.has(nextCandidate.id)) { void api(`/api/admin/answer-completion-reviews/${nextCandidate.id}`) .then((nextResult) => detailCache.current.set(nextCandidate.id, nextResult.data)) .catch(() => {}); } } catch (err) { window.alert(err.message); } finally { setDetailLoading(null); } } async function load(showLoading = true) { const version = ++loadVersion.current; if (showLoading) setLoading(true); try { const result = await api("/api/admin/answer-completion-reviews"); if (version !== loadVersion.current) return; setRecords(result.data); setError(""); } catch (err) { if (version === loadVersion.current) setError(err.message); } finally { if (showLoading && version === loadVersion.current) setLoading(false); } } useEffect(() => { load(); }, []); const allRows = useMemo( () => records.filter((record) => record.status === "待审核"), [records], ); const rows = useMemo( () => statusFilter === "全部" ? allRows : allRows.filter((record) => record.status === statusFilter), [allRows, statusFilter], ); const count = (status) => status === "全部" ? allRows.length : allRows.filter((record) => record.status === status).length; async function decide(record, approved, reasonCode = "", reasonText = "") { setBusy(true); try { const result = await api(`/api/admin/answer-completion-reviews/${record.id}/decision`, { method: "POST", body: JSON.stringify({ approved, reason_code: reasonCode, reason_text: reasonText, }), }); const updated = { ...record, ...result.data, invited_screenshots: record.invited_screenshots || [], completion_screenshots: record.completion_screenshots || [], payment_screenshots: record.payment_screenshots || [], }; detailCache.current.set(record.id, updated); setDetail(updated); setRecords((current) => current.filter((item) => item.id !== record.id)); void load(false); window.dispatchEvent(new Event("review-count-changed")); setStatusFilter("待审核"); if (approved) { setRefundPrompt({ hasNext: Boolean(nextRecord), onNext: () => openAnswerDetail(nextRecord), }); } } catch (err) { window.alert(err.message); } finally { setBusy(false); } } return ( <>

审核中心

提问审核与回答审核,对照用户提交截图进行任务终审。

{queue === "answer" ? "回答审核说明" : "提问审核说明"}

{queue === "answer" ? "同一用户在同一任务下的多组问答按「用户ID + 任务ID」归并为一条;点击查看任务列表,逐组对照问答和用户完成截图。" : "审核提问用户提交的问题、截图及任务完成情况。提问任务链路接入后会显示在这里。"}

{error &&
{error}
} {queue === "ask" ? ( setRefundPrompt(navigation)} /> ) : ( <>
审核状态: {STATUSES.map((status) => ( ))}
用户 / 任务 商品 提交时间 问答数 审核状态 操作
{loading ? (
正在加载审核任务…
) : rows.length === 0 ? (
当前状态暂无回答审核任务
) : ( rows.map((record) => (
{record.user_id} {record.submission_no} {(record.taobao_id || record.phone) !== record.user_id && {record.taobao_id || record.phone}} {productName(record)} {formatTime(record.updated_at)} {record.questions.filter((q) => q.selected).length} {" "} 组问答
)) )}
)} {detail && ( setDetail(null)} decide={decide} busy={busy} hasNext={Boolean(nextRecord)} nextBusy={Boolean(detailLoading)} onNext={() => openAnswerDetail(nextRecord)} /> )} {detailLoading && !detail && } {refundPrompt && ( { setRefundPrompt(null); refundPrompt.onNext(); }} onCancel={() => setRefundPrompt(null)} onConfirm={() => { setRefundPrompt(null); window.dispatchEvent(new CustomEvent("open-task-board", { detail: { status: "待返款" } })); }} /> )} ); }