800 lines
28 KiB
JavaScript
800 lines
28 KiB
JavaScript
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 }) => (
|
||
<em className={`review-v2-status ${status}`}>{status}</em>
|
||
);
|
||
|
||
function RefundRecipientInfo({ record }) {
|
||
return (
|
||
<div className="refund-recipient-info">
|
||
<strong>支付宝收款信息</strong>
|
||
<span>
|
||
支付宝账号
|
||
<b>{record.alipay_account || "未填写(历史任务)"}</b>
|
||
</span>
|
||
<span>
|
||
收款人姓名
|
||
<b>{record.payee_name || "未填写(历史任务)"}</b>
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="review-v2-backdrop review-detail-loading-backdrop">
|
||
<section className="review-detail-loading" role="status" aria-live="polite">
|
||
<LoaderCircle size={24} className="spin" />
|
||
<strong>{title}</strong>
|
||
<span>请求已收到,图片将在弹窗内按需加载…</span>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function NextReviewButton({ hasNext, onNext, busy, label = "下一个审核任务" }) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
className="review-next-button"
|
||
disabled={!hasNext || busy}
|
||
onClick={onNext}
|
||
>
|
||
{busy ? "正在打开…" : hasNext ? label : "已是最后一个"}
|
||
{hasNext && !busy && <ChevronRight size={15} />}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function RefundPrompt({ onConfirm, onCancel, onNext, hasNext }) {
|
||
return (
|
||
<div className="review-v2-backdrop refund-prompt-backdrop">
|
||
<section className="refund-prompt" role="dialog" aria-modal="true" aria-labelledby="refund-prompt-title">
|
||
<div className="refund-prompt-icon">✓</div>
|
||
<div className="refund-prompt-content">
|
||
<h2 id="refund-prompt-title">审核已通过</h2>
|
||
<p>是否前往任务看板的“待返款”模块进行返款?</p>
|
||
</div>
|
||
<div className="refund-prompt-actions">
|
||
<button type="button" className="refund-prompt-cancel" onClick={onCancel}>暂不前往</button>
|
||
{hasNext && (
|
||
<button type="button" className="refund-prompt-next" onClick={onNext}>
|
||
继续下一个审核
|
||
<ChevronRight size={15} />
|
||
</button>
|
||
)}
|
||
<button type="button" className="refund-prompt-confirm" onClick={onConfirm}>前往待返款</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div
|
||
className="review-v2-backdrop"
|
||
onMouseDown={(event) => event.target === event.currentTarget && close()}
|
||
>
|
||
<section className="review-v2-modal">
|
||
<header>
|
||
<div>
|
||
<h2>
|
||
回答审核 · 用户任务列表 <StatusBadge status={record.status} />
|
||
</h2>
|
||
<p>任务ID:{record.submission_no}</p>
|
||
</div>
|
||
<button onClick={close}>
|
||
<X size={22} />
|
||
</button>
|
||
</header>
|
||
<div className="review-v2-modal-body">
|
||
<div className="review-v2-summary">
|
||
<code>{record.user_id}</code>
|
||
<code>{record.submission_no}</code>
|
||
<span>商品:{productName(record)}</span>
|
||
<span>提交时间:{formatTime(record.updated_at)}</span>
|
||
</div>
|
||
<RefundRecipientInfo record={record} />
|
||
<h3>① 问答记录(发布问题 ↔ 用户回答)</h3>
|
||
<div className="review-v2-qa">
|
||
{selected.map((question, index) => (
|
||
<article key={question.id}>
|
||
<strong>
|
||
Q{index + 1}:{question.question}
|
||
</strong>
|
||
<p>
|
||
<b>A:</b>
|
||
{question.answer}
|
||
</p>
|
||
</article>
|
||
))}
|
||
</div>
|
||
<div className="review-v2-media">
|
||
<section>
|
||
<h3>② 用户上传的任务截图</h3>
|
||
<div>
|
||
{record.completion_screenshots.map((image, index) => (
|
||
<figure key={image.id}>
|
||
<img
|
||
src={image.data_url}
|
||
alt={`任务截图${index + 1}`}
|
||
loading="lazy"
|
||
decoding="async"
|
||
/>
|
||
<figcaption>任务截图 {index + 1}</figcaption>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
</section>
|
||
{!!record.payment_screenshots?.length && <section>
|
||
<h3>③ 返款二维码截图</h3>
|
||
<div>
|
||
{record.payment_screenshots.map((image) => (
|
||
<figure key={image.id}>
|
||
<img
|
||
src={image.data_url}
|
||
alt="收款码"
|
||
loading="lazy"
|
||
decoding="async"
|
||
/>
|
||
<figcaption>审核通过后用于后续人工处理</figcaption>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
</section>}
|
||
</div>
|
||
{record.latest_review && !pending && (
|
||
<div
|
||
className={`review-v2-result ${record.latest_review.approved ? "approved" : ""}`}
|
||
>
|
||
<strong>
|
||
{record.latest_review.approved ? "审核已通过" : "审核未通过"}
|
||
</strong>
|
||
{!record.latest_review.approved && (
|
||
<p>
|
||
原因:{record.latest_review.reason_code}
|
||
{record.latest_review.reason_text
|
||
? `;${record.latest_review.reason_text}`
|
||
: ""}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{pending ? (
|
||
<footer className="review-v2-sticky-actions">
|
||
<div>
|
||
<span>审核不通过原因:</span>
|
||
<select
|
||
value={reason}
|
||
onChange={(event) => setReason(event.target.value)}
|
||
>
|
||
{REASONS.map((item) => (
|
||
<option key={item}>{item}</option>
|
||
))}
|
||
</select>
|
||
<input
|
||
value={customReason}
|
||
onChange={(event) => setCustomReason(event.target.value)}
|
||
placeholder={
|
||
reason === "其他"
|
||
? "请输入自定义原因(必填)"
|
||
: "补充说明(选填)"
|
||
}
|
||
/>
|
||
</div>
|
||
<NextReviewButton hasNext={hasNext} onNext={onNext} busy={nextBusy || busy} />
|
||
<button
|
||
className="reject"
|
||
disabled={busy || (reason === "其他" && !customReason.trim())}
|
||
onClick={() => decide(record, false, reason, customReason.trim())}
|
||
>
|
||
{busy ? "处理中…" : "审核不通过(需重做)"}
|
||
</button>
|
||
<button
|
||
className="approve"
|
||
disabled={busy}
|
||
onClick={() => decide(record, true)}
|
||
>
|
||
{busy ? "处理中…" : "审核通过"}
|
||
</button>
|
||
</footer>
|
||
) : (
|
||
<footer className="review-v2-processed-actions">
|
||
<span>
|
||
该任务已完成审核,当前状态:
|
||
<StatusBadge status={record.status} />
|
||
</span>
|
||
<NextReviewButton hasNext={hasNext} onNext={onNext} busy={nextBusy} />
|
||
<button onClick={close}>关闭</button>
|
||
</footer>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div
|
||
className="review-v2-backdrop"
|
||
onMouseDown={(event) => event.target === event.currentTarget && close()}
|
||
>
|
||
<section className="review-v2-modal">
|
||
<header>
|
||
<div>
|
||
<h2>
|
||
提问审核 · 用户任务详情 <StatusBadge status={record.status} />
|
||
</h2>
|
||
<p>任务ID:{record.task_id}</p>
|
||
</div>
|
||
<button onClick={close}>
|
||
<X size={22} />
|
||
</button>
|
||
</header>
|
||
<div className="review-v2-modal-body">
|
||
<div className="review-v2-summary">
|
||
<code>{record.user_id}</code>
|
||
<code>{record.task_id}</code>
|
||
<span>商品:{record.product_name}</span>
|
||
<span>创建人:{record.creator_name || "运营后台"}</span>
|
||
<span>提交时间:{formatTime(record.updated_at)}</span>
|
||
</div>
|
||
<RefundRecipientInfo record={record} />
|
||
<h3>① 用户认领并完成的提问</h3>
|
||
<div className="review-v2-qa">
|
||
{selected.map((item, index) => (
|
||
<article key={item.id}>
|
||
<strong>
|
||
Q{index + 1}:{item.question}
|
||
</strong>
|
||
</article>
|
||
))}
|
||
</div>
|
||
<div className="review-v2-media">
|
||
<section>
|
||
<h3>② 用户上传的提问截图</h3>
|
||
<div>
|
||
{record.question_screenshots.map((image, index) => (
|
||
<figure key={image.id}>
|
||
<img
|
||
src={image.data_url}
|
||
alt={`提问截图${index + 1}`}
|
||
loading="lazy"
|
||
decoding="async"
|
||
/>
|
||
<figcaption>提问截图 {index + 1}</figcaption>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
</section>
|
||
{!!record.payment_screenshots?.length && <section>
|
||
<h3>③ 用户上传的收款码</h3>
|
||
<div>
|
||
{record.payment_screenshots.map((image) => (
|
||
<figure key={image.id}>
|
||
<img
|
||
src={image.data_url}
|
||
alt="收款码"
|
||
loading="lazy"
|
||
decoding="async"
|
||
/>
|
||
<figcaption>收款码截图</figcaption>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
</section>}
|
||
</div>
|
||
{record.latest_review && record.status !== "待审核" && (
|
||
<div
|
||
className={`review-v2-result ${record.latest_review.approved ? "approved" : ""}`}
|
||
>
|
||
<strong>
|
||
{record.latest_review.approved ? "审核已通过" : "审核未通过"}
|
||
</strong>
|
||
{!record.latest_review.approved && (
|
||
<p>
|
||
原因:{record.latest_review.reason_code}
|
||
{record.latest_review.reason_text
|
||
? `;${record.latest_review.reason_text}`
|
||
: ""}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{record.status === "待审核" ? (
|
||
<footer className="review-v2-sticky-actions">
|
||
<div>
|
||
<span>审核不通过原因:</span>
|
||
<select
|
||
value={reason}
|
||
onChange={(event) => setReason(event.target.value)}
|
||
>
|
||
<option>截图不清晰</option>
|
||
<option>数量不足</option>
|
||
<option>问题不匹配</option>
|
||
<option>商品不一致</option>
|
||
<option>重复提交</option>
|
||
<option>其他</option>
|
||
</select>
|
||
<input
|
||
value={reasonText}
|
||
onChange={(event) => setReasonText(event.target.value)}
|
||
placeholder="补充说明(选填)"
|
||
/>
|
||
</div>
|
||
<NextReviewButton hasNext={hasNext} onNext={onNext} busy={nextBusy || busy} />
|
||
<button
|
||
className="reject"
|
||
disabled={busy}
|
||
onClick={() => decide(false)}
|
||
>
|
||
审核不通过
|
||
</button>
|
||
<button
|
||
className="approve"
|
||
disabled={busy}
|
||
onClick={() => decide(true)}
|
||
>
|
||
审核通过
|
||
</button>
|
||
</footer>
|
||
) : (
|
||
<footer className="review-v2-processed-actions">
|
||
<span>
|
||
当前审核状态:
|
||
<StatusBadge status={record.status} />
|
||
</span>
|
||
<NextReviewButton hasNext={hasNext} onNext={onNext} busy={nextBusy} />
|
||
<button onClick={close}>关闭</button>
|
||
</footer>
|
||
)}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<>
|
||
<section className="review-v2-table panel">
|
||
<div className="head">
|
||
<span>用户 / 任务</span>
|
||
<span>商品</span>
|
||
<span>提交时间</span>
|
||
<span>提问数</span>
|
||
<span>审核状态</span>
|
||
<span>操作</span>
|
||
</div>
|
||
{loading ? (
|
||
<div className="review-v2-empty">正在加载提问审核任务…</div>
|
||
) : records.length === 0 ? (
|
||
<div className="review-v2-empty">暂无提问审核任务</div>
|
||
) : (
|
||
records.map((record) => (
|
||
<div className="row" key={record.batch_id || record.assignment_id}>
|
||
<span>
|
||
<b>
|
||
<code>{record.user_id}</code>
|
||
<code>{record.task_id}</code>
|
||
</b>
|
||
{(record.taobao_id || record.phone) !== record.user_id && <small>{record.taobao_id || record.phone}</small>}
|
||
</span>
|
||
<span>{record.product_name}</span>
|
||
<span>{formatTime(record.updated_at)}</span>
|
||
<span>
|
||
<strong>
|
||
{record.questions.filter((item) => item.selected).length}
|
||
</strong>{" "}
|
||
条提问
|
||
</span>
|
||
<span>
|
||
<StatusBadge status={record.status} />
|
||
</span>
|
||
<span>
|
||
<button disabled={Boolean(detailLoading)} onClick={() => openDetail(record)}>
|
||
{detailLoading === reviewKey(record)
|
||
? "正在打开…"
|
||
: record.status === "待审核" ? "查看并审核" : "查看详情"}
|
||
</button>
|
||
</span>
|
||
</div>
|
||
))
|
||
)}
|
||
</section>
|
||
{detailLoading && !detail && <DetailLoadingModal title="正在打开提问审核详情" />}
|
||
{detail && (
|
||
<AskReviewDetail
|
||
key={reviewKey(detail)}
|
||
record={detail}
|
||
close={() => 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 (
|
||
<>
|
||
<div className="review-v2-heading">
|
||
<div>
|
||
<h2>审核中心</h2>
|
||
<p>提问审核与回答审核,对照用户提交截图进行任务终审。</p>
|
||
</div>
|
||
<div>
|
||
<button
|
||
className={queue === "ask" ? "active" : ""}
|
||
onClick={() => setQueue("ask")}
|
||
>
|
||
队列一:提问审核
|
||
</button>
|
||
<button
|
||
className={queue === "answer" ? "active" : ""}
|
||
onClick={() => {
|
||
setQueue("answer");
|
||
load();
|
||
}}
|
||
>
|
||
队列二:回答审核
|
||
</button>
|
||
<button
|
||
className="review-v2-refresh"
|
||
type="button"
|
||
onClick={() => load()}
|
||
disabled={loading}
|
||
title="刷新审核列表"
|
||
>
|
||
<RefreshCw size={14} className={loading ? "spin" : ""} />
|
||
{loading ? "刷新中…" : "刷新列表"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className={`review-v2-guide ${queue}`}>
|
||
<MessageCircleMore size={17} />
|
||
<div>
|
||
<strong>
|
||
{queue === "answer" ? "回答审核说明" : "提问审核说明"}
|
||
</strong>
|
||
<p>
|
||
{queue === "answer"
|
||
? "同一用户在同一任务下的多组问答按「用户ID + 任务ID」归并为一条;点击查看任务列表,逐组对照问答和用户完成截图。"
|
||
: "审核提问用户提交的问题、截图及任务完成情况。提问任务链路接入后会显示在这里。"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{error && <div className="form-error">{error}</div>}
|
||
{queue === "ask" ? (
|
||
<AskReviewQueue api={api} onApproved={(navigation) => setRefundPrompt(navigation)} />
|
||
) : (
|
||
<>
|
||
<div className="review-v2-status-filter">
|
||
<strong>审核状态:</strong>
|
||
{STATUSES.map((status) => (
|
||
<button
|
||
key={status}
|
||
className={statusFilter === status ? "active" : ""}
|
||
onClick={() => setStatusFilter(status)}
|
||
>
|
||
{status}
|
||
<small>{count(status)}</small>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<section className="review-v2-table panel">
|
||
<div className="head">
|
||
<span>用户 / 任务</span>
|
||
<span>商品</span>
|
||
<span>提交时间</span>
|
||
<span>问答数</span>
|
||
<span>审核状态</span>
|
||
<span>操作</span>
|
||
</div>
|
||
{loading ? (
|
||
<div className="review-v2-empty">正在加载审核任务…</div>
|
||
) : rows.length === 0 ? (
|
||
<div className="review-v2-empty">当前状态暂无回答审核任务</div>
|
||
) : (
|
||
rows.map((record) => (
|
||
<div className="row" key={record.id}>
|
||
<span>
|
||
<b>
|
||
<code>{record.user_id}</code>
|
||
<code>{record.submission_no}</code>
|
||
</b>
|
||
{(record.taobao_id || record.phone) !== record.user_id && <small>{record.taobao_id || record.phone}</small>}
|
||
</span>
|
||
<span>{productName(record)}</span>
|
||
<span>{formatTime(record.updated_at)}</span>
|
||
<span>
|
||
<strong>
|
||
{record.questions.filter((q) => q.selected).length}
|
||
</strong>{" "}
|
||
组问答
|
||
</span>
|
||
<span>
|
||
<StatusBadge status={record.status} />
|
||
</span>
|
||
<span>
|
||
<button disabled={Boolean(detailLoading)} onClick={() => openAnswerDetail(record)}>
|
||
{detailLoading === record.id
|
||
? "正在打开…"
|
||
: record.status === "待审核"
|
||
? "查看并审核"
|
||
: "查看任务列表"}
|
||
</button>
|
||
</span>
|
||
</div>
|
||
))
|
||
)}
|
||
</section>
|
||
</>
|
||
)}
|
||
{detail && (
|
||
<ReviewDetail
|
||
key={detail.id}
|
||
record={detail}
|
||
close={() => setDetail(null)}
|
||
decide={decide}
|
||
busy={busy}
|
||
hasNext={Boolean(nextRecord)}
|
||
nextBusy={Boolean(detailLoading)}
|
||
onNext={() => openAnswerDetail(nextRecord)}
|
||
/>
|
||
)}
|
||
{detailLoading && !detail && <DetailLoadingModal title="正在打开回答审核详情" />}
|
||
{refundPrompt && (
|
||
<RefundPrompt
|
||
hasNext={refundPrompt.hasNext}
|
||
onNext={() => {
|
||
setRefundPrompt(null);
|
||
refundPrompt.onNext();
|
||
}}
|
||
onCancel={() => setRefundPrompt(null)}
|
||
onConfirm={() => {
|
||
setRefundPrompt(null);
|
||
window.dispatchEvent(new CustomEvent("open-task-board", { detail: { status: "待返款" } }));
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
}
|