diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
index 8e05f35..43b7d5e 100644
--- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js
@@ -1187,9 +1187,251 @@ async function renderAccountDetail(view, id) {
}
/* ================= 视频详情 ================= */
+
+/* ===== R59 视频关键信息卡(拆解 tab 默认视图)= 前端 SVG 生成器,复刻账号设定卡(F6)视觉语言:
+ 竖版长图信息图(viewBox 0 0 680 自适应高度),渐变底 + 顶部标题框 + 圆角内容卡,色系按拆解调性自动选深/浅;
+ 数据从 MCP 解析 analysis.json(13 维 dict)自动抽取(无 AI),维度/标签以数据实存为准(有则展示、缺失即跳过)===== */
+/* 提取 markdown 文本中的 `**标签**: 值` 行 → [['标签','值']...](支持中文/英文冒号;值仅取当行) */
+function mdTagLines(text) {
+ const out = [];
+ String(text || '').split('\n').forEach((ln) => {
+ const m = /^\s*\*\*([^*]+)\*\*\s*[::]\s*(.+)$/.exec(ln);
+ if (m && m[2].trim()) out.push([m[1].trim(), m[2].trim()]);
+ });
+ return out;
+}
+/* 解析 markdown 管道表格 → { head:[], rows:[[...]] }(跳过分隔行,自动去首尾空单元格) */
+function mdTableRows(text) {
+ const head = []; const rows = [];
+ String(text || '').split('\n').forEach((ln) => {
+ const t = ln.trim();
+ if (!t.startsWith('|')) return;
+ const cells = t.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
+ if (cells.every((c) => /^:?-{2,}:?$/.test(c))) return; /* 分隔行 |---| */
+ if (!head.length) head.push(...cells); else rows.push(cells);
+ });
+ return { head, rows };
+}
+/* —— SVG 布局工具(中文按 1 全角宽、ASCII 按 0.55 估算折行/截断) —— */
+const _u = (ch) => (ch.charCodeAt(0) > 255 ? 1 : 0.55);
+const _uLen = (s) => { let w = 0; for (const c of String(s)) w += _u(c); return w; };
+function svgWrap(text, maxU) { /* 按全角单位宽折行 → 行数组(不截断) */
+ const lines = []; let cur = ''; let w = 0;
+ for (const ch of String(text || '')) {
+ const cw = _u(ch);
+ if (w + cw > maxU && cur) { lines.push(cur); cur = ''; w = 0; }
+ cur += ch; w += cw;
+ }
+ if (cur) lines.push(cur);
+ return lines;
+}
+function svgCut(text, maxU) { /* 超宽截断加省略号 */
+ const s = String(text || '');
+ return _uLen(s) <= maxU ? s : (svgWrap(s, maxU - 1)[0] + '…');
+}
+/* —— 配色:按拆解文本情绪调性选 深色系/浅色系(复用 F6 配色规范;赛道标签歧义时以情绪锚点为准) —— */
+function vkPalette(analysis) {
+ const blob = ['赛道', '账号核心定位', '内容核心', '内容风格', '可复用的爆款内容体系'].map((k) => String((analysis || {})[k] || '')).join(' ');
+ const deep = /深沉|催泪|怀旧|忧郁|厚重|悲情|救赎|泪目|旧时光|岁月|思念|遗憾|守护/.test(blob);
+ const light = /明快|欢乐|治愈|解压|暖心|温馨|可爱|元气|清新|甜蜜|宠溺|搞笑|积极/.test(blob);
+ /* 深色词命中 → 深色系(如催泪向治愈);否则浅色系(默认) */
+ return deep && !light ? 'deep' : 'light';
+}
+/* 抽取拆解为「关键信息模块数据」→ [{ title, kind:'kv', rows:[[k,v]...] } | { title, kind:'blocks', blocks:[[b, [[k,v]...]]] }](无内容模块不返回) */
+function vkCollect(analysis) {
+ if (!analysis || typeof analysis !== 'object') return [];
+ const A = analysis;
+ const first = (k) => { const v = A[k]; return (v == null || v === '') ? '' : String(v).trim(); };
+ const tagItems = (k, skipTag) => mdTagLines(A[k]).filter(([t, v]) => v && !(skipTag && skipTag === t));
+ let track = first('赛道');
+ if (!track) { const tl = mdTagLines(A['账号核心定位']).find(([t]) => t === '赛道'); if (tl) track = tl[1]; }
+ const g1 = [...tagItems('账号核心定位', track ? '赛道' : ''), ...tagItems('内容核心', '')].slice(0, 8); /* ① 内容定位 */
+ const g2 = tagItems('可复用的爆款内容体系', '').slice(0, 6); /* ② 可复用爆款内容 */
+ /* ③ 内容结构:完整段落板块表(段落板块|维度|内容)→ 独立整卡板块,行全量保留(板块≤7);
+ 无内容结构表时回退:板块式框架表 → 「结构与钩子」速览;镜号表/无表 → 不展示 */
+ const struct = [];
+ const cs = mdTableRows(A['内容结构']);
+ if (cs.head.length >= 2 && cs.rows.length) {
+ const map = {}; const order = [];
+ cs.rows.forEach((r) => {
+ const b = r[0] || '其他', d = r[1] || '', v = r[2] || '';
+ if (!v) return;
+ if (!map[b]) { map[b] = []; order.push(b); }
+ map[b].push([d, v]);
+ });
+ order.slice(0, 7).forEach((b) => { if (map[b].length) struct.push([b, map[b]]); });
+ }
+ let g3 = [];
+ if (!struct.length) {
+ const fw = mdTableRows(A['框架']);
+ if (fw.head.length >= 3 && /段落板块|板块|段落/.test(fw.head[0]) && fw.rows.length) {
+ const seen = {};
+ fw.rows.forEach((r) => {
+ const b = r[0] || '其他'; if (seen[b] || Object.keys(seen).length >= 5) return;
+ seen[b] = 1;
+ const tail = (r[2] || '') + (r[3] ? ' · ' + r[3] : '');
+ g3.push([b, [['', tail]]]);
+ });
+ }
+ }
+ const g4 = [...tagItems('内容风格', ''), ...tagItems('传播元素', '')].slice(0, 8); /* ④ 风格与传播 */
+ const g5 = []; /* ⑤ 台词金句/网络热梗(有才展示) */
+ Object.keys(A).forEach((k) => { mdTagLines(A[k]).forEach(([t, v]) => { if (/台词|金句/.test(t)) g5.push([t, v]); }); });
+ const meme = first('梗元素');
+ if (meme && !/^无/.test(meme) && meme !== '-' && meme !== '暂无') g5.push(['网络热梗', meme]);
+ const mods = [];
+ if (g1.length) mods.push({ title: '内容定位', kind: 'kv', rows: g1 });
+ if (g2.length) mods.push({ title: '可复用爆款内容', kind: 'kv', rows: g2 });
+ if (struct.length) mods.push({ title: '内容结构', kind: 'struct', groups: struct }); /* ③ 完整结构板块(含钩子/节奏等维度行) */
+ else if (g3.length) mods.push({ title: '结构与钩子', kind: 'blocks', blocks: g3 }); /* 无内容结构时回退框架速览 */
+ if (g4.length) mods.push({ title: '风格与传播', kind: 'kv', rows: g4 });
+ if (g5.length) mods.push({ title: '台词金句 · 网络热梗', kind: 'kv', rows: g5 });
+ return { track, mods };
+}
+/* SVG 生成器:analysis=拆解 dict,vt=视频标题(含话题标签原样,卡内自行清理),acc=账号名。
+ 排版模型:全宽 680,头部标题框 520×170(同账号设定卡),其下逐模块排版——模块标题(底色上书写)→ 内容卡(圆角白/米卡)。
+ 文本折行按「全角单位」计算:中文 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';
+ /* 色板(复用账号设定卡深浅两系规范) */
+ 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: '#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',
+ };
+ const W = 680, LX = 40, CW = W - LX * 2; /* 内容区:x 40,宽 600 */
+ const X = (u) => Math.max(4, Math.floor(u)); /* 全角单位 → 字数 */
+ const titleRaw = String(vt || '').split('#')[0].trim() || '视频拆解';
+ const headY0 = 30, headH = 170, headCY = headY0 + headH / 2; /* 头部框:y30 h170 同账号设定卡 */
+
+ /* —— 收集绘制元素(全部绝对坐标,直接输出) —— */
+ const els = []; /* {tag, x,y,w,h, fill,stroke,rx,rw} rect | {x,y,text,size,fill,weight,anchor,middle} text */
+ const R = (x, y, w, h, fill, stroke, rx = 12, rw = 1) => els.push({ tag: 'r', x, y, w, h, fill, stroke, rx, rw });
+ const T = (x, y, text, size, fill, weight, anchor) => els.push({ tag: 't', x, y, text, size, fill, weight, anchor });
+ /* 文字辅助:基线定位。text 行 y 直接为基线;中文高度约 size*1.35,行距取 size+5~8 */
+ /* 折行(maxU=该行最多全角字数)→ 行数组 */
+ const lineH = (size) => Math.round(size * 1.5); /* 行高(基线间距) */
+
+ /* —— 顶部标题框 —— */
+ R(80, headY0, 520, headH, P.headBox, P.headBd, 14, 1);
+ const tl = svgWrap(titleRaw, 20).slice(0, 2); /* 主标题最多 2 行,每行 ≤20 全角字 */
+ const tlMore = svgWrap(titleRaw, 20).length > 2;
+ const ty = tl.length === 2 ? headY0 + 52 : headY0 + 84;
+ tl.forEach((ln, i) => { T(340, ty + i * 32, (i === 1 && tlMore) ? ln + '…' : ln, 21, `url(#vkT${deep ? 'D' : 'L'})`, 700, 'middle'); });
+ T(340, headY0 + 146, '视频拆解 · 关键信息', 11, P.headSub, 600, 'middle');
+ T(340, headY0 + 162, `${acc ? acc + ' · ' : ''}赛道:${D.track || '未知'}`, 9.5, P.headDim, null, 'middle');
+
+ /* —— 逐模块排版 —— */
+ let y = headY0 + headH + 22; /* 头部框下起点 */
+ const vPadLR = 16, vPadT = 15, vPadB = 14; /* 卡内左右/上/下留白(F6:文字禁止贴靠卡边) */
+ const insetTop = (s) => Math.round(s * 0.9); /* 字形顶 → 基线(中文 glyph 顶在基线之上约 0.9em) */
+ const kvU = (size) => X((CW - vPadLR * 2 - 6) / (size + 1.1)); /* 卡内正文每行最多全角字数 */
+ D.mods.forEach((mod, mi) => {
+ if (mi > 0) y += 14;
+ T(LX, y, '▍' + mod.title, 14, P.secTitle, 700);
+ y += 30;
+ if (mod.kind === 'kv') {
+ /* kv 卡:label 独占行(主题粗体)→ 值折行 ≤2 行;游标自卡顶 vPadT 起步 → 首行文字顶与卡顶留白 vPadT */
+ const rows = mod.rows.filter(([, v]) => String(v).trim());
+ const items = []; /* {text,size,fill,weight,top} top=文字顶(相对卡顶) */
+ let cur = vPadT;
+ rows.forEach(([k, v]) => {
+ items.push({ text: k, size: 12.5, fill: P.boxTitle, weight: 700, top: cur }); cur += 22;
+ const ws = svgWrap(v, kvU(11.5)); const more = ws.length > 2;
+ ws.slice(0, 2).forEach((ln, i) => { items.push({ text: more && i === 1 ? ln + '…' : ln, size: 11.5, fill: P.boxText, top: cur }); cur += 17; });
+ cur += 8; /* 条目间距 */
+ });
+ if (!items.length) return;
+ cur -= 8;
+ const h = cur + vPadB;
+ R(LX, y, CW, h, P.box, P.boxBd, 12, 1);
+ items.forEach((it) => T(LX + vPadLR, y + it.top + insetTop(it.size), it.text, it.size, it.fill, it.weight));
+ y += h + 16;
+ } else if (mod.kind === 'struct') {
+ /* 内容结构整卡:板块小节 = 板块名行(左侧竖条) + 其下「维度:内容」全量行(值折行 ≤3 行) */
+ const items = [];
+ let cur = vPadT;
+ mod.groups.forEach(([b, rows], gi) => {
+ if (gi > 0) cur += 8;
+ items.push({ text: b, size: 12.5, fill: P.boxTitle, weight: 700, top: cur, bar: true }); cur += 23;
+ rows.forEach(([d, v]) => {
+ const s = (d ? d + ':' : '') + v;
+ const ws = svgWrap(s, kvU(11.5)); const more = ws.length > 3;
+ ws.slice(0, 3).forEach((ln, i) => { items.push({ text: more && i === 2 ? ln + '…' : ln, size: 11.5, fill: P.boxText, top: cur }); cur += 17; });
+ cur += 3;
+ });
+ });
+ cur -= 3;
+ const h = cur + vPadB;
+ R(LX, y, CW, h, P.box, P.boxBd, 12, 1);
+ items.forEach((it) => {
+ if (it.bar) R(LX + vPadLR, y + it.top + 4, 4, 14, P.boxTag, null, 2, 0);
+ T(LX + vPadLR + (it.bar ? 13 : 0), y + it.top + insetTop(it.size), it.text, it.size, it.fill, it.weight);
+ });
+ y += h + 16;
+ } else {
+ /* blocks 多列小卡(无内容结构时的框架速览等):板块名 + 要点行 ≤2 行折行 */
+ const nb = mod.blocks.length, gap = 12;
+ const bw = (CW - gap * (nb - 1)) / nb;
+ const bu = X((bw - 24) / 11.8); /* 小卡正文每行全角字数 */
+ const segs = mod.blocks.map(([b, rows]) => {
+ const its = []; let cur = 13;
+ its.push({ text: b, size: 12, fill: P.boxTitle, weight: 700, top: cur }); cur += 21;
+ rows.slice(0, 4).forEach(([d, v]) => {
+ const s = (d ? d + ':' : '') + v;
+ const ws = svgWrap(s, bu); const more = ws.length > 2;
+ ws.slice(0, 2).forEach((ln, i) => { its.push({ text: more && i === 1 ? ln + '…' : ln, size: 10.5, fill: P.boxText, top: cur }); cur += 16; });
+ cur += 4;
+ });
+ cur -= 4;
+ return { its, h: cur + 12 };
+ });
+ const rowH = Math.max(...segs.map((s) => s.h));
+ segs.forEach((sg, si) => {
+ const x = LX + si * (bw + gap);
+ R(x, y, bw, rowH, P.box, P.boxBd, 10, 1);
+ sg.its.forEach((it) => T(x + 12, y + it.top + insetTop(it.size), it.text, it.size, it.fill, it.weight));
+ });
+ y += rowH + 16;
+ }
+ });
+ const totalH = y + 6;
+ /* —— 输出 —— */
+ const escX = esc;
+ let out = '';
+ els.forEach((e) => {
+ if (e.tag === 'r') out += `