diff --git a/app/admin-app.tsx b/app/admin-app.tsx index 12df605..0d27504 100644 --- a/app/admin-app.tsx +++ b/app/admin-app.tsx @@ -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(null); const [customerChatTask, setCustomerChatTask] = useState(null); const [metricModal, setMetricModal] = useState(null); + const [detailDistributionId, setDetailDistributionId] = useState(null); const [publishUrlModal, setPublishUrlModal] = useState<{ distribution: Distribution; value: string; @@ -572,7 +575,13 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { const [taskFormProjects, setTaskFormProjects] = useState>([]); const [sourcePreview, setSourcePreview] = useState(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(null); const [exportingResources, setExportingResources] = useState(false); const [creatingPartnerId, setCreatingPartnerId] = useState(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 }) {
event.stopPropagation()}>
-

OCR异常兜底

-

确认创作者数据

+

+ {metricModal.collection_status === "failed" + ? "采集失败兜底 · 数据核对补录" + : "OCR异常兜底"} +

+

核对并补录数据

@@ -1688,19 +1734,36 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { {metricModal.account_nickname || "待识别账号"} {metricModal.content_title} -
- - -
+ + + {metricModal.public_metrics_screenshot_key && ( + + 查看 KOC 补交的数据截图 ↗ + + )}
- +
@@ -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 ( + 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 &&
✓{toast}
} {working &&
}
@@ -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; + onOpenDetail: (distribution: Distribution) => void; }) { const [selectedTaskId, setSelectedTaskId] = useState(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; onBack: () => void; + onOpenDetail: (distribution: Distribution) => void; }) { const [releaseTarget, setReleaseTarget] = useState(null); const taskDistributions = distributions.filter( @@ -5059,6 +5172,7 @@ function TaskDistributionDetail({ distributions={taskDistributions} onRelease={canRelease ? setReleaseTarget : undefined} showRecovery={recoveryEnabled} + onOpenDetail={onOpenDetail} /> {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({ {distributions.map((item) => ( - + onOpenDetail(item) : undefined} + title={onOpenDetail ? "点击查看分发详情" : undefined} + >
{item.content_id.split("-").at(-1)?.toUpperCase()} @@ -5220,7 +5341,7 @@ function DistributionTable({ )} {statusLabel(item.status)} {onRelease && ( - + event.stopPropagation()}> {!item.publish_url && !item.result_submitted_at ? ( + setOpen(false)} + /> + + ); +} + +/* ------------------------------------------------------------ + 分发详情弹窗:单条分发全量数据,按发布/回收双阶段自适应展示 + ------------------------------------------------------------ */ +function DistributionDetailModal({ + item, + recoveryEnabled, + working, + collectingDistributionIds, + canUpdatePublishUrl, + canRelease, + onClose, + onCollect, + onFallback, + onUpdatePublishUrl, + onRelease, +}: { + item: Distribution; + recoveryEnabled: boolean; + working: boolean; + collectingDistributionIds: ReadonlySet; + 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 ( +
+
event.stopPropagation()} + > + {/* 头部:标题 + 阶段徽章 + 任务元信息 */} +
+
+
+

分发详情

+

{item.content_title}

+
+ +
+
+ {statusLabel(item.status)} + + {item.content_format === "video" ? "视频" : "图文"} + + 截止 {formatDate(item.due_at)} + +
+
+ {item.task_name} + · {item.task_brand} + · 合作方 {item.partner_name} + {item.claimant_name && · 领取人 {item.claimant_name}} +
+
+ + {/* 内容:四个阶段区块(未开通回收的任务不展示截图回收) */} +
+
+
+ ① 发布阶段 + + {item.publish_url ? "已发布" : "待发布"} + +
+
+
+
+ 实际发布账号 + {item.account_nickname ? ( + {item.account_nickname} + ) : ( + 回填链接后识别 + )} +
+
+ 发布时间 + {formatDate(item.publish_time, true) || "—"} +
+
+ 发布链接 + {noteUrl ? ( + + 打开作品 ↗ + + ) : ( + 待回填 + )} +
+
+ 领取时间 + {formatDate(item.claimed_at, true)} +
+
+
+ 发布截图 + {item.publish_screenshot_key ? ( + + ) : ( + + 待KOC上传 + + )} +
+
+
+ +
+
+ ② 数据回收(自动采集) + + {collectionStatusLabel(resolvedCollectionStatus, metrics.likes !== null)} + +
+
+
+
+ 点赞 + {formatNumber(metrics.likes)} +
+
+ 收藏 + {formatNumber(metrics.collects)} +
+ {item.task_platform === "抖音" && ( +
+ 转发 + {formatNumber(metrics.shares)} +
+ )} +
+ 评论 + {formatNumber(metrics.comments)} +
+
+ 总互动 + {formatNumber(metrics.total)} +
+
+
+
+ 数据更新时间 + + {formatDate( + item.collection_updated_at || + (metrics.likes !== null ? item.updated_at : null), + true, + ) || "—"} + +
+
+ 采集计划 + + {item.task_collection_start_date && collectionDays.length > 0 + ? `${item.task_collection_start_date} 起 D${collectionDays.join("/D")}` + : "待设置"} + +
+
+ {(item.collection_status_description || item.public_metrics_screenshot_key) && ( +
+ {item.collection_status_description && ( + + {resolvedCollectionStatus === "failed" ? "⚠ " : ""} + {item.collection_status_description} + + )} + {item.public_metrics_screenshot_key && ( + + KOC补交截图 ↗ + + )} +
+ )} +
+
+ +
+
+ ③ 创作者数据(第7天) + + {hasCreatorMetrics(item) ? "已提交" : "待提交"} + +
+
+
+ 创作者截图 + {item.screenshot_key ? ( + + ) : ( + + 待KOC上传 + + )} + {hasCreatorMetrics(item) ? ( + + 曝光 {formatNumber(item.exposure)} + 阅读 {formatNumber(item.views)} + + ) : item.screenshot_key ? ( + + {item.ocr_status === "processing" ? "正在识别数据" : "识别失败,可手动填写"} + + ) : null} +
+
+
+ + {recoveryEnabled && ( +
+
+ ④ 截图回收 + + {item.result_submitted_at ? "已提交" : "待提交"} + +
+
+
+
+ 提交时间 + + {item.result_submitted_at + ? formatDate(item.result_submitted_at, true) + : "—"} + +
+
+ 结果截图 + {resultKeys.length > 0 ? ( + {resultKeys.length} 张 + ) : ( + 待KOC上传 + )} +
+
+ {resultKeys.length > 0 && ( +
+
+ {resultKeys.map((_, index) => ( + + ))} +
+
+ )} +
+
+ )} +
+ + {/* 底部操作条 */} +
+ {canUpdatePublishUrl && ( + + )} + {item.screenshot_key && !hasCreatorMetrics(item) ? ( + + ) : item.collection_status === "failed" ? ( + + ) : ( + + )} + {canRelease && !item.publish_url && !item.result_submitted_at && ( + + )} +
+
+ + setPreviewGallery((current) => + current ? { ...current, activeIndex } : current, + ) + } + onClose={() => setPreviewGallery(null)} + /> +
+ ); +} + 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(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 ( + ) : item.collection_status === "failed" ? ( + ) : ( + ) : ( +
+ + + 点击上传笔记数据页截图 + 需包含点赞、收藏、评论数字,最大8MB +
+ )} + +
+ + + + )} {selected.publish_url && (
@@ -1689,8 +1807,23 @@ export default function Home() { {creatorDataReady ? "已提交" : "待提交"}
+ {selected.latest_likes !== null && + selected.latest_likes !== undefined ? ( +

+ 公开数据系统自动采集中:赞 {selected.latest_likes ?? "—"} · 藏{" "} + {selected.latest_collects ?? "—"} · 评 {selected.latest_comments ?? "—"} + {selected.collection_updated_at + ? `(${formatDate(selected.collection_updated_at, true)})` + : ""} + ,无需你填写。 +

+ ) : ( +

+ 点赞、收藏、评论等公开数据由系统每天自动采集,无需你填写。 +

+ )}

- 请在发布满7天后,上传创作者中心数据页完整截图,并填写页面显示的数据。 + 这一步只需做一次:发布满7天后,上传创作者中心数据页截图,系统自动识别曝光量和阅读量;仅当识别失败时才需要手动填写这两个数字。 {creatorDueAt && ` 建议上传日期:${formatDate(creatorDueAt)}。`}

@@ -1772,6 +1905,9 @@ export default function Home() { />
+ + 两个数字一般由截图自动识别填入;只有识别失败时才需要手动修改。 +