R59b: 视频关键信息卡定稿白底(用户改稿:bgGrad 纯白 + accent 双系 tint 卡, F6禁止贴靠已落实, cdp-verify-r59.mjs 回归脚本)

This commit is contained in:
maogeigei committed 2026-09-04 15:25:22 +08:00
1 parent 950b920c9b
commit 07fbcd489a
4 files changed
+105 -11

No files matched your search

+13
View File
@@ -68,3 +68,16 @@
- **实测(9223,fake 全清)**:①多版本(video 190 4 版)删第 4/4 → 弹窗重建 3/3 内容切 rw25 ✅;②单版本视频脚本 isLast → router 刷新 ✅;③自定义选题 id38(video_id 空)查看弹窗含删除按钮 → 确认删除 → 弹窗关+行消失,DB 复查 rewrite/review/storyboard 三级全 0 ✅
- **版本号**:data-pages.js bump ?v=20260904e→f(静态 js 实时读盘未重启)
- **遗留**:R55-R58 改动未 commit(待 git 提交);帮助文档一行两卡 + 模拟操作指引点击无效果仍待修
## R59 视频关键信息卡迭代收尾(15:30,全路径实测通过)
- **用户诉求递进**(多轮反馈):①"在视频详情页拆解tab右侧加视频关键信息卡片"(已有实现)→ ②"加上可复用爆款内容" → ③"现在这个太难看了,参考账号设定卡的卡片" → ④"每一个卡片的内容都顶着卡片上边" → ⑤"内容结构板块也要加进去" → ⑥"视频解析的卡片用白色背景就行"(白底定稿,去除账号设定卡式渐变大底)
- **三处代码改动(public/data-pages.js)**:
1. **vkCollect 内容结构板块全量化**:拆解表「内容结构」(段落板块|维度|内容)作为独立 `kind:'struct'` 整卡(板块≤7、行全保留、值折行≤3+省略号);无内容结构表时回退板块式框架表 → `kind:'blocks'` 速览;`mods` 装配顺序:内容定位→可复用爆款内容→**内容结构**→风格与传播→台词金句
2. **renderVideoKeySvg 布局重写(统一"文字顶游标"排版)**:`vPadLR=16, vPadT=15, vPadB=14, insetTop=size*0.9`;kv 卡 label 22 行盒/正文 17、struct 卡板块头 23(左侧 boxTag 竖条 4×14 rx2 + 文字 x+13)、blocks 卡 padT=13 padLR=12;卡内首行 gap=vPadT+insetTop≈26(之前 ≈12 贴顶)—— F6 规范"禁止贴靠"统一落实
3. **色板白底定稿**:`bgA=bgB=#FFFFFF`(两 stop 纯白);deep 暖米 accent(#FBF3E7 box / #C13B2F boxTag / #7B2D1B boxTitle / 渐变标题 #8C2F1E→#C05A32);light 粉白 accent(#FDF1F5 box / #D84A8A boxTag / #A13D6B boxTitle / 渐变 #B03E76→#E0579A);模块标题色 secTitle=accent 深色(白底上不再用浅色字);header 框同色 tint 形成"主卡"质感
- **数据形态主目标确认**:13 维 MCP 拆解(赛道/账号核心定位/内容核心/可复用的爆款内容体系/…/内容结构/框架/事件/梗元素)—— v93/v190 实测标准形态;v205 类 F4 形态(无 tag 行无内容结构)不在本卡范围(keyCardHtml='' → 自动 fallback 拆解全文 md)
- **版本号**:data-pages.js bump `?v=20260904i→j`,index.html 同步
- **回归脚本 cdp-verify-r59.mjs**(根目录):9223 真实渲染 → 审计模块齐全/卡内首行 gap≥20/不越出 viewBox/bgGrad stops #FFFFFF → v93(vbH 1719 4 模块 4 板块 5 卡 gap 26 全绿)+ v190(vbH 1766 同构)→ `=== PASS ===`
- **产物**:`tmp/r59_v93_card.svg` + `tmp/r59_v190_card.svg`(白底定稿版完整 SVG 源);旧 `tmp/v190_keycard.svg`/`v93_keycard.svg` 渐变版已清
- **环境复用**:9223 专用 Chrome profile `C:/Users/maidou/.workbuddy/chrome-9223-profile`(已 9-02 授权过);会话间被回收时需 `run_in_background` 重启 chrome 进程(bash `&` 会随沙箱会话结束被杀)
- **R55-R58 同步清理**:cdp-verify-r59.mjs 与既有 cdp-verify-r28 等同级保留为回归;commit/push 待办
@@ -0,0 +1,80 @@
// R59 关键信息卡(白底版)真实渲染验证:拆解 tab 默认卡视图
// 断言:① 卡片 SVG 渲染且含「内容结构」整卡(板块小节齐全)② 卡内首行文字不贴顶(gap≥20)
// ③ 文本不越出 viewBox ④ 深浅两系均出卡;并截图存档
const CDP = 9223;
const list = await fetch(`http://localhost:${CDP}/json`).then((r) => r.json());
const page = list.find((t) => t.type === 'page' && (t.url || '').includes('8900')) || list.find((t) => t.type === 'page');
if (!page) { console.log('ERR: no page'); process.exit(1); }
const ws = new WebSocket(page.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
await new Promise((r) => (ws.onopen = r));
const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await send('Page.enable'); await send('Runtime.enable');
await send('Emulation.setDeviceMetricsOverride', { width: 860, height: 1000, deviceScaleFactor: 1, mobile: false });
const waitFor = async (expr, ms = 15000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; };
const fail = (m) => { console.log('FAIL@' + m); process.exit(1); };
const fs = await import('node:fs');
const AUDIT = `(() => {
const svg = document.querySelector('.vk-view svg');
if (!svg) return { err: 'no svg in .vk-view' };
const vbH = svg.viewBox.baseVal.height;
const rects = [...svg.querySelectorAll('rect')].map((r) => ({ x: Math.round(r.x.baseVal.value * 10) / 10, y: Math.round(r.y.baseVal.value * 10) / 10, w: Math.round(r.width.baseVal.value), h: Math.round(r.height.baseVal.value), fill: r.getAttribute('fill') }));
const texts = [...svg.querySelectorAll('text')].map((t) => ({ x: +t.getAttribute('x'), y: +t.getAttribute('y'), size: +t.getAttribute('font-size'), wt: t.getAttribute('font-weight'), txt: (t.textContent || '').slice(0, 42) }));
const titles = texts.filter((t) => t.txt.startsWith('▍')).map((t) => t.txt.slice(1));
const cards = rects.filter((r) => (r.fill || '').startsWith('#F') && r.w > 500 && r.h > 40); /* 整宽 tint 内容卡 */
const audits = cards.map((r) => {
const inT = texts.filter((t) => t.y > r.y + 2 && t.y < r.y + r.h && t.x >= r.x - 1 && t.x <= r.x + r.w + 1).sort((a, b) => a.y - b.y);
const first = inT[0];
return { cardTop: r.y, h: r.h, firstTxt: first ? first.txt : '', gap: first ? Math.round((first.y - r.y) * 10) / 10 : null };
});
const maxTextY = Math.max(...texts.map((t) => t.y));
/* 内容结构整卡内应出现的板块名(struct 板块头 text 加粗、位于该卡内且不重复) */
const structCard = cards[1]; /* 第二张卡(内容定位卡之后)可能为内容结构;改用标题定位 */
const titleYs = texts.filter((t) => t.txt.startsWith('▍')).map((t) => ({ t: t.txt.slice(1), y: t.y }));
const cardByTitle = {};
cards.forEach((r) => { const t = titleYs.find((x) => x.y < r.y && r.y - x.y < 60); if (t) cardByTitle[t.t] = r; });
const secCard = cardByTitle['内容结构'];
const secBoard = secCard ? texts.filter((t) => t.y > secCard.y && t.y < secCard.y + secCard.h && (t.wt || '') === '700' && t.x > secCard.x && t.x < secCard.x + 120).map((t) => t.txt) : [];
const bgStops = [...svg.querySelectorAll('#bgGrad stop')].map((s) => s.getAttribute('stop-color'));
return { vbH, titles, nCards: cards.length, audits, cardByTitle: Object.keys(cardByTitle), secBoard, maxTextY, over: maxTextY > vbH - 4, bgStops };
})()`;
const VIDS = [93, 190];
const out = {};
for (const id of VIDS) {
await send('Page.navigate', { url: `http://127.0.0.1:8900/#/video/${id}` });
/* 等路由渲染完成:hash 正确 + 视频信息卡渲染(旧 DOM 残留会误判,需等新内容) */
if (!(await waitFor(`location.hash === '#/video/${id}' && !!document.querySelector('#view .video-grid')`, 15000))) fail(`v${id} route`);
await sleep(400);
await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`);
const hasCard = await waitFor(`!!document.querySelector('#view .vk-view svg')`, 12000);
if (!hasCard) { out['v' + id] = { __err: 'no keycard svg (纯 md fallback?)', hasMd: await ev(`!!document.querySelector('.vk-view .md-body, .vk-text')`) }; continue; }
await sleep(500);
const audit = await ev(AUDIT);
out['v' + id] = audit;
/* 截图:滚动 svg 到顶部后按元素区域截图 */
await ev(`document.querySelector('.vk-view svg').scrollIntoView({ block: 'start' })`); await sleep(250);
const rect = await ev(`(() => { const r = document.querySelector('.vk-view svg').getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; })()`);
if (rect && !rect.__err && rect.w > 100) {
const shot = await send('Page.captureScreenshot', { format: 'png', clip: { x: Math.max(0, rect.x - 4), y: Math.max(0, rect.y - 4), width: Math.min(rect.w + 8, 1200), height: Math.min(rect.h + 8, 4000), scale: 1 } });
fs.writeFileSync(`tmp/r59_card_v${id}.png`, Buffer.from(shot.data, 'base64'));
out['v' + id].shot = `tmp/r59_card_v${id}.png`;
}
}
console.log(JSON.stringify(out, null, 1));
const v93 = out.v93, v190 = out.v190;
const ok = v93 && !v93.__err && v190 && !v190.__err
&& v93.bgStops && v93.bgStops.length === 2 && v93.bgStops.every((c) => c === '#FFFFFF')
&& v190.bgStops && v190.bgStops.length === 2 && v190.bgStops.every((c) => c === '#FFFFFF')
&& v93.titles.includes('内容结构') && v190.titles.includes('内容结构')
&& v93.secBoard && v93.secBoard.some((b) => b.includes('开场钩子')) && v93.secBoard.some((b) => b.includes('中段叙事'))
&& v93.audits.every((a) => a.gap >= 20) && v190.audits.every((a) => a.gap >= 20)
&& !v93.over && !v190.over;
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
ws.close();
process.exit(ok ? 0 : 1);
@@ -1289,23 +1289,24 @@ function vkCollect(analysis) {
return { track, mods };
}
/* SVG 生成器:analysis=拆解 dict,vt=视频标题(含话题标签原样,卡内自行清理),acc=账号名。
排版模型:全宽 680,头部标题框 520×170(同账号设定卡),其下逐模块排版——模块标题(底色上书写)→ 内容卡(圆角白/米卡)。
排版模型:全宽 680,白色画布(用户定稿:白底简洁版,非账号设定卡式渐变大底),头部标题框 520×170,
其下逐模块排版——模块标题(accent 色书写)→ 浅 tint 圆角内容卡;深/浅调性只作用于 accent 与卡片 tint。
文本折行按「全角单位」计算:中文 1u≈字号宽、ASCII 0.55u;行 y 均为 text 基线,行高按字号 + 视觉间距直接给出(无字符串后处理)。 */
function renderVideoKeySvg(analysis, vt, acc) {
const D = vkCollect(analysis);
if (!D || !D.mods || !D.mods.length) return '';
const deep = vkPalette(analysis) === 'deep';
/* 色板(复用账号设定卡深浅两系规范) */
/* 色板:白底 + tint 卡;deep=暖米棕红系(深沉向)/ light=粉白玫瑰金系(明快向) */
const P = deep ? {
bgA: '#2B0F0A', bgB: '#1A0805', /* 深棕红渐变底(深沉向) */
box: '#FAEEDA', boxBd: '#EDCBA4', boxTitle: '#7B2D1B', boxText: '#6F5B4A', boxTag: '#C13B2F',
headBox: '#FAEEDA', headBd: '#EDCBA4', headTitleA: '#8C2F1E', headTitleB: '#C05A32', headSub: '#8C2F1E', headDim: '#A88770',
secTitle: '#FAEEDA', deco: '#A88770',
bgA: '#FFFFFF', bgB: '#FFFFFF', /* 纯白底(09-04 用户定稿) */
box: '#FBF3E7', boxBd: '#EBD9BC', boxTitle: '#7B2D1B', boxText: '#6A5644', boxTag: '#C13B2F',
headBox: '#FBF3E7', headBd: '#EBD9BC', headTitleA: '#8C2F1E', headTitleB: '#C05A32', headSub: '#C13B2F', headDim: '#A08B76',
secTitle: '#8C2F1E', deco: '#EBD9BC',
} : {
bgA: '#D6A3A9', bgB: '#9A5563', /* 玫瑰金渐变底(明快向,王微斯定稿) */
box: '#FFFFFF', boxBd: '#E8C9DC', boxTitle: '#7B2D5E', boxText: '#6F4A5E', boxTag: '#E0437F',
headBox: '#FFFFFF', headBd: '#E8C9DC', headTitleA: '#C0457F', headTitleB: '#E876A9', headSub: '#7B2D5E', headDim: '#B98AA6',
secTitle: '#FDE8EC', deco: '#EAC6CE',
bgA: '#FFFFFF', bgB: '#FFFFFF',
box: '#FDF1F5', boxBd: '#F0D3E0', boxTitle: '#A13D6B', boxText: '#6E4A5C', boxTag: '#D84A8A',
headBox: '#FDF1F5', headBd: '#F0D3E0', headTitleA: '#B03E76', headTitleB: '#E0579A', headSub: '#D84A8A', headDim: '#B193A5',
secTitle: '#A13D6B', deco: '#F0D3E0',
};
const W = 680, LX = 40, CW = W - LX * 2; /* 内容区:x 40,宽 600 */
const X = (u) => Math.max(4, Math.floor(u)); /* 全角单位 → 字数 */
@@ -43,6 +43,6 @@
<script src="app.js?v=20260904e"></script>
<script src="help-guide.js?v=20260904b"></script>
<script src="data-pages.js?v=20260904i"></script>
<script src="data-pages.js?v=20260904j"></script>
</body>
</html>