R59: 视频详情拆解tab默认视图升级SVG关键信息卡(analysis 13维自动抽取无AI/md标签表双解析/深浅色系按调性/内容结构整卡板块化/卡文切换+cache bump e/i)
This commit is contained in:
1 parent
1f59acd16b
commit
92fccbb768
3 files changed
+275
-5
No files matched your search
@@ -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 += `<rect x="${e.x}" y="${e.y}" width="${e.w}" height="${e.h}" rx="${e.rx}" fill="${e.fill}" stroke="${e.stroke}" stroke-width="${e.rw}"/>`;
|
||||
else out += `<text x="${e.x}" y="${e.y}" font-size="${e.size}"${e.fill ? ` fill="${e.fill}"` : ''}${e.weight ? ` font-weight="${e.weight}"` : ''}${e.anchor ? ` text-anchor="${e.anchor}"` : ''}>${escX(e.text)}</text>`;
|
||||
});
|
||||
return `<svg viewBox="0 0 ${W} ${totalH}" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" style="display:block">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="${P.bgA}"/>
|
||||
<stop offset="1" stop-color="${P.bgB}"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="vkT${deep ? 'D' : 'L'}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="${P.headTitleA}"/>
|
||||
<stop offset="1" stop-color="${P.headTitleB}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="${W}" height="${totalH}" fill="url(#bgGrad)"/>
|
||||
${out}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
|
||||
async function renderVideoDetail(view, id) {
|
||||
const videoId = Number(id);
|
||||
let tab = 'script';
|
||||
let vkMode = 'card'; /* R59:拆解 tab 默认展示「视频关键信息卡」,可切换「拆解全文」文案版 */
|
||||
const d = await api('/api/dsh/video-detail?videoId=' + videoId).catch((e) => ({ ok: false, error: e.message }));
|
||||
if (!d.ok) { view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}</div><div class="empty">${esc(d.error || '视频不存在')}</div></div>`; bindBack(view); return; }
|
||||
const v = d.video, analysis = d.analysis, parse = d.parse || {};
|
||||
@@ -1200,17 +1442,27 @@ async function renderVideoDetail(view, id) {
|
||||
const viewLink = v.video_url ? `<a class="link" data-ext="${esc(v.video_url)}" title="打开视频网页">查看视频 ↗</a>` : '无';
|
||||
const scriptMd = parse.content || null;
|
||||
const deconstructMd = parse.analysis ? analysisToMd(parse.analysis) : null;
|
||||
const keyCardHtml = renderVideoKeySvg(parse.analysis, v.video_title, v.account_name); /* R59:关键信息卡(SVG 生成器,拆解维度自动抽取,无则空 → 纯全文) */
|
||||
/* 解析/刷新按钮(09-03 R28):未解析=空态「解析」;已解析=内容右上角「刷新」(同 F5 解析任务,幂等覆盖) */
|
||||
const parseBtn = (mode) => `<button class="btn btn-sm" data-parse="${mode}" title="${mode === 'refresh' ? '重新解析该视频(覆盖更新视频脚本与视频拆解)' : '对该视频做 MCP 解析(产出视频脚本与视频拆解)'}">${svgIcon(mode === 'refresh' ? 'refresh' : 'zap', 14, 'currentColor')} ${mode === 'refresh' ? '刷新' : '解析'}</button>`;
|
||||
const emptyAct = (icon, title, desc) => `<div class="empty-state"><div class="empty-icon">${svgIcon(icon, 40)}</div><div class="empty-title">${title}</div><div class="empty-desc">${desc}</div><div style="margin-top:12px">${parseBtn('parse')}</div></div>`;
|
||||
const tabBtns = [['script', '视频脚本'], ['deconstruct', '视频拆解']].map(([k, label]) => `<button class="tab ${tab === k ? 'tab-active' : ''}" data-tab="${k}">${label}</button>`).join('');
|
||||
/* R59:拆解 tab 内容区 = 关键信息卡(默认)/ 拆解全文 切换视图(同账号设定卡片交互:右上角卡/文切换钮,无 AI、纯前端抽取) */
|
||||
const deconPanel = () => {
|
||||
if (!deconstructMd) return emptyAct('chart', '暂无视频拆解', '该视频尚未完成结构化拆解。点击「解析」对该视频做 MCP 内容解析(同时产出「视频脚本」与「视频拆解」,约需数分钟)。');
|
||||
if (!keyCardHtml) return `<div class="vd-tools">${parseBtn('refresh')}</div><div class="md-body">${renderMarkdown(deconstructMd)}</div>`;
|
||||
const vkSwitch = `<div class="vk-switch" title="切换视图"><button class="vi-btn ${vkMode === 'card' ? 'vi-active' : ''}" data-vk="card" title="关键信息卡片版">${svgIcon('image', 15)}</button><button class="vi-btn ${vkMode === 'text' ? 'vi-active' : ''}" data-vk="text" title="拆解全文版">${svgIcon('file-text', 15)}</button></div>`;
|
||||
return `<div class="vd-tools">${vkSwitch}${parseBtn('refresh')}</div>
|
||||
<div class="vd-wrap">
|
||||
<div class="vk-view ${vkMode === 'card' ? '' : 'vs-hidden'}">${keyCardHtml}</div>
|
||||
<div class="md-body persona-text vk-text ${vkMode === 'text' ? '' : 'vs-hidden'}">${renderMarkdown(deconstructMd)}</div>
|
||||
</div>`;
|
||||
};
|
||||
let body = '';
|
||||
if (tab === 'script') body = scriptMd
|
||||
? `<div class="vd-tools">${parseBtn('refresh')}</div><div class="md-body">${renderMarkdown(scriptMd)}</div>`
|
||||
: emptyAct('file-text', '暂无视频脚本', '该视频尚未解析。点击「解析」对该视频做 MCP 内容解析(同时产出「视频脚本」与「视频拆解」,约需数分钟)。');
|
||||
else if (tab === 'deconstruct') body = deconstructMd
|
||||
? `<div class="vd-tools">${parseBtn('refresh')}</div><div class="md-body">${renderMarkdown(deconstructMd)}</div>`
|
||||
: emptyAct('chart', '暂无视频拆解', '该视频尚未完成结构化拆解。点击「解析」对该视频做 MCP 内容解析(同时产出「视频脚本」与「视频拆解」,约需数分钟)。');
|
||||
else if (tab === 'deconstruct') body = deconPanel();
|
||||
view.innerHTML = `
|
||||
<div class="page page-detail">
|
||||
<div class="page-toolbar">${toolbarBack()}<span class="page-title">${esc(cleanTitle(v.video_title) || '视频详情')}</span></div>
|
||||
@@ -1230,6 +1482,14 @@ async function renderVideoDetail(view, id) {
|
||||
bindBack(view);
|
||||
view.querySelectorAll('[data-tab]').forEach((b) => b.addEventListener('click', () => { tab = b.dataset.tab; load(); }));
|
||||
view.querySelectorAll('[data-parse]').forEach((b) => b.addEventListener('click', () => confirmVideoParse(b, v, b.dataset.parse)));
|
||||
/* R59:关键信息卡 / 拆解全文 切换(DOM 切换不重渲染,保留滚动位置;同账号设定 data-vs 交互) */
|
||||
view.querySelectorAll('[data-vk]').forEach((b) => b.addEventListener('click', () => {
|
||||
vkMode = b.dataset.vk;
|
||||
view.querySelectorAll('[data-vk]').forEach((x) => x.classList.toggle('vi-active', x.dataset.vk === vkMode));
|
||||
const cv = view.querySelector('.vk-view'), tv = view.querySelector('.vk-text');
|
||||
if (cv) cv.classList.toggle('vs-hidden', vkMode !== 'card');
|
||||
if (tv) tv.classList.toggle('vs-hidden', vkMode !== 'text');
|
||||
}));
|
||||
view._reload = load; // 静默自动刷新(保留当前 tab)
|
||||
}
|
||||
load();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>短视频工作台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260904c">
|
||||
<link rel="stylesheet" href="style.css?v=20260904e">
|
||||
<script src="vendor/echarts.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -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=20260904f"></script>
|
||||
<script src="data-pages.js?v=20260904i"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -549,6 +549,16 @@ td.td-title { max-width: 240px; }
|
||||
/* 视频详情属性内容统一宽度(09-03 R48):与账号详情同款——8 字段固定 width 140px、横向间隔 0;超宽单行省略号 + hover 看全量;
|
||||
标签(info-wide)占整行不受限宽保持换行;原视频ID vid-limited 独立 max-width 220 已被统一 140 覆盖,移除冗余规则 */
|
||||
.video-info .video-grid .info-item:not(.info-wide) .info-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ================= R59 视频关键信息卡(拆解 tab 默认视图,卡/文切换) ================= */
|
||||
.vd-tools { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin: 2px 0 8px 20px; }
|
||||
.vk-switch { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--panel); }
|
||||
.vk-switch .vi-btn { padding: 4px 9px; }
|
||||
.vd-wrap .md-body { margin-left: 20px; }
|
||||
/* SVG 关键信息卡:白底阴影浮卡,图片随宽自适应 */
|
||||
.vk-view { margin-left: 20px; }
|
||||
.vk-view svg { display: block; width: auto; max-width: 100%; height: auto; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,.08); }
|
||||
|
||||
/* 长文本默认最多 4 行,可点击展开/收起(账号定位等) */
|
||||
/* 09-01 改用 block+max-height 裁剪:display:-webkit-box 下长句不折行,内容宽被撑到 1896px 导致 main 横向滚动条 */
|
||||
.clamp-text { display: block; max-height: 96px; overflow: hidden; word-break: break-word; overflow-wrap: anywhere; } /* 96px=4行×15px×1.6 */
|
||||
|
||||
Reference in new issue
Block a user