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

@@ -1683,6 +1683,28 @@ footer {
line-height: 1.65;
}
/* 公开数据自动采集状态行:让 KOC 明白数据不用每天手动填 */
.auto-collect-line {
margin: 10px 0 0;
padding: 8px 11px;
border-radius: 8px;
background: #e9f5f0;
border: 1px solid #cdeade;
color: #27775b;
font-size: 8px;
line-height: 1.6;
}
/* 公开数据补交(采集失败兜底)区块 */
.metrics-fallback {
border-top-color: #f3c6cf;
}
.metrics-fallback .creator-recovery-heading > span {
background: #ffeef1;
color: #be123c;
}
.screenshot-picker.compact {
min-height: 96px;
}

View File

@@ -23,6 +23,12 @@ type Assignment = {
ocr_status: string;
exposure: number | null;
views: number | null;
collection_status?: string | null;
latest_likes?: number | null;
latest_comments?: number | null;
latest_collects?: number | null;
collection_updated_at?: string | null;
public_metrics_screenshot_key?: string | null;
title: string;
body: string;
source_row: number | null;
@@ -460,6 +466,7 @@ export default function Home() {
const [creatorScreenshot, setCreatorScreenshot] = useState<File | null>(null);
const [creatorExposure, setCreatorExposure] = useState("");
const [creatorViews, setCreatorViews] = useState("");
const [publicMetricsShot, setPublicMetricsShot] = useState<File | null>(null);
const [entryMode, setEntryMode] = useState<"claim" | "recover">("claim");
const [recoveredClaims, setRecoveredClaims] = useState<RecoveredClaim[]>([]);
const [wecomIdentity, setWecomIdentity] = useState<WecomPortalIdentity | null>(
@@ -485,6 +492,7 @@ export default function Home() {
const batchWorkbookInputRef = useRef<HTMLInputElement | null>(null);
const publishScreenshotPreview = useFilePreview(screenshot);
const creatorScreenshotPreview = useFilePreview(creatorScreenshot);
const publicMetricsPreview = useFilePreview(publicMetricsShot);
const taskResultPreviews = useMemo(
() =>
taskResultScreenshots.map((file) => ({
@@ -678,6 +686,7 @@ export default function Home() {
setScreenshot(null);
setTaskResultScreenshots([]);
setCreatorScreenshot(null);
setPublicMetricsShot(null);
setCreatorExposure(
selected.exposure === null ? "" : String(selected.exposure),
);
@@ -844,6 +853,36 @@ export default function Home() {
return `${adminOrigin}/api/partner-image?${params}`;
};
const publicMetricsImageUrl = () => {
if (!selected) return "";
const params = new URLSearchParams({
distribution: selected.id,
index: "1",
kind: "public-metrics",
});
if (delegationToken) params.set("share", delegationToken);
else {
params.set("task", taskToken);
params.set("claim", claimToken);
}
return `${adminOrigin}/api/partner-image?${params}`;
};
const submitPublicMetricsShot = async () => {
if (!selected || !publicMetricsShot) return;
try {
setWorking(true);
await uploadEvidence(selected, publicMetricsShot, "public-metrics");
setPublicMetricsShot(null);
await loadTask(taskToken, claimToken, delegationToken);
setToast("数据截图已补交,运营核对后会补录公开数据");
} catch (reason) {
setToast(reason instanceof Error ? reason.message : "数据截图上传失败");
} finally {
setWorking(false);
}
};
const evidenceImageUrl = (
item: Assignment,
kind: "publish" | "creator",
@@ -1207,7 +1246,7 @@ export default function Home() {
const uploadEvidence = async (
selectedItem: Assignment,
file: File,
kind: "publish" | "creator-center" | "task-result",
kind: "publish" | "creator-center" | "task-result" | "public-metrics",
): Promise<{ exposure?: number | null; views?: number | null; ocrStatus?: string }> => {
const compressed = await compressScreenshot(file);
const headers: Record<string, string> = {
@@ -1678,6 +1717,85 @@ export default function Home() {
{selected.publish_time && (
<p className="submitted-at">上次回填:{formatDate(selected.publish_time, true)}</p>
)}
{selected.publish_url && selected.collection_status === "failed" && (
<section className="creator-recovery metrics-fallback">
<div className="creator-recovery-heading">
<div>
<p className="micro">公开数据补交</p>
<h3>自动采集失败,请补交数据截图</h3>
</div>
<span className={selected.public_metrics_screenshot_key ? "done" : ""}>
{selected.public_metrics_screenshot_key ? "已补交,待核对" : "待补交"}
</span>
</div>
<p className="creator-recovery-tip">
系统自动采集这篇笔记的公开数据失败了。请打开笔记页面,
截一张包含点赞、收藏、评论数字的截图上传,运营核对后会补录数据。
</p>
<div className="evidence-field compact-field">
<span>数据截图</span>
<div
className={`screenshot-picker compact ${
publicMetricsShot || selected.public_metrics_screenshot_key
? "has-file preview-mode"
: ""
}`}
>
{publicMetricsShot || selected.public_metrics_screenshot_key ? (
<button
type="button"
className="evidence-preview-button compact-preview"
onClick={() =>
setPreviewImage({
src:
publicMetricsPreview ||
publicMetricsImageUrl(),
alt: "公开数据截图",
})
}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={publicMetricsPreview || publicMetricsImageUrl()}
alt="公开数据截图"
/>
<span>点击查看大图</span>
</button>
) : (
<div className="evidence-empty-state">
<b>+</b>
<strong>点击上传笔记数据页截图</strong>
<small>需包含点赞、收藏、评论数字,最大8MB</small>
</div>
)}
<label className="evidence-upload-action">
<input
type="file"
accept="image/*"
onChange={(event) =>
setPublicMetricsShot(event.target.files?.[0] ?? null)
}
/>
{publicMetricsShot || selected.public_metrics_screenshot_key
? "重新选择"
: "选择图片"}
</label>
</div>
</div>
<button
type="button"
className="creator-submit"
disabled={!publicMetricsShot || working}
onClick={() => void submitPublicMetricsShot()}
>
{working
? "正在上传…"
: selected.public_metrics_screenshot_key
? "更新数据截图"
: "提交数据截图"}
</button>
</section>
)}
{selected.publish_url && (
<section className="creator-recovery">
<div className="creator-recovery-heading">
@@ -1689,8 +1807,23 @@ export default function Home() {
{creatorDataReady ? "已提交" : "待提交"}
</span>
</div>
{selected.latest_likes !== null &&
selected.latest_likes !== undefined ? (
<p className="auto-collect-line">
公开数据系统自动采集中:赞 {selected.latest_likes ?? "—"} · 藏{" "}
{selected.latest_collects ?? "—"} · 评 {selected.latest_comments ?? "—"}
{selected.collection_updated_at
? `(${formatDate(selected.collection_updated_at, true)})`
: ""}
,无需你填写。
</p>
) : (
<p className="auto-collect-line">
点赞、收藏、评论等公开数据由系统每天自动采集,无需你填写。
</p>
)}
<p className="creator-recovery-tip">
请在发布满7天后,上传创作者中心数据页完整截图,并填写页面显示的数据。
这一步只需做一次:发布满7天后,上传创作者中心数据页截图,系统自动识别曝光量和阅读量;仅当识别失败时才需要手动填写这两个数字。
{creatorDueAt && ` 建议上传日期:${formatDate(creatorDueAt)}。`}
</p>
<div className="evidence-field compact-field">
@@ -1772,6 +1905,9 @@ export default function Home() {
/>
</label>
</div>
<small className="task-result-hint">
两个数字一般由截图自动识别填入;只有识别失败时才需要手动修改。
</small>
<button
type="button"
className="creator-submit"