fix: 修复两端走查发现的4个UI断点

- 门户回填表单:缺发布截图时提交按钮前置禁用并提示「先上传发布截图,
  链接回填需以截图为凭证」,不再等提交后才 toast 报错(服务端守卫保留)
- 门户笔记详情:回填链接前收起回收区块的说明与示例图,只留一行锁定提示,
  降低回填阶段信息密度
- 运营端分发表:数据状态按任务采集计划显示等待日(等待D{首个采集日}),
  未设置计划时显示「待设置采集计划」,不再写死「等待D2」
- 截图包回收清单:序号列对无 source_row 的内容回退行号;种子内容补
  source_row,演示数据导出不再出现空序号
This commit is contained in:
ABAPPLO
2026-10-08 14:44:47 +08:00
parent 2db06063ee
commit c54649258b
5 changed files with 120 additions and 15 deletions

View File

@@ -188,6 +188,8 @@ type Distribution = {
task_platform: "小红书" | "抖音";
content_format: "image_text" | "video";
due_at: string;
task_collection_days: string | null;
task_collection_start_date: string | null;
};
type RecoverySortKey =
@@ -5033,6 +5035,25 @@ function DistributionTable({
onRelease?: (distribution: Distribution) => void;
showRecovery?: boolean;
}) {
// 已发布但还没采到数据的行:有计划时按首个采集日显示,没计划时
// 明说待设置,而不是写死「等待D2」误导运营。
const waitingCollectionLabel = (item: Distribution) => {
let days: number[] = [];
try {
const parsed = JSON.parse(item.task_collection_days || "[]") as unknown;
if (Array.isArray(parsed)) {
days = parsed
.map(Number)
.filter((day) => Number.isInteger(day) && day >= 1 && day <= 7);
}
} catch {
days = [];
}
if (!item.task_collection_start_date || days.length === 0) {
return "待设置采集计划";
}
return `等待D${Math.min(...days)}`;
};
return (
<div className="table-scroll">
<table className="data-table">
@@ -5075,7 +5096,11 @@ function DistributionTable({
) : item.d2_likes !== null ? (
<span className="metric-waiting">采集中</span>
) : (
<span className="muted">{item.publish_url ? "等待D2" : "尚未发布"}</span>
<span className="muted">
{item.publish_url
? waitingCollectionLabel(item)
: "尚未发布"}
</span>
)}
</td>
{showRecovery && (

View File

@@ -78,7 +78,7 @@ export async function GET(request: Request) {
exported += 1;
}
manifest.push([
String(row.source_row ?? ""),
String(row.source_row ?? rowIndex + 1),
row.title,
row.claimant_name || row.partner_name,
row.claimed_at,

View File

@@ -1666,9 +1666,15 @@ export default function Home() {
</label>
</div>
</div>
<button className="submit-primary" disabled={working}>
<button
className="submit-primary"
disabled={working || !(screenshot || selected.publish_screenshot_key)}
>
{working ? "正在提交…" : selected.publish_url ? "更新这篇笔记" : "确认回填这篇笔记"}
</button>
{!(screenshot || selected.publish_screenshot_key) && (
<p className="submitted-at">先上传发布截图,链接回填需以截图为凭证</p>
)}
{selected.publish_time && (
<p className="submitted-at">上次回填:{formatDate(selected.publish_time, true)}</p>
)}
@@ -1796,13 +1802,16 @@ export default function Home() {
{selected.result_submitted_at ? "已提交" : "待提交"}
</span>
</div>
{selected.publish_url && (
<p className="creator-recovery-tip">
{payload.task.recoveryInstructions || "请提交发布后的笔记截图"}
{payload.task.recoveryDueAt
? ` 截止:${formatDate(payload.task.recoveryDueAt)}。`
: ""}
</p>
{(payload.task.recoveryExampleImages?.length ?? 0) > 0 && (
)}
{selected.publish_url &&
(payload.task.recoveryExampleImages?.length ?? 0) > 0 && (
<div className="task-result-gallery recovery-example">
{payload.task.recoveryExampleImages?.map((image) => (
<button

View File

@@ -640,8 +640,8 @@ export async function seedIfEmpty() {
batch.push(
db
.prepare(
`INSERT INTO contents (id, task_id, title, body, status)
VALUES (?, ?, ?, ?, ?)`,
`INSERT INTO contents (id, task_id, title, body, status, source_row)
VALUES (?, ?, ?, ?, ?, ?)`,
)
.bind(
`content-s-${index + 1}`,
@@ -649,6 +649,7 @@ export async function seedIfEmpty() {
title,
"来自飞书内容表的完整笔记正文与素材说明。",
index < 9 ? "allocated" : "available",
index + 1,
),
);
});
@@ -656,8 +657,8 @@ export async function seedIfEmpty() {
batch.push(
db
.prepare(
`INSERT INTO contents (id, task_id, title, body, status)
VALUES (?, ?, ?, ?, ?)`,
`INSERT INTO contents (id, task_id, title, body, status, source_row)
VALUES (?, ?, ?, ?, ?, ?)`,
)
.bind(
`content-l-${index + 1}`,
@@ -665,6 +666,7 @@ export async function seedIfEmpty() {
title,
"来自飞书内容表的完整笔记正文与素材说明。",
index < 6 ? "allocated" : "available",
index + 1,
),
);
});
@@ -898,7 +900,9 @@ export async function getDashboardData(ownerUserid?: string) {
t.task_type AS task_type,
t.platform AS task_platform,
t.content_format AS content_format,
t.due_at AS due_at
t.due_at AS due_at,
t.collection_days AS task_collection_days,
t.collection_start_date AS task_collection_start_date
FROM distributions d
JOIN contents c ON c.id = d.content_id
JOIN partners p ON p.id = d.partner_id

View File

@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
// 2026-10-08 两端走查发现的 UI 断点修复:
// 1) 回填提交在缺发布截图时前置禁用并说明原因(原本只在提交后 toast 报错)
// 2) 未回填链接时收起回收区块的说明/示例图,只留锁定提示
// 3) 分发表「数据状态」按任务采集计划显示等待日,没计划时明说待设置
// 4) 截图包回收清单的序号列对无 source_row 的内容回退为行号;种子内容补 source_row
test("portal gates the backfill submit button on the publish screenshot", async () => {
const source = await readFile(
new URL("../koc-portal/app/page.tsx", import.meta.url),
"utf8",
);
assert.match(
source,
/disabled=\{working \|\| !\(screenshot \|\| selected\.publish_screenshot_key\)\}/,
);
assert.match(source, /先上传发布截图,链接回填需以截图为凭证/);
// 服务端/提交守卫仍保留,双保险
assert.match(source, /请上传这篇笔记的发布截图/);
});
test("portal collapses recovery instructions until the link is backfilled", async () => {
const source = await readFile(
new URL("../koc-portal/app/page.tsx", import.meta.url),
"utf8",
);
const lockedHint = "回填发布链接后,这里会开放回收截图上传。";
assert.ok(source.includes(lockedHint), "锁定提示必须保留");
const tipIndex = source.indexOf(
`{selected.publish_url && (\n <p className="creator-recovery-tip">`,
);
assert.ok(tipIndex > 0, "回收说明必须由 publish_url 门控");
// 示例图区块同样门控
assert.match(
source,
/\{selected\.publish_url &&\n \(payload\.task\.recoveryExampleImages\?\.length \?\? 0\) > 0 && \(/,
);
});
test("admin waiting label follows the task collection plan", async () => {
const [adminApp, mvpDb] = await Promise.all([
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
readFile(new URL("../lib/mvp-db.ts", import.meta.url), "utf8"),
]);
assert.match(adminApp, /waitingCollectionLabel/);
assert.match(adminApp, /待设置采集计划/);
assert.match(adminApp, /等待D\$\{Math\.min\(\.\.\.days\)\}/);
assert.doesNotMatch(adminApp, /"等待D2"/);
// 分发查询带上任务级采集计划字段
assert.match(mvpDb, /t\.collection_days AS task_collection_days/);
assert.match(mvpDb, /t\.collection_start_date AS task_collection_start_date/);
});
test("export manifest and seed contents always carry a sequence number", async () => {
const [exportRoute, seed] = await Promise.all([
readFile(
new URL("../app/api/screenshot-task-export/route.ts", import.meta.url),
"utf8",
),
readFile(new URL("../lib/mvp-db.ts", import.meta.url), "utf8"),
]);
assert.match(exportRoute, /String\(row\.source_row \?\? rowIndex \+ 1\)/);
assert.match(seed, /INSERT INTO contents \(id, task_id, title, body, status, source_row\)/);
});