diff --git a/app/admin-app.tsx b/app/admin-app.tsx index c91f9ac..6b6cd87 100644 --- a/app/admin-app.tsx +++ b/app/admin-app.tsx @@ -52,7 +52,6 @@ type Task = { claimed_quantity: number; due_at: string; status: string; - task_type: "content_publish" | "screenshot_collect"; platform: "小红书" | "抖音"; content_format: "image_text" | "video"; project_id?: string; @@ -64,14 +63,9 @@ type Task = { join_qr_code?: string; keyword?: string; instructions?: string; - related_task_id?: string; - related_task_name?: string | null; - related_dist_total?: number; - related_dist_complete?: number; - reverse_related_task_id?: string | null; - reverse_related_task_name?: string | null; - reverse_dist_total?: number; - reverse_dist_submitted?: number; + recovery_enabled?: number; + recovery_due_at?: string; + recovery_instructions?: string | null; dist_total?: number; dist_complete?: number; dist_submitted?: number; @@ -185,15 +179,12 @@ type Distribution = { last_collection_day: number | null; updated_at: string; content_title: string; - related_content_id?: string; - related_content_title?: string | null; partner_name: string; claimant_name: string | null; account_nickname: string | null; account_platform: string | null; task_name: string; task_brand: string; - task_type: "content_publish" | "screenshot_collect"; task_platform: "小红书" | "抖音"; content_format: "image_text" | "video"; due_at: string; @@ -336,6 +327,13 @@ function statusLabel(status: string) { return labels[status] || status; } +function dueBadgeState(dueAt: string) { + const time = new Date(dueAt).getTime(); + if (Number.isNaN(time)) return ""; + if (time < Date.now()) return " overdue"; + return time - Date.now() <= 3 * 24 * 60 * 60 * 1000 ? " soon" : ""; +} + function latestPublicMetrics(distribution: Distribution) { const legacyDay = distribution.d7_likes !== null @@ -550,19 +548,17 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { const [metricModal, setMetricModal] = useState(null); const [taskForm, setTaskForm] = useState({ id: "", - taskType: "content_publish" as "content_publish" | "screenshot_collect", platform: "小红书" as "小红书" | "抖音", contentFormat: "image_text" as "image_text" | "video", name: "", brand: "", dueAt: "2026-08-12", feishuUrl: "", - relatedTaskId: "", - keyword: "", - instructions: "在小红书搜索指定关键词,打开 AI 总结并上传完整截图。", - quantity: 1, projectId: "", ownerUserid: "", + recoveryEnabled: true, + recoveryDueAt: "", + recoveryInstructions: "", }); const [taskOwnerPickerOpen, setTaskOwnerPickerOpen] = useState(false); const [taskExampleImage, setTaskExampleImage] = useState(null); @@ -663,7 +659,6 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { const stats = useMemo(() => { const published = data.distributions.filter((item) => - item.task_type !== "screenshot_collect" && ["published", "collecting", "complete"].includes(item.status), ).length; const completed = data.distributions.filter( @@ -675,20 +670,19 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { (latestPublicMetrics(item).likes === null || !hasCreatorMetrics(item)), ).length; - const screenshotPending = data.distributions.filter( - (item) => - item.task_type === "screenshot_collect" && !item.result_submitted_at, - ).length; - const pairedTotals = data.tasks.reduce( + const recoveryTotals = data.tasks.reduce( (acc, task) => { - if (!task.reverse_related_task_id) return acc; + if (Number(task.recovery_enabled ?? 1) !== 1) return acc; return { - submitted: acc.submitted + (task.reverse_dist_submitted ?? 0), - total: acc.total + (task.reverse_dist_total ?? 0), + submitted: acc.submitted + (task.dist_submitted ?? 0), + total: acc.total + (task.dist_total ?? 0), }; }, { submitted: 0, total: 0 }, ); + const screenshotPending = data.distributions.filter( + (item) => item.publish_url && !item.result_submitted_at, + ).length; return { resources: data.accounts.length, activeTasks: data.tasks.filter((task) => task.status === "active").length, @@ -698,8 +692,8 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { completedTasks: data.tasks.filter((task) => task.status === "completed") .length, screenshotPending, - pairedSubmitted: pairedTotals.submitted, - pairedTotal: pairedTotals.total, + pairedSubmitted: recoveryTotals.submitted, + pairedTotal: recoveryTotals.total, }; }, [data]); @@ -722,21 +716,20 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { setToast("请选择所属项目"); return; } - if (!editing && taskForm.taskType === "content_publish" && !sourcePreview) { + if (!editing && !sourcePreview) { setToast("请先读取飞书表格"); return; } if ( !editing && - taskForm.taskType === "content_publish" && taskForm.contentFormat === "video" && (sourcePreview?.videoCount ?? 0) === 0 ) { setToast("当前飞书表格没有识别到视频附件"); return; } - let exampleImageKey = ""; - if (taskForm.taskType === "screenshot_collect" && taskExampleImage) { + let recoveryExampleImageKey = ""; + if (taskForm.recoveryEnabled && taskExampleImage) { try { setWorking(true); const form = new FormData(); @@ -752,63 +745,41 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { if (!uploadResponse.ok || !uploadResult.key) { throw new Error(uploadResult.error || "示例截图上传失败"); } - exampleImageKey = uploadResult.key; + recoveryExampleImageKey = uploadResult.key; } catch (reason) { setToast(reason instanceof Error ? reason.message : "示例截图上传失败"); setWorking(false); return; } } - const sharedFields = { + const fields = { name: taskForm.name, brand: taskForm.brand, dueAt: taskForm.dueAt, projectId: taskForm.projectId, ownerUserid: taskForm.ownerUserid, - }; - const screenshotFields = { - ...sharedFields, - keyword: taskForm.keyword, - instructions: taskForm.instructions, - quantity: taskForm.quantity, - exampleImageKey, - }; - const relatedSourceTask = data.tasks.find( - (task) => task.id === taskForm.relatedTaskId, - ); - const createScreenshotFields = { - name: taskForm.name, - relatedTaskId: taskForm.relatedTaskId, - dueAt: taskForm.dueAt, - instructions: taskForm.instructions, - exampleImageKey, - projectId: taskForm.projectId, - ownerUserid: taskForm.ownerUserid, - }; - const publishFields = { - ...sharedFields, platform: taskForm.platform, contentFormat: taskForm.contentFormat, feishuUrl: taskForm.feishuUrl, + recoveryEnabled: taskForm.recoveryEnabled, + recoveryDueAt: taskForm.recoveryEnabled + ? taskForm.recoveryDueAt || taskForm.dueAt + : "", + recoveryInstructions: taskForm.recoveryInstructions, + ...(recoveryExampleImageKey ? { recoveryExampleImageKey } : {}), }; const success = editing ? await runAction( { action: "task_update", taskId: taskForm.id, - ...(taskForm.taskType === "screenshot_collect" - ? screenshotFields - : publishFields), + ...fields, }, "任务已更新", ) : await runAction( - taskForm.taskType === "screenshot_collect" - ? { action: "create_screenshot_task", ...createScreenshotFields } - : { action: "create_task", ...publishFields }, - taskForm.taskType === "screenshot_collect" - ? `截图回收任务已创建,${relatedSourceTask?.quantity ?? 0} 份任务(与「${relatedSourceTask?.name ?? "分发任务"}」的笔记一一对应)进入可领取池` - : `任务已创建,${sourcePreview?.rowCount ?? 0} 篇内容进入可领取池`, + { action: "create_task", ...fields }, + `任务已创建,${sourcePreview?.rowCount ?? 0} 篇内容进入可领取池`, ); if (success) { setTaskModalOpen(false); @@ -816,19 +787,17 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { setTaskExampleImage(null); setTaskForm({ id: "", - taskType: "content_publish", platform: "小红书", contentFormat: "image_text", name: "", brand: "", dueAt: "2026-08-12", feishuUrl: "", - relatedTaskId: "", - keyword: "", - instructions: "在小红书搜索指定关键词,打开 AI 总结并上传完整截图。", - quantity: 1, projectId: "", ownerUserid: "", + recoveryEnabled: true, + recoveryDueAt: "", + recoveryInstructions: "", }); } }; @@ -1341,19 +1310,17 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { setTaskExampleImage(null); setTaskForm({ id: task.id, - taskType: task.task_type, platform: task.platform, contentFormat: task.content_format, name: task.name, brand: task.brand, dueAt: task.due_at, feishuUrl: "", - relatedTaskId: task.related_task_id ?? "", - keyword: task.keyword ?? "", - instructions: task.instructions ?? "", - quantity: task.quantity, projectId: task.project_id ?? "", ownerUserid: task.owner_userid ?? "", + recoveryEnabled: Number(task.recovery_enabled ?? 1) === 1, + recoveryDueAt: task.recovery_due_at || task.due_at, + recoveryInstructions: task.recovery_instructions ?? "", }); setTaskModalOpen(true); }} @@ -1361,6 +1328,19 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { onPushToGroups={(task) => setGroupPushTask(task)} onOwnerLinks={(task) => setOwnerLinksTask(task)} onBindCustomerChat={(task) => setCustomerChatTask(task)} + onCreateRecovery={async (task) => { + await runAction( + { + action: "task_enable_recovery", + taskId: task.id, + recoveryDueAt: task.recovery_due_at || task.due_at, + recoveryInstructions: + task.recovery_instructions || "请提交发布后的笔记截图", + recoveryExampleImageKey: "", + }, + "回收已开通", + ); + }} onRelease={async (distribution) => { const result = await runAction( { @@ -1434,32 +1414,7 @@ export default function Home({ currentUser }: { currentUser: AuthUser }) { -
- - -
- {taskForm.taskType === "content_publish" ? ( - <> - {!taskForm.id && ( + {!taskForm.id && (
)} - - ) : ( -
- {!taskForm.id ? ( - <> - - {(() => { - const related = data.tasks.find( - (task) => task.id === taskForm.relatedTaskId, - ); - return related ? ( -

- 将为「{related.name}」生成 {related.quantity} 份回收任务 -

- ) : null; - })()} - - ) : taskForm.relatedTaskId ? ( -
- - -
- ) : ( -
- - -
- )} -