R59b: 视频关键信息卡定稿白底(用户改稿:bgGrad 纯白 + accent 双系 tint 卡, F6禁止贴靠已落实, cdp-verify-r59.mjs 回归脚本)
This commit is contained in:
1 parent
950b920c9b
commit
07fbcd489a
4 files changed
+105
-11
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user