feat: 采集失败KOC补交截图闭环 + 任务中心分发详情弹窗

公开数据补交闭环(F1):
- distributions 新增 public_metrics_screenshot_key(mysql/0022 迁移)
- KOC 门户:链接已回填但采集失败时展示补交数据截图入口(kind=public-metrics)
- 运营端:回收队列"核对补录"支持公开互动数据(赞/藏/评),按补交截图补录后采集状态置成功
- 企微催办:采集失败纳入待办(公开数据待补交)
- task-result-image 支持 kind=publish 输出发布截图

门户文案(F2):
- 自动采集数据明确"由系统每天自动采集,无需你填写",仅识别失败时才手动填写曝光/阅读

任务中心分发详情:
- 任务中心/数据回收的分发表行可点击,弹窗查看详情
- 详情按任务类型适配:发布阶段/数据回收(自动采集)/创作者数据(第7天)/截图回收四区块三段式布局
This commit is contained in:
ABAPPLO
2026-10-09 17:02:46 +08:00
parent c686003cfb
commit c11afd5f99
15 changed files with 1250 additions and 67 deletions

View File

@@ -152,6 +152,7 @@ type Distribution = {
account_id: string | null;
publish_url: string | null;
publish_time: string | null;
publish_screenshot_key: string | null;
result_screenshot_key: string | null;
result_submitted_at: string | null;
status: string;
@@ -173,6 +174,7 @@ type Distribution = {
latest_comments: number | null;
latest_collects: number | null;
latest_shares: number | null;
public_metrics_screenshot_key: string | null;
collection_status: string;
collection_status_description: string | null;
collection_updated_at: string | null;
@@ -548,6 +550,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
const [ownerLinksTask, setOwnerLinksTask] = useState<Task | null>(null);
const [customerChatTask, setCustomerChatTask] = useState<Task | null>(null);
const [metricModal, setMetricModal] = useState<Distribution | null>(null);
const [detailDistributionId, setDetailDistributionId] = useState<string | null>(null);
const [publishUrlModal, setPublishUrlModal] = useState<{
distribution: Distribution;
value: string;
@@ -572,7 +575,13 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
const [taskFormProjects, setTaskFormProjects] = useState<Array<{ id: string; name: string }>>([]);
const [sourcePreview, setSourcePreview] = useState<FeishuPreview | null>(null);
const [sourceWorking, setSourceWorking] = useState(false);
const [metricForm, setMetricForm] = useState({ exposure: "", views: "" });
const [metricForm, setMetricForm] = useState({
exposure: "",
views: "",
likes: "",
collects: "",
comments: "",
});
const [exportingTaskId, setExportingTaskId] = useState<string | null>(null);
const [exportingResources, setExportingResources] = useState(false);
const [creatingPartnerId, setCreatingPartnerId] = useState<string | null>(null);
@@ -1126,18 +1135,46 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
const submitManualMetrics = async (event: FormEvent) => {
event.preventDefault();
if (!metricModal) return;
const hasCreatorInput = Boolean(metricForm.exposure.trim() && metricForm.views.trim());
const hasPublicInput = Boolean(
metricForm.likes.trim() ||
metricForm.collects.trim() ||
metricForm.comments.trim(),
);
if (!hasCreatorInput && !hasPublicInput) {
setToast("请至少填写创作者数据或公开互动数据中的一项");
return;
}
const success = await runAction(
{
action: "manual_metrics",
distributionId: metricModal.id,
exposure: metricForm.exposure,
views: metricForm.views,
...(hasCreatorInput
? { exposure: metricForm.exposure, views: metricForm.views }
: {}),
...(metricForm.likes.trim()
? { publicLikes: metricForm.likes }
: {}),
...(metricForm.collects.trim()
? { publicCollects: metricForm.collects }
: {}),
...(metricForm.comments.trim()
? { publicComments: metricForm.comments }
: {}),
},
"创作者中心数据已确认",
hasPublicInput && !hasCreatorInput
? "公开互动数据已按补交截图补录"
: "创作者中心数据已确认",
);
if (success) {
setMetricModal(null);
setMetricForm({ exposure: "", views: "" });
setMetricForm({
exposure: "",
views: "",
likes: "",
collects: "",
comments: "",
});
}
};
@@ -1146,6 +1183,9 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
setMetricForm({
exposure: distribution.exposure?.toString() || "",
views: distribution.views?.toString() || "",
likes: distribution.latest_likes?.toString() || "",
collects: distribution.latest_collects?.toString() || "",
comments: distribution.latest_comments?.toString() || "",
});
};
@@ -1360,6 +1400,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
);
return result !== false;
}}
onOpenDetail={(distribution) => setDetailDistributionId(distribution.id)}
/>
)}
{activeNav === "resources" && (
@@ -1404,6 +1445,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
onExport={exportRecoveryData}
onExportScreenshots={exportScreenshotResults}
exportingTaskId={exportingTaskId}
onOpenDetail={(distribution) => setDetailDistributionId(distribution.id)}
/>
)}
</>
@@ -1679,8 +1721,12 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
<form className="modal-card compact" onSubmit={submitManualMetrics} onMouseDown={(event) => event.stopPropagation()}>
<div className="modal-heading">
<div>
<p className="eyebrow">OCR异常兜底</p>
<h2>确认创作者数据</h2>
<p className="eyebrow">
{metricModal.collection_status === "failed"
? "采集失败兜底 · 数据核对补录"
: "OCR异常兜底"}
</p>
<h2>核对并补录数据</h2>
</div>
<button type="button" onClick={() => setMetricModal(null)} aria-label="关闭">×</button>
</div>
@@ -1688,19 +1734,36 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
<strong>{metricModal.account_nickname || "待识别账号"}</strong>
<span>{metricModal.content_title}</span>
</div>
<div className="form-grid two">
<label>
<span>曝光量</span>
<input type="number" min="0" value={metricForm.exposure} onChange={(event) => setMetricForm({ ...metricForm, exposure: event.target.value })} required />
</label>
<label>
<span>阅读量</span>
<input type="number" min="0" value={metricForm.views} onChange={(event) => setMetricForm({ ...metricForm, views: event.target.value })} required />
</label>
</div>
<label>
<span>创作者数据(曝光 / 阅读,第7天截图数据)</span>
<div className="form-grid two" style={{ marginBottom: 0 }}>
<input type="number" min="0" placeholder="曝光量" value={metricForm.exposure} onChange={(event) => setMetricForm({ ...metricForm, exposure: event.target.value })} />
<input type="number" min="0" placeholder="阅读量" value={metricForm.views} onChange={(event) => setMetricForm({ ...metricForm, views: event.target.value })} />
</div>
<small>曝光 + 阅读齐了才算「回收完成」;本次不改可留空。</small>
</label>
<label>
<span>公开互动数据(按 KOC 补交的数据截图核对)</span>
<div className="form-grid two" style={{ marginBottom: 0 }}>
<input type="number" min="0" placeholder="点赞" value={metricForm.likes} onChange={(event) => setMetricForm({ ...metricForm, likes: event.target.value })} />
<input type="number" min="0" placeholder="收藏" value={metricForm.collects} onChange={(event) => setMetricForm({ ...metricForm, collects: event.target.value })} />
<input type="number" min="0" placeholder="评论" value={metricForm.comments} onChange={(event) => setMetricForm({ ...metricForm, comments: event.target.value })} />
</div>
<small>填写后采集状态会更新为「采集成功(运营已核对)」;不需要补录可留空。</small>
</label>
{metricModal.public_metrics_screenshot_key && (
<a
className="share-link-button"
href={`/api/creator-screenshot?distribution=${encodeURIComponent(metricModal.id)}&kind=public-metrics`}
target="_blank"
rel="noreferrer"
>
查看 KOC 补交的数据截图 ↗
</a>
)}
<div className="modal-actions">
<button type="button" className="ghost-button" onClick={() => setMetricModal(null)}>取消</button>
<button className="primary-button" disabled={working}>确认数据</button>
<button className="primary-button" disabled={working}>确认补录</button>
</div>
</form>
</div>
@@ -1830,6 +1893,51 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) {
/>
)}
{(() => {
// 详情按 id 实时取行:任何操作刷新数据后弹窗内容自动更新
const detailRow = detailDistributionId
? data.distributions.find((item) => item.id === detailDistributionId)
: null;
if (!detailRow) return null;
const detailTask = data.tasks.find((task) => task.id === detailRow.task_id);
return (
<DistributionDetailModal
item={detailRow}
recoveryEnabled={Number(detailTask?.recovery_enabled ?? 1) === 1}
working={working}
collectingDistributionIds={collectingDistributionIds}
canUpdatePublishUrl={isManager}
canRelease={isManager}
onClose={() => setDetailDistributionId(null)}
onCollect={collectMetrics}
onFallback={openMetricFallback}
onUpdatePublishUrl={(distribution) =>
setPublishUrlModal({
distribution,
value: distribution.publish_url || "",
})
}
onRelease={(distribution) => {
void (async () => {
if (
!window.confirm(
`确认释放“${distribution.content_title}”?释放后回到可领取池,不可撤销。`,
)
)
return;
const result = await runAction(
{
action: "release_distribution",
distributionId: distribution.id,
},
`“${distribution.content_title}”已释放,可重新领取`,
);
if (result !== false) setDetailDistributionId(null);
})();
}}
/>
);
})()}
{toast && <div className="toast"><span>✓</span>{toast}</div>}
{working && <div className="working-bar" />}
</div>
@@ -4580,6 +4688,7 @@ function TasksPage({
onBindCustomerChat,
onCreateRecovery,
onRelease,
onOpenDetail,
}: {
tasks: Task[];
distributions: Distribution[];
@@ -4598,6 +4707,7 @@ function TasksPage({
onBindCustomerChat: (task: Task) => void;
onCreateRecovery: (task: Task) => void;
onRelease: (distribution: Distribution) => Promise<boolean>;
onOpenDetail: (distribution: Distribution) => void;
}) {
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<"grouped" | "flat">("grouped");
@@ -4734,6 +4844,7 @@ function TasksPage({
working={working}
onRelease={onRelease}
onBack={() => setSelectedTaskId(null)}
onOpenDetail={onOpenDetail}
/>
);
}
@@ -5007,6 +5118,7 @@ function TaskDistributionDetail({
working,
onRelease,
onBack,
onOpenDetail,
}: {
task: Task;
distributions: Distribution[];
@@ -5014,6 +5126,7 @@ function TaskDistributionDetail({
working: boolean;
onRelease: (distribution: Distribution) => Promise<boolean>;
onBack: () => void;
onOpenDetail: (distribution: Distribution) => void;
}) {
const [releaseTarget, setReleaseTarget] = useState<Distribution | null>(null);
const taskDistributions = distributions.filter(
@@ -5059,6 +5172,7 @@ function TaskDistributionDetail({
distributions={taskDistributions}
onRelease={canRelease ? setReleaseTarget : undefined}
showRecovery={recoveryEnabled}
onOpenDetail={onOpenDetail}
/>
</section>
{releaseTarget && (
@@ -5128,11 +5242,13 @@ function DistributionTable({
compact = false,
onRelease,
showRecovery = false,
onOpenDetail,
}: {
distributions: Distribution[];
compact?: boolean;
onRelease?: (distribution: Distribution) => void;
showRecovery?: boolean;
onOpenDetail?: (distribution: Distribution) => void;
}) {
// 已发布但还没采到数据的行:有计划时按首个采集日显示,没计划时
// 明说待设置,而不是写死「等待D2」误导运营。
@@ -5170,7 +5286,12 @@ function DistributionTable({
</thead>
<tbody>
{distributions.map((item) => (
<tr key={item.id}>
<tr
key={item.id}
className={onOpenDetail ? "clickable" : ""}
onClick={onOpenDetail ? () => onOpenDetail(item) : undefined}
title={onOpenDetail ? "点击查看分发详情" : undefined}
>
<td>
<div className="content-cell">
<span className="content-id">{item.content_id.split("-").at(-1)?.toUpperCase()}</span>
@@ -5220,7 +5341,7 @@ function DistributionTable({
)}
<td><span className={`status-pill ${item.status}`}>{statusLabel(item.status)}</span></td>
{onRelease && (
<td>
<td onClick={(event) => event.stopPropagation()}>
{!item.publish_url && !item.result_submitted_at ? (
<button
type="button"
@@ -6525,6 +6646,398 @@ function CreatorScreenshotPreview({
);
}
/* 发布截图预览(管理端核对发布凭证用) */
function PublishEvidencePreview({
distributionId,
title,
}: {
distributionId: string;
title: string;
}) {
const [open, setOpen] = useState(false);
const imageUrl = `/api/task-result-image?distribution=${encodeURIComponent(distributionId)}&kind=publish`;
return (
<>
<button
type="button"
className="detail-shot"
onClick={() => setOpen(true)}
title="查看发布截图大图"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={imageUrl} alt={`${title} 发布截图`} loading="lazy" />
<span>发布截图 · 点击查看</span>
</button>
<AdminImageLightbox
images={open ? [{ src: imageUrl, title: `${title} · 发布截图` }] : []}
activeIndex={0}
onClose={() => setOpen(false)}
/>
</>
);
}
/* ------------------------------------------------------------
分发详情弹窗:单条分发全量数据,按发布/回收双阶段自适应展示
------------------------------------------------------------ */
function DistributionDetailModal({
item,
recoveryEnabled,
working,
collectingDistributionIds,
canUpdatePublishUrl,
canRelease,
onClose,
onCollect,
onFallback,
onUpdatePublishUrl,
onRelease,
}: {
item: Distribution;
recoveryEnabled: boolean;
working: boolean;
collectingDistributionIds: ReadonlySet<string>;
canUpdatePublishUrl: boolean;
canRelease: boolean;
onClose: () => void;
onCollect: (distribution: Distribution) => void;
onFallback: (distribution: Distribution) => void;
onUpdatePublishUrl: (distribution: Distribution) => void;
onRelease: (distribution: Distribution) => void;
}) {
const [previewGallery, setPreviewGallery] = useState<{
images: AdminPreviewImage[];
activeIndex: number;
} | null>(null);
const metrics = latestPublicMetrics(item);
const noteUrl = publicPublishUrl(item.publish_url);
const collectionDays = parseCollectionDays(item.task_collection_days);
const resultKeys = parseResultScreenshotKeys(item.result_screenshot_key);
const collecting =
collectingDistributionIds.size > 0 && collectingDistributionIds.has(item.id);
const resolvedCollectionStatus =
item.collection_status || (metrics.likes !== null ? "success" : "pending");
return (
<div
className="modal-backdrop"
role="presentation"
onMouseDown={onClose}
>
<div
className="modal-card distribution-detail-modal"
role="dialog"
aria-modal="true"
aria-label={`分发详情:${item.content_title}`}
onMouseDown={(event) => event.stopPropagation()}
>
{/* 头部:标题 + 阶段徽章 + 任务元信息 */}
<div className="distribution-detail-header">
<div className="modal-heading" style={{ marginBottom: 0 }}>
<div>
<p className="eyebrow">分发详情</p>
<h2>{item.content_title}</h2>
</div>
<button type="button" onClick={onClose} aria-label="关闭">×</button>
</div>
<div className="detail-badges">
<span className={`status-pill ${item.status}`}>{statusLabel(item.status)}</span>
<PlatformBadge platform={item.task_platform} compact />
<span className="master-chip">{item.content_format === "video" ? "视频" : "图文"}</span>
<span className={`due-badge${dueBadgeState(item.due_at)}`}>
截止 {formatDate(item.due_at)}
</span>
</div>
<div className="detail-meta">
<span>{item.task_name}</span>
<span>· {item.task_brand}</span>
<span>· 合作方 {item.partner_name}</span>
{item.claimant_name && <span>· 领取人 {item.claimant_name}</span>}
</div>
</div>
{/* 内容:四个阶段区块(未开通回收的任务不展示截图回收) */}
<div className="detail-body">
<section className="detail-section">
<div className="detail-section-head">
<b>① 发布阶段</b>
<span className={`status-pill ${item.publish_url ? "published" : "claimed"}`}>
{item.publish_url ? "已发布" : "待发布"}
</span>
</div>
<div className="detail-section-body">
<div className="detail-kv-grid">
<div className="detail-kv">
<span>实际发布账号</span>
{item.account_nickname ? (
<strong>{item.account_nickname}</strong>
) : (
<span className="muted">回填链接后识别</span>
)}
</div>
<div className="detail-kv">
<span>发布时间</span>
<strong>{formatDate(item.publish_time, true) || "—"}</strong>
</div>
<div className="detail-kv">
<span>发布链接</span>
{noteUrl ? (
<a href={noteUrl} target="_blank" rel="noopener noreferrer">
打开作品 ↗
</a>
) : (
<span className="muted">待回填</span>
)}
</div>
<div className="detail-kv">
<span>领取时间</span>
<strong>{formatDate(item.claimed_at, true)}</strong>
</div>
</div>
<div className="detail-evidence">
<span className="detail-evidence-label">发布截图</span>
{item.publish_screenshot_key ? (
<PublishEvidencePreview
distributionId={item.id}
title={item.content_title}
/>
) : (
<span className="muted" style={{ fontSize: 12 }}>
待KOC上传
</span>
)}
</div>
</div>
</section>
<section className="detail-section">
<div className="detail-section-head">
<b>② 数据回收(自动采集)</b>
<span className={`collection-status ${resolvedCollectionStatus}`}>
{collectionStatusLabel(resolvedCollectionStatus, metrics.likes !== null)}
</span>
</div>
<div className="detail-section-body">
<div className="detail-metric-row">
<div className="detail-metric">
<span>点赞</span>
<strong>{formatNumber(metrics.likes)}</strong>
</div>
<div className="detail-metric">
<span>收藏</span>
<strong>{formatNumber(metrics.collects)}</strong>
</div>
{item.task_platform === "抖音" && (
<div className="detail-metric">
<span>转发</span>
<strong>{formatNumber(metrics.shares)}</strong>
</div>
)}
<div className="detail-metric">
<span>评论</span>
<strong>{formatNumber(metrics.comments)}</strong>
</div>
<div className="detail-metric">
<span>总互动</span>
<strong>{formatNumber(metrics.total)}</strong>
</div>
</div>
<div className="detail-kv-grid" style={{ marginTop: 11 }}>
<div className="detail-kv">
<span>数据更新时间</span>
<strong>
{formatDate(
item.collection_updated_at ||
(metrics.likes !== null ? item.updated_at : null),
true,
) || "—"}
</strong>
</div>
<div className="detail-kv">
<span>采集计划</span>
<strong>
{item.task_collection_start_date && collectionDays.length > 0
? `${item.task_collection_start_date} 起 D${collectionDays.join("/D")}`
: "待设置"}
</strong>
</div>
</div>
{(item.collection_status_description || item.public_metrics_screenshot_key) && (
<div className="detail-evidence">
{item.collection_status_description && (
<span
className={`detail-evidence-label${
resolvedCollectionStatus === "failed" ? " warn" : ""
}`}
>
{resolvedCollectionStatus === "failed" ? "⚠ " : ""}
{item.collection_status_description}
</span>
)}
{item.public_metrics_screenshot_key && (
<a
className="text-button"
href={`/api/creator-screenshot?distribution=${encodeURIComponent(item.id)}&kind=public-metrics`}
target="_blank"
rel="noreferrer"
>
KOC补交截图 ↗
</a>
)}
</div>
)}
</div>
</section>
<section className="detail-section">
<div className="detail-section-head">
<b>③ 创作者数据(第7天)</b>
<span className={`status-pill ${hasCreatorMetrics(item) ? "complete" : "claimed"}`}>
{hasCreatorMetrics(item) ? "已提交" : "待提交"}
</span>
</div>
<div className="detail-section-body">
<div className="detail-evidence" style={{ marginTop: 0 }}>
<span className="detail-evidence-label">创作者截图</span>
{item.screenshot_key ? (
<CreatorScreenshotPreview
distributionId={item.id}
title={item.content_title}
/>
) : (
<span className="muted" style={{ fontSize: 12 }}>
待KOC上传
</span>
)}
{hasCreatorMetrics(item) ? (
<span className="creator-data-summary">
<b>曝光 {formatNumber(item.exposure)}</b>
<b>阅读 {formatNumber(item.views)}</b>
</span>
) : item.screenshot_key ? (
<span className="creator-ocr-failed">
{item.ocr_status === "processing" ? "正在识别数据" : "识别失败,可手动填写"}
</span>
) : null}
</div>
</div>
</section>
{recoveryEnabled && (
<section className="detail-section">
<div className="detail-section-head">
<b>④ 截图回收</b>
<span
className={`status-pill ${item.result_submitted_at ? "complete" : "claimed"}`}
>
{item.result_submitted_at ? "已提交" : "待提交"}
</span>
</div>
<div className="detail-section-body">
<div className="detail-kv-grid">
<div className="detail-kv">
<span>提交时间</span>
<strong>
{item.result_submitted_at
? formatDate(item.result_submitted_at, true)
: "—"}
</strong>
</div>
<div className="detail-kv">
<span>结果截图</span>
{resultKeys.length > 0 ? (
<strong>{resultKeys.length} 张</strong>
) : (
<span className="muted">待KOC上传</span>
)}
</div>
</div>
{resultKeys.length > 0 && (
<div className="detail-evidence">
<div className="admin-result-gallery">
{resultKeys.map((_, index) => (
<button
type="button"
key={`${item.id}-result-${index}`}
title={`查看第${index + 1}张结果截图`}
onClick={() =>
setPreviewGallery({
images: resultKeys.map((__, imageIndex) => ({
src: `/api/task-result-image?distribution=${encodeURIComponent(item.id)}&index=${imageIndex + 1}`,
title: `${item.content_title} · 第${imageIndex + 1}张结果截图`,
})),
activeIndex: index,
})
}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`/api/task-result-image?distribution=${encodeURIComponent(item.id)}&index=${index + 1}`}
alt={`结果截图 ${index + 1}`}
loading="lazy"
/>
</button>
))}
</div>
</div>
)}
</div>
</section>
)}
</div>
{/* 底部操作条 */}
<div className="detail-actions">
{canUpdatePublishUrl && (
<button
className="publish-url-edit-button"
disabled={working}
onClick={() => onUpdatePublishUrl(item)}
>
{noteUrl ? "更新链接" : "填写链接"}
</button>
)}
{item.screenshot_key && !hasCreatorMetrics(item) ? (
<button className="text-button" onClick={() => onFallback(item)}>
手动填写
</button>
) : item.collection_status === "failed" ? (
<button className="text-button" onClick={() => onFallback(item)}>
核对补录
</button>
) : (
<button
className="collect-button"
disabled={!noteUrl || working || collecting}
onClick={() => onCollect(item)}
>
{noteUrl ? "立即采集" : "待填链接"}
</button>
)}
{canRelease && !item.publish_url && !item.result_submitted_at && (
<button
className="release-distribution-button"
disabled={working}
onClick={() => onRelease(item)}
>
释放
</button>
)}
</div>
</div>
<AdminImageLightbox
images={previewGallery?.images ?? []}
activeIndex={previewGallery?.activeIndex ?? 0}
onIndexChange={(activeIndex) =>
setPreviewGallery((current) =>
current ? { ...current, activeIndex } : current,
)
}
onClose={() => setPreviewGallery(null)}
/>
</div>
);
}
function RecoveryPage({
tasks,
distributions,
@@ -6542,6 +7055,7 @@ function RecoveryPage({
onExport,
onExportScreenshots,
exportingTaskId,
onOpenDetail,
}: {
tasks: Task[];
distributions: Distribution[];
@@ -6563,6 +7077,7 @@ function RecoveryPage({
onExport: (task: Task) => void;
onExportScreenshots: (task: Task) => void;
exportingTaskId: string | null;
onOpenDetail: (distribution: Distribution) => void;
}) {
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [recoverySort, setRecoverySort] = useState<{
@@ -6664,7 +7179,11 @@ function RecoveryPage({
const publishPending = taskDistributions.filter(
(item) => !item.publish_url && !item.result_submitted_at,
).length;
const pendingCount = publishPending + (recoveryEnabled ? recoveryPending : 0);
// 与催办口径一致:待发布 + 截图待交 + 公开数据采集失败(催 KOC 补交数据截图)
const pendingCount =
publishPending +
(recoveryEnabled ? recoveryPending : 0) +
failedCollectionCount;
return (
<button
className="retry-failed-button"
@@ -6749,7 +7268,12 @@ function RecoveryPage({
item.collection_status ||
(hasMetrics ? "success" : "pending");
return (
<tr key={item.id}>
<tr
key={item.id}
className="clickable"
onClick={() => onOpenDetail(item)}
title="点击查看分发详情"
>
<td>
<div className="account-cell recovery-content">
<div className={`avatar small ${avatarColor(item.content_title)}`}>{Array.from(item.content_title)[0] || "笔"}</div>
@@ -6798,7 +7322,7 @@ function RecoveryPage({
{item.last_collection_day && <span>第{item.last_collection_day}天采集</span>}
</div>
</td>
<td>
<td onClick={(event) => event.stopPropagation()}>
<div className="collection-status-cell">
<span className={`collection-status ${collectionStatus}`}>
{collectionStatusLabel(collectionStatus, hasMetrics)}
@@ -6807,9 +7331,20 @@ function RecoveryPage({
{item.collection_status_description ||
(hasMetrics ? "成功" : "等待采集计划")}
</small>
{item.public_metrics_screenshot_key && (
<a
className="text-button"
href={`/api/creator-screenshot?distribution=${encodeURIComponent(item.id)}&kind=public-metrics`}
target="_blank"
rel="noreferrer"
title="查看 KOC 补交的公开数据截图"
>
KOC补交截图 ↗
</a>
)}
</div>
</td>
<td>
<td onClick={(event) => event.stopPropagation()}>
<div className="creator-screenshot-cell">
{item.screenshot_key ? (
<CreatorScreenshotPreview
@@ -6860,10 +7395,12 @@ function RecoveryPage({
</div>
</td>
)}
<td>
<td onClick={(event) => event.stopPropagation()}>
<div className="recovery-row-actions">
{item.screenshot_key && !hasCreatorMetrics(item) ? (
<button className="text-button" onClick={() => onFallback(item)}>手动填写</button>
) : item.collection_status === "failed" ? (
<button className="text-button" onClick={() => onFallback(item)}>核对补录</button>
) : (
<button
className="collect-button"