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:
@@ -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);
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user