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()}
>
{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 + 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 ? (
) : (
)}
);
}
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 + 1}
))}
③ 用户上传的收款码
{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 === "待审核" ? (
) : (
)}
);
}
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: "待返款" } }));
}}
/>
)}
>
);
}