fix: 修复两端走查发现的4个UI断点
- 门户回填表单:缺发布截图时提交按钮前置禁用并提示「先上传发布截图,
链接回填需以截图为凭证」,不再等提交后才 toast 报错(服务端守卫保留)
- 门户笔记详情:回填链接前收起回收区块的说明与示例图,只留一行锁定提示,
降低回填阶段信息密度
- 运营端分发表:数据状态按任务采集计划显示等待日(等待D{首个采集日}),
未设置计划时显示「待设置采集计划」,不再写死「等待D2」
- 截图包回收清单:序号列对无 source_row 的内容回退行号;种子内容补
source_row,演示数据导出不再出现空序号
This commit is contained in:
@@ -188,6 +188,8 @@ type Distribution = {
|
|||||||
task_platform: "小红书" | "抖音";
|
task_platform: "小红书" | "抖音";
|
||||||
content_format: "image_text" | "video";
|
content_format: "image_text" | "video";
|
||||||
due_at: string;
|
due_at: string;
|
||||||
|
task_collection_days: string | null;
|
||||||
|
task_collection_start_date: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecoverySortKey =
|
type RecoverySortKey =
|
||||||
@@ -5033,6 +5035,25 @@ function DistributionTable({
|
|||||||
onRelease?: (distribution: Distribution) => void;
|
onRelease?: (distribution: Distribution) => void;
|
||||||
showRecovery?: boolean;
|
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 (
|
return (
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
@@ -5075,7 +5096,11 @@ function DistributionTable({
|
|||||||
) : item.d2_likes !== null ? (
|
) : item.d2_likes !== null ? (
|
||||||
<span className="metric-waiting">采集中</span>
|
<span className="metric-waiting">采集中</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="muted">{item.publish_url ? "等待D2" : "尚未发布"}</span>
|
<span className="muted">
|
||||||
|
{item.publish_url
|
||||||
|
? waitingCollectionLabel(item)
|
||||||
|
: "尚未发布"}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
{showRecovery && (
|
{showRecovery && (
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function GET(request: Request) {
|
|||||||
exported += 1;
|
exported += 1;
|
||||||
}
|
}
|
||||||
manifest.push([
|
manifest.push([
|
||||||
String(row.source_row ?? ""),
|
String(row.source_row ?? rowIndex + 1),
|
||||||
row.title,
|
row.title,
|
||||||
row.claimant_name || row.partner_name,
|
row.claimant_name || row.partner_name,
|
||||||
row.claimed_at,
|
row.claimed_at,
|
||||||
|
|||||||
@@ -1666,9 +1666,15 @@ export default function Home() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="submit-primary" disabled={working}>
|
<button
|
||||||
|
className="submit-primary"
|
||||||
|
disabled={working || !(screenshot || selected.publish_screenshot_key)}
|
||||||
|
>
|
||||||
{working ? "正在提交…" : selected.publish_url ? "更新这篇笔记" : "确认回填这篇笔记"}
|
{working ? "正在提交…" : selected.publish_url ? "更新这篇笔记" : "确认回填这篇笔记"}
|
||||||
</button>
|
</button>
|
||||||
|
{!(screenshot || selected.publish_screenshot_key) && (
|
||||||
|
<p className="submitted-at">先上传发布截图,链接回填需以截图为凭证</p>
|
||||||
|
)}
|
||||||
{selected.publish_time && (
|
{selected.publish_time && (
|
||||||
<p className="submitted-at">上次回填:{formatDate(selected.publish_time, true)}</p>
|
<p className="submitted-at">上次回填:{formatDate(selected.publish_time, true)}</p>
|
||||||
)}
|
)}
|
||||||
@@ -1796,13 +1802,16 @@ export default function Home() {
|
|||||||
{selected.result_submitted_at ? "已提交" : "待提交"}
|
{selected.result_submitted_at ? "已提交" : "待提交"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{selected.publish_url && (
|
||||||
<p className="creator-recovery-tip">
|
<p className="creator-recovery-tip">
|
||||||
{payload.task.recoveryInstructions || "请提交发布后的笔记截图"}
|
{payload.task.recoveryInstructions || "请提交发布后的笔记截图"}
|
||||||
{payload.task.recoveryDueAt
|
{payload.task.recoveryDueAt
|
||||||
? ` 截止:${formatDate(payload.task.recoveryDueAt)}。`
|
? ` 截止:${formatDate(payload.task.recoveryDueAt)}。`
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
{(payload.task.recoveryExampleImages?.length ?? 0) > 0 && (
|
)}
|
||||||
|
{selected.publish_url &&
|
||||||
|
(payload.task.recoveryExampleImages?.length ?? 0) > 0 && (
|
||||||
<div className="task-result-gallery recovery-example">
|
<div className="task-result-gallery recovery-example">
|
||||||
{payload.task.recoveryExampleImages?.map((image) => (
|
{payload.task.recoveryExampleImages?.map((image) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -640,8 +640,8 @@ export async function seedIfEmpty() {
|
|||||||
batch.push(
|
batch.push(
|
||||||
db
|
db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO contents (id, task_id, title, body, status)
|
`INSERT INTO contents (id, task_id, title, body, status, source_row)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
`content-s-${index + 1}`,
|
`content-s-${index + 1}`,
|
||||||
@@ -649,6 +649,7 @@ export async function seedIfEmpty() {
|
|||||||
title,
|
title,
|
||||||
"来自飞书内容表的完整笔记正文与素材说明。",
|
"来自飞书内容表的完整笔记正文与素材说明。",
|
||||||
index < 9 ? "allocated" : "available",
|
index < 9 ? "allocated" : "available",
|
||||||
|
index + 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -656,8 +657,8 @@ export async function seedIfEmpty() {
|
|||||||
batch.push(
|
batch.push(
|
||||||
db
|
db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO contents (id, task_id, title, body, status)
|
`INSERT INTO contents (id, task_id, title, body, status, source_row)
|
||||||
VALUES (?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
)
|
)
|
||||||
.bind(
|
.bind(
|
||||||
`content-l-${index + 1}`,
|
`content-l-${index + 1}`,
|
||||||
@@ -665,6 +666,7 @@ export async function seedIfEmpty() {
|
|||||||
title,
|
title,
|
||||||
"来自飞书内容表的完整笔记正文与素材说明。",
|
"来自飞书内容表的完整笔记正文与素材说明。",
|
||||||
index < 6 ? "allocated" : "available",
|
index < 6 ? "allocated" : "available",
|
||||||
|
index + 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -898,7 +900,9 @@ export async function getDashboardData(ownerUserid?: string) {
|
|||||||
t.task_type AS task_type,
|
t.task_type AS task_type,
|
||||||
t.platform AS task_platform,
|
t.platform AS task_platform,
|
||||||
t.content_format AS content_format,
|
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
|
FROM distributions d
|
||||||
JOIN contents c ON c.id = d.content_id
|
JOIN contents c ON c.id = d.content_id
|
||||||
JOIN partners p ON p.id = d.partner_id
|
JOIN partners p ON p.id = d.partner_id
|
||||||
|
|||||||
67
tests/ui-walkthrough-fixes.test.mjs
Normal file
67
tests/ui-walkthrough-fixes.test.mjs
Normal 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\)/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user