fix: 修复两端走查发现的4个UI断点
- 门户回填表单:缺发布截图时提交按钮前置禁用并提示「先上传发布截图,
链接回填需以截图为凭证」,不再等提交后才 toast 报错(服务端守卫保留)
- 门户笔记详情:回填链接前收起回收区块的说明与示例图,只留一行锁定提示,
降低回填阶段信息密度
- 运营端分发表:数据状态按任务采集计划显示等待日(等待D{首个采集日}),
未设置计划时显示「待设置采集计划」,不再写死「等待D2」
- 截图包回收清单:序号列对无 source_row 的内容回退行号;种子内容补
source_row,演示数据导出不再出现空序号
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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