feat: 任务模型物理合并,发布+回收并为单任务双阶段

- tasks 新增 recovery_enabled/recovery_due_at/recovery_instructions/recovery_example_images,回收挂自己名下(先发布后交截图),不再置 complete
- 存量 screenshot_collect 迁移并入父任务后删除(mysql/0020 含 DELETE,生产执行前务必备份)
- 新 action task_enable_recovery:ghost 面板一键开通回收,截止默认=发布截止
- 企微催办双阶段(待发布/截图待提交);超期自动释放仅发布阶段
- admin 删 groupTaskMasters 聚合改单卡双面板;portal 详情页回收截图区块+批次回收进度
- 测试 309 全绿 + Playwright E2E 20/20
This commit is contained in:
ABAPPLO
2026-09-24 15:52:45 +08:00
parent ab78b187c0
commit e2a93e22cf
29 changed files with 2219 additions and 2195 deletions

View File

@@ -702,6 +702,19 @@ footer {
linear-gradient(130deg, #142721, #235b47);
}
.hero-progress {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.recovery-progress {
color: #96b8ab;
font-size: 9px;
font-weight: 600;
}
.claimed-hero h1 {
margin: 0;
font-size: clamp(25px, 4vw, 38px);

View File

@@ -71,9 +71,17 @@ type TaskPayload = {
availableQuantity?: number;
dueAt: string;
status: string;
type: "content_publish" | "screenshot_collect";
type: "content_publish";
platform: "小红书" | "抖音";
contentFormat: "image_text" | "video";
recoveryEnabled?: boolean;
recoveryDueAt?: string;
recoveryInstructions?: string;
recoveryExampleImages?: Array<{
index: number;
width: number | null;
height: number | null;
}>;
};
claim: null | {
id: string;
@@ -250,10 +258,7 @@ function isMobilePortalViewport() {
);
}
function statusLabel(item: Assignment, taskType = "content_publish") {
if (taskType === "screenshot_collect") {
return item.result_submitted_at ? "已提交" : "待提交";
}
function statusLabel(item: Assignment) {
return item.publish_url ? "已回填" : "待发布";
}
@@ -714,11 +719,7 @@ export default function Home() {
setDelegationToken("");
updateUrl(result.claimToken);
await loadTask(taskToken, result.claimToken);
setToast(
payload?.task.type === "screenshot_collect"
? `已领取 ${result.claimedCount} 份,完成后逐份上传截图`
: `已领取 ${result.claimedCount} 篇,逐篇发布即可`,
);
setToast(`已领取 ${result.claimedCount} 篇,逐篇发布即可`);
} catch (reason) {
setToast(reason instanceof Error ? reason.message : "领取失败");
} finally {
@@ -795,6 +796,21 @@ export default function Home() {
return `${adminOrigin}/api/partner-image?${params}`;
};
const recoveryExampleImageUrl = (imageIndex: number) => {
if (!selected) return "";
const params = new URLSearchParams({
distribution: selected.id,
index: String(imageIndex),
kind: "task-example",
});
if (delegationToken) params.set("share", delegationToken);
else {
params.set("task", taskToken);
params.set("claim", claimToken);
}
return `${adminOrigin}/api/partner-image?${params}`;
};
const noteVideoUrl = (
item: Assignment,
videoIndex: number,
@@ -1275,9 +1291,13 @@ export default function Home() {
}
};
const submitScreenshotTask = async (event: FormEvent) => {
event.preventDefault();
const submitRecoveryScreenshots = async (event?: FormEvent) => {
event?.preventDefault();
if (!selected) return;
if (!selected.publish_url) {
setToast("请先回填这篇笔记的发布链接,再提交回收截图");
return;
}
try {
setWorking(true);
const existingScreenshots = resultScreenshotKeys(
@@ -1291,7 +1311,7 @@ export default function Home() {
);
}
} else if (existingScreenshots.length === 0) {
throw new Error("请上传任务要求的截图");
throw new Error("请上传发布后的笔记截图");
}
const response = await fetch(partnerApi("/api/partner"), {
method: "POST",
@@ -1310,8 +1330,8 @@ export default function Home() {
setTaskResultScreenshots([]);
setToast(
taskResultScreenshots.length > 1
? `${taskResultScreenshots.length}张任务截图已提交`
: "任务截图已提交",
? `${taskResultScreenshots.length}张回收截图已提交`
: "回收截图已提交",
);
} catch (reason) {
setToast(reason instanceof Error ? reason.message : "截图提交失败");
@@ -1407,149 +1427,6 @@ export default function Home() {
}
if (selected && activeBatch) {
if (payload.task.type === "screenshot_collect") {
return (
<main className="portal-shell detail-shell">
<header className="portal-topbar">
<div className="portal-brand"><span>K</span><strong>KOC LOOP</strong></div>
<div className="claimant-chip">
{payload.claim ? payload.claim.claimantName : "合作社转派 · 无需登录"}
</div>
</header>
<button className="back-link" onClick={closeNote}>← 返回我的任务</button>
<div className="detail-grid screenshot-task-detail">
<article className="note-document">
<div className="note-document-meta">
<span>截图任务 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1}</span>
<span>只需上传结果截图</span>
</div>
<div className="document-label-row">
<p className="document-label">小红书搜索关键词</p>
<PlatformBadge platform={payload.task.platform} compact />
</div>
<div className="note-title-row">
<h1>{selected.title}</h1>
<button
type="button"
className="inline-copy"
onClick={() => void copyText(selected.title, "搜索关键词已复制")}
>
复制关键词
</button>
</div>
<div className="document-rule" />
<p className="document-label">任务说明</p>
<div className="note-body">{selected.body}</div>
{selected.images.length > 0 && (
<section className="note-images">
<div className="note-images-heading">
<div>
<p className="document-label">示例截图</p>
<span>请参考示例,确保小红书 AI 总结完整可见</span>
</div>
</div>
<div className="note-image-grid">
{selected.images.map((image, index) => (
<div className="note-image-card" key={image.index}>
<a className="note-image-preview" href={noteImageUrl(selected, image.index)} target="_blank" rel="noreferrer">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={noteImageUrl(selected, image.index)} alt={`示例截图 ${index + 1}`} />
</a>
</div>
))}
</div>
</section>
)}
</article>
<form className="submit-card" onSubmit={submitScreenshotTask}>
<div className="submit-heading">
<div>
<p className="micro">截图回收</p>
<h2>上传小红书 AI 总结截图</h2>
</div>
<span className={selected.result_submitted_at ? "done" : "pending"}>
{statusLabel(selected, payload.task.type)}
</span>
</div>
<div className="mapping-note">
<span>1 : N</span>
<p>这些截图只会绑定到当前任务,不需要填写发布链接或其他数据。</p>
</div>
<label>
<span className="task-result-label-row">
<span>任务结果截图</span>
<small>
已选择 {resultScreenshotKeys(selected.result_screenshot_key).length + taskResultScreenshots.length}/{MAX_TASK_RESULT_SCREENSHOTS} 张
</small>
</span>
<div className="task-result-gallery">
{resultScreenshotKeys(selected.result_screenshot_key).map((_, index) => {
const imageUrl = taskResultImageUrl(selected, index + 1);
return (
<button
type="button"
className="task-result-thumb uploaded"
onClick={() =>
setPreviewImage({
src: imageUrl,
alt: `已上传任务截图 ${index + 1}`,
})
}
key={`uploaded-${index}`}
>
<img src={imageUrl} alt={`已上传任务截图 ${index + 1}`} />
<span>已上传</span>
</button>
)})}
{taskResultPreviews.map(({ file, url }, index) => (
<div className="task-result-thumb pending-upload" key={`${file.name}-${file.lastModified}`}>
<img src={url} alt={`待上传任务截图 ${index + 1}`} />
<button
type="button"
aria-label={`移除 ${file.name}`}
onClick={() => removeTaskResultScreenshot(file)}
>
×
</button>
<span>待提交</span>
</div>
))}
{resultScreenshotKeys(selected.result_screenshot_key).length + taskResultScreenshots.length < MAX_TASK_RESULT_SCREENSHOTS && (
<div className="task-result-add">
<input
type="file"
accept="image/*"
multiple
onChange={chooseTaskResultScreenshots}
/>
<b>+</b>
<strong>
{taskResultScreenshots.length || selected.result_screenshot_key
? "继续添加"
: "选择截图"}
</strong>
</div>
)}
</div>
<small className="task-result-hint">支持一次选择或继续追加,最多9张,每张不超过8MB;点击图片可在当前页面查看大图。</small>
</label>
<button className="submit-primary" disabled={working}>
{working
? "正在提交…"
: selected.result_submitted_at
? "更新任务截图"
: "确认提交任务截图"}
</button>
{selected.result_submitted_at && (
<p className="submitted-at">上次提交:{formatDate(selected.result_submitted_at, true)}</p>
)}
</form>
</div>
<ImageLightbox image={previewImage} onClose={() => setPreviewImage(null)} />
{toast && <div className={toastClassName(toast)}>{toast}</div>}
</main>
);
}
const creatorDueAt = creatorScreenshotDueAt(selected.publish_time);
const creatorDataReady =
Boolean(selected.creator_screenshot_key) &&
@@ -1908,6 +1785,153 @@ export default function Home() {
</button>
</section>
)}
{payload.task.recoveryEnabled && (
<section className="creator-recovery task-recovery">
<div className="creator-recovery-heading">
<div>
<p className="micro">截图回收</p>
<h3>上传发布后的笔记截图</h3>
</div>
<span className={selected.result_submitted_at ? "done" : ""}>
{selected.result_submitted_at ? "已提交" : "待提交"}
</span>
</div>
<p className="creator-recovery-tip">
{payload.task.recoveryInstructions || "请提交发布后的笔记截图"}
{payload.task.recoveryDueAt
? ` 截止:${formatDate(payload.task.recoveryDueAt)}。`
: ""}
</p>
{(payload.task.recoveryExampleImages?.length ?? 0) > 0 && (
<div className="task-result-gallery recovery-example">
{payload.task.recoveryExampleImages?.map((image) => (
<button
type="button"
className="task-result-thumb uploaded"
key={`recovery-example-${image.index}`}
onClick={() =>
setPreviewImage({
src: recoveryExampleImageUrl(image.index),
alt: "回收示例截图",
})
}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={recoveryExampleImageUrl(image.index)}
alt="回收示例截图"
/>
<span>示例</span>
</button>
))}
</div>
)}
{selected.publish_url ? (
<>
<div className="evidence-field compact-field">
<span className="task-result-label-row">
<span>回收截图</span>
<small>
已选择{" "}
{resultScreenshotKeys(selected.result_screenshot_key)
.length +
taskResultScreenshots.length}
/{MAX_TASK_RESULT_SCREENSHOTS} 张
</small>
</span>
<div className="task-result-gallery">
{resultScreenshotKeys(selected.result_screenshot_key).map(
(_, index) => {
const imageUrl = taskResultImageUrl(selected, index + 1);
return (
<button
type="button"
className="task-result-thumb uploaded"
onClick={() =>
setPreviewImage({
src: imageUrl,
alt: `已上传回收截图 ${index + 1}`,
})
}
key={`uploaded-${index}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageUrl}
alt={`已上传回收截图 ${index + 1}`}
/>
<span>已上传</span>
</button>
);
},
)}
{taskResultPreviews.map(({ file, url }, index) => (
<div
className="task-result-thumb pending-upload"
key={`${file.name}-${file.lastModified}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={url} alt={`待上传回收截图 ${index + 1}`} />
<button
type="button"
aria-label={`移除 ${file.name}`}
onClick={() => removeTaskResultScreenshot(file)}
>
×
</button>
<span>待提交</span>
</div>
))}
{resultScreenshotKeys(selected.result_screenshot_key)
.length +
taskResultScreenshots.length <
MAX_TASK_RESULT_SCREENSHOTS && (
<div className="task-result-add">
<input
type="file"
accept="image/*"
multiple
onChange={chooseTaskResultScreenshots}
/>
<b>+</b>
<strong>
{taskResultScreenshots.length ||
selected.result_screenshot_key
? "继续添加"
: "选择截图"}
</strong>
</div>
)}
</div>
<small className="task-result-hint">
支持一次选择或继续追加,最多9张,每张不超过8MB;点击图片可在当前页面查看大图。
</small>
</div>
<button
type="button"
className="creator-submit"
disabled={working}
onClick={() => void submitRecoveryScreenshots()}
>
{working
? "正在提交…"
: selected.result_submitted_at
? "更新回收截图"
: "确认提交回收截图"}
</button>
{selected.result_submitted_at && (
<p className="submitted-at">
上次提交:{formatDate(selected.result_submitted_at, true)}
</p>
)}
</>
) : (
<p className="creator-recovery-tip">
回填发布链接后,这里会开放回收截图上传。
</p>
)}
</section>
)}
</form>
</div>
<ImageLightbox image={previewImage} onClose={() => setPreviewImage(null)} />
@@ -1917,16 +1941,18 @@ export default function Home() {
}
if (activeBatch) {
const isScreenshotTask = payload.task.type === "screenshot_collect";
const completed = activeBatch.assignments.filter((item) =>
isScreenshotTask ? item.result_submitted_at : item.publish_url,
const recoveryEnabled = payload.task.recoveryEnabled === true;
const completed = activeBatch.assignments.filter(
(item) => item.publish_url,
).length;
const recoverySubmitted = recoveryEnabled
? activeBatch.assignments.filter((item) => item.result_submitted_at).length
: 0;
const isClaimOwner = Boolean(payload.claim);
const shareableCount = payload.claim?.assignments.filter(
(item) =>
!(isScreenshotTask ? item.result_submitted_at : item.publish_url) &&
!item.delegation_bundle_id,
).length ?? 0;
const shareableCount =
payload.claim?.assignments.filter(
(item) => !item.publish_url && !item.delegation_bundle_id,
).length ?? 0;
return (
<main className="portal-shell">
<header className="portal-topbar">
@@ -1943,23 +1969,29 @@ export default function Home() {
<section className="claimed-hero">
<div>
<p className="micro">
{isClaimOwner
? "我的领取任务"
: isScreenshotTask
? "合作社转派截图任务包"
: "合作社转派发布包"}
{isClaimOwner ? "我的领取任务" : "合作社转派发布包"}
</p>
<h1>{payload.task.name}</h1>
<p className="platform-meta-line">
<span>{payload.task.brand}</span>
<PlatformBadge platform={payload.task.platform} compact />
<span>{formatDate(payload.task.dueAt)}前{isScreenshotTask ? "提交" : "发布"}</span>
<span>{formatDate(payload.task.dueAt)}前发布</span>
{recoveryEnabled && payload.task.recoveryDueAt
? ` · 截图回收 ${formatDate(payload.task.recoveryDueAt)}截止`
: ""}
{payload.delegation ? ` · ${payload.delegation.label}` : ""}
</p>
</div>
<div className="completion-ring">
<strong>{completed}/{activeBatch.assignments.length}</strong>
<span>已回填</span>
<div className="hero-progress">
<div className="completion-ring">
<strong>{completed}/{activeBatch.assignments.length}</strong>
<span>已回填</span>
</div>
{recoveryEnabled && (
<small className="recovery-progress">
截图已收 {recoverySubmitted}/{activeBatch.assignments.length}
</small>
)}
</div>
</section>
{isClaimOwner && payload.claim?.joinQrCode ? (
@@ -1977,47 +2009,39 @@ export default function Home() {
<div className="section-heading">
<div>
<h2>
{isClaimOwner
? isScreenshotTask ? "我领取的截图任务" : "我领取的笔记"
: isScreenshotTask ? "需要完成的截图任务" : "需要发布的笔记"}
{isClaimOwner ? "我领取的笔记" : "需要发布的笔记"}
</h2>
<p>
{isClaimOwner
? isScreenshotTask
? "打开一份查看关键词和要求,完成后单独上传截图;也可以转派给底层KOC"
: "可逐篇回填,也可导出Excel填写后批量上传;还可以选择笔记转派给底层KOC"
: isScreenshotTask
? "打开任务查看搜索关键词和要求,完成后逐份上传截图"
: "打开一篇查看完整内容,发布后逐篇回填"}
? "可逐篇回填,也可导出Excel填写后批量上传;还可以选择笔记转派给底层KOC"
: "打开一篇查看完整内容,发布后逐篇回填"}
</p>
</div>
<div className="section-actions">
<span>{activeBatch.assignments.length} {isScreenshotTask ? "份" : "篇"}</span>
{!isScreenshotTask && (
<>
<button
type="button"
disabled={batchWorkbookWorking}
onClick={() => void exportBatchWorkbook()}
>
{batchWorkbookWorking ? "处理中…" : "导出Excel"}
</button>
<button
type="button"
disabled={batchWorkbookWorking}
onClick={() => batchWorkbookInputRef.current?.click()}
>
上传回填表
</button>
<input
ref={batchWorkbookInputRef}
className="batch-workbook-input"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
onChange={(event) => void importBatchWorkbook(event)}
/>
</>
)}
<span>{activeBatch.assignments.length} 篇</span>
<>
<button
type="button"
disabled={batchWorkbookWorking}
onClick={() => void exportBatchWorkbook()}
>
{batchWorkbookWorking ? "处理中…" : "导出Excel"}
</button>
<button
type="button"
disabled={batchWorkbookWorking}
onClick={() => batchWorkbookInputRef.current?.click()}
>
上传回填表
</button>
<input
ref={batchWorkbookInputRef}
className="batch-workbook-input"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
onChange={(event) => void importBatchWorkbook(event)}
/>
</>
{isClaimOwner && (
<button
type="button"
@@ -2029,7 +2053,7 @@ export default function Home() {
setShareLabel("");
}}
>
{shareComposerOpen ? "取消选择" : isScreenshotTask ? "选择任务并分享" : "选择笔记并分享"}
{shareComposerOpen ? "取消选择" : "选择笔记并分享"}
</button>
)}
</div>
@@ -2040,9 +2064,7 @@ export default function Home() {
<p className="micro">合作社转派</p>
<h3>生成免登录分享链接</h3>
<p>
{isScreenshotTask
? "勾选待提交任务,底层KOC打开链接即可查看要求并上传截图。"
: "勾选待发布笔记,底层KOC打开链接即可查看内容并逐篇回填。"}
勾选待发布笔记,底层KOC打开链接即可查看内容并逐篇回填。
</p>
</div>
<label>
@@ -2056,7 +2078,7 @@ export default function Home() {
/>
</label>
<div className="share-composer-action">
<strong>已选 {selectedForShare.length} {isScreenshotTask ? "份" : "篇"}</strong>
<strong>已选 {selectedForShare.length} 篇</strong>
<button
className="share-primary"
disabled={
@@ -2076,8 +2098,7 @@ export default function Home() {
<div className="note-list">
{activeBatch.assignments.map((item, index) => {
const shareDisabled = Boolean(
(isScreenshotTask ? item.result_submitted_at : item.publish_url) ||
item.delegation_bundle_id,
item.publish_url || item.delegation_bundle_id,
);
return (
<article
@@ -2088,8 +2109,8 @@ export default function Home() {
<label
className={`share-checkbox ${shareDisabled ? "disabled" : ""}`}
title={
(isScreenshotTask ? item.result_submitted_at : item.publish_url)
? isScreenshotTask ? "已提交任务不能转派" : "已发布笔记不能转派"
item.publish_url
? "已发布笔记不能转派"
: item.delegation_bundle_id
? `已转派给 ${item.delegation_label}`
: "选择这篇笔记"
@@ -2105,7 +2126,7 @@ export default function Home() {
<span>✓</span>
</label>
)}
<span className={`note-index ${(isScreenshotTask ? item.result_submitted_at : item.publish_url) ? "done" : ""}`}>
<span className={`note-index ${item.publish_url ? "done" : ""}`}>
{String(index + 1).padStart(2, "0")}
</span>
{item.images[0] ? (
@@ -2129,36 +2150,35 @@ export default function Home() {
<div>
<h3>{item.title}</h3>
<p>
{isScreenshotTask
? `搜索关键词 · ${statusLabel(item, payload.task.type)}`
: <>
<span>{item.videos.length > 0 ? `${item.videos.length} 个视频` : `${item.images.length} 张配图`}</span>
<PlatformBadge platform={payload.task.platform} compact />
<span>飞书源行 {item.source_row ?? "—"} · {statusLabel(item, payload.task.type)}</span>
</>}
{!isScreenshotTask && item.creator_screenshot_key ? " · D7截图已交" : ""}
<span>{item.videos.length > 0 ? `${item.videos.length} 个视频` : `${item.images.length} 张配图`}</span>
<PlatformBadge platform={payload.task.platform} compact />
<span>飞书源行 {item.source_row ?? "—"} · {statusLabel(item)}</span>
{recoveryEnabled && item.publish_url
? item.result_submitted_at
? " · 截图已交"
: " · 截图待交"
: ""}
{item.creator_screenshot_key ? " · D7截图已交" : ""}
{item.delegation_label ? ` · 已转派给 ${item.delegation_label}` : ""}
</p>
</div>
<span
className={`note-status ${
(isScreenshotTask ? item.result_submitted_at : item.publish_url)
item.publish_url
? "done"
: item.delegation_bundle_id
? "delegated"
: ""
}`}
>
{(isScreenshotTask ? item.result_submitted_at : item.publish_url)
? isScreenshotTask ? "已提交" : "已回填"
{item.publish_url
? "已回填"
: item.delegation_bundle_id
? "已转派"
: isScreenshotTask ? "待提交" : "待发布"}
: "待发布"}
</span>
<button onClick={() => openNote(item)}>
{(isScreenshotTask ? item.result_submitted_at : item.publish_url)
? "查看 / 修改"
: isScreenshotTask ? "查看并上传" : "查看并回填"} →
{item.publish_url ? "查看 / 修改" : "查看并回填"} →
</button>
</article>
);
@@ -2169,7 +2189,7 @@ export default function Home() {
<div className="delegation-heading">
<div>
<p className="micro">分享记录</p>
<h3>{isScreenshotTask ? "转派截图任务包" : "转派发布包"}</h3>
<h3>转派发布包</h3>
</div>
<span>{payload.claim.delegations.length} 个</span>
</div>
@@ -2182,9 +2202,9 @@ export default function Home() {
<div>
<strong>{bundle.label}</strong>
<span>
{bundle.quantity} {isScreenshotTask ? "份" : "篇"} · {isScreenshotTask ? "已提交" : "已发布"}{" "}
{Number(isScreenshotTask ? bundle.result_completed_count ?? bundle.completed_count : bundle.completed_count ?? 0)}/{bundle.quantity}
{!isScreenshotTask && ` · D7完成 ${Number(bundle.creator_completed_count ?? 0)}`}
{bundle.quantity} 篇 · 已发布{" "}
{Number(bundle.completed_count ?? 0)}/{bundle.quantity}
{` · D7完成 ${Number(bundle.creator_completed_count ?? 0)}`}
</span>
</div>
<small>{formatDate(bundle.created_at, true)}</small>
@@ -2204,7 +2224,7 @@ export default function Home() {
>
复制链接
</button>
{Number(isScreenshotTask ? bundle.result_completed_count ?? bundle.completed_count : bundle.completed_count ?? 0) === 0 && (
{Number(bundle.completed_count ?? 0) === 0 && (
<button
type="button"
className="danger"
@@ -2225,9 +2245,7 @@ export default function Home() {
<div className="safe-tip">
{isClaimOwner
? "以后再次打开原任务链接,填写相同微信号或手机号即可找回全部领取批次和分享记录。"
: isScreenshotTask
? "请保存当前分享链接;完成任务后通过此链接上传截图即可。"
: "请保存当前分享链接;发布后仍需通过此链接回填第7天截图、曝光量和阅读量。"}
: "请保存当前分享链接;发布后仍需通过此链接回填第7天截图、曝光量和阅读量。"}
</div>
{toast && <div className={toastClassName(toast)}>{toast}</div>}
</main>
@@ -2248,13 +2266,15 @@ export default function Home() {
</div>
<h1>{payload.task.name}</h1>
<p>
{payload.task.type === "screenshot_collect"
? "领取后查看搜索关键词和任务要求,完成后逐份上传截图。"
: "领取后可查看完整笔记正文,每篇笔记独立回填发布信息。"}
领取后可查看完整笔记正文,每篇笔记独立回填发布信息。
{payload.task.recoveryEnabled ? "发布后还需提交回收截图。" : ""}
</p>
<div className="hero-meta">
<span>截止 {formatDate(payload.task.dueAt)}</span>
<span>剩余 {payload.task.availableQuantity} {payload.task.type === "screenshot_collect" ? "份" : "篇"}</span>
{payload.task.recoveryEnabled && payload.task.recoveryDueAt ? (
<span>截图回收截止 {formatDate(payload.task.recoveryDueAt)}</span>
) : null}
<span>剩余 {payload.task.availableQuantity} 篇</span>
</div>
</div>
<aside className="claim-card">
@@ -2267,7 +2287,7 @@ export default function Home() {
setRecoveredClaims([]);
}}
>
{payload.task.type === "screenshot_collect" ? "领取新任务" : "领取新笔记"}
领取新笔记
</button>
<button
type="button"
@@ -2293,7 +2313,7 @@ export default function Home() {
<form onSubmit={claimNotes}>
<div>
<p className="micro">01 · 确认领取</p>
<h2>你要领取多少{payload.task.type === "screenshot_collect" ? "份" : "篇"}?</h2>
<h2>你要领取多少篇?</h2>
</div>
{(!wecomIdentity || manualEntry) && (
<label>
@@ -2312,7 +2332,7 @@ export default function Home() {
)}
<div className="quantity-control">
<button type="button" onClick={() => setQuantity(Math.max(1, quantity - 1))}>−</button>
<div><strong>{quantity}</strong><span>{payload.task.type === "screenshot_collect" ? "份" : "篇"}</span></div>
<div><strong>{quantity}</strong><span>篇</span></div>
<button
type="button"
onClick={() =>
@@ -2330,15 +2350,15 @@ export default function Home() {
? "当前任务已领完"
: working
? "领取中…"
: `确认领取 ${quantity} ${payload.task.type === "screenshot_collect" ? "份" : "篇"}`}
: `确认领取 ${quantity} 篇`}
</button>
<small>领取后系统会生成你的专属{payload.task.type === "screenshot_collect" ? "任务清单" : "笔记清单"}</small>
<small>领取后系统会生成你的专属笔记清单</small>
</form>
) : (
<form onSubmit={recoverClaims}>
<div>
<p className="micro">找回历史记录</p>
<h2>继续之前领取的{payload.task.type === "screenshot_collect" ? "任务" : "笔记"}</h2>
<h2>继续之前领取的笔记</h2>
<p className="recover-intro">
{wecomIdentity && !manualEntry
? "已用企微身份查找,同一任务多次领取会分批展示。"
@@ -2386,9 +2406,9 @@ export default function Home() {
<section className="steps">
<div><span>1</span><strong>选择数量</strong><p>一次可领多篇</p></div>
<i />
<div><span>2</span><strong>{payload.task.type === "screenshot_collect" ? "逐份查看" : "逐篇查看"}</strong><p>{payload.task.type === "screenshot_collect" ? "关键词与要求完整展示" : "标题正文完整展示"}</p></div>
<div><span>2</span><strong>逐篇查看</strong><p>标题正文完整展示</p></div>
<i />
<div><span>3</span><strong>{payload.task.type === "screenshot_collect" ? "上传截图" : "单篇回填"}</strong><p>{payload.task.type === "screenshot_collect" ? "每份任务与截图一一对应" : "昵称、链接、截图一一对应"}</p></div>
<div><span>3</span><strong>单篇回填</strong><p>昵称、链接、截图一一对应</p></div>
</section>
<footer>由 KOC LOOP 提供任务与数据安全保障</footer>
{toast && <div className={toastClassName(toast)}>{toast}</div>}