@@ -1035,9 +1050,8 @@ async function renderAccountDetail(view, id) {
if (tab === 'persona') renderTrendChart(view); // 视频列表页上方:点赞/评论趋势折线图(lieflat-charts G8 骨架,ECharts)
view.querySelectorAll('[data-tab]').forEach((b) => b.addEventListener('click', async () => {
tab = b.dataset.tab;
- if (tab === 'analysis' && analysis === null) await loadAnalysis();
syncHash();
- load(); // info tab 的账号设定加载已在 load() 内处理
+ load(); // info 的账号设定 / analysis 的分析报告均在 load() 内按需拉取
}));
bindPager(view, (p) => { page = p; syncHash(); load(); });
bindBatchBar(view, {
@@ -1099,6 +1113,17 @@ async function renderAccountDetail(view, id) {
skills: ['mcn-dou-analysis'],
btn: exBtn,
}));
+ // 09-04:数据分析 tab 空态「账号数据分析」按钮 → mcn-dou-analysis 功能四(近4个月数据 逐条拆解+汇总分析)
+ const anaBtn = view.querySelector('[data-analyze]');
+ if (anaBtn) anaBtn.addEventListener('click', () => confirmRun({
+ title: '账号数据分析',
+ desc: `将基于账号「${acc.account_name}」近 4 个月视频数据(每月最好/最差各 1 条,最多 8 条),先逐条拆解再汇总分析,产出 8 章结构分析报告(内容表现规律 / 月度趋势 / 差异化壁垒 / 商业化空间)。任务需数分钟,完成后自动刷新本页。`,
+ target: `账号「${acc.account_name}」|近 4 个月视频数据`,
+ prompt: `${SKILL_HINT_ANALYZE}\n请为达人「${acc.account_name}」进行账号数据分析(功能四)。`,
+ label: '分析指令',
+ skills: ['mcn-dou-analysis'],
+ btn: anaBtn,
+ }));
view._reload = load; // 静默自动刷新(保留当前 tab/翻页)
}
@@ -1364,7 +1389,8 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
if (days) days.addEventListener('change', (e) => { state.custom.days = e.target.value; state.custom.page = 1; load(); });
const clear = view.querySelector('[data-scope-clear]');
if (clear) clear.addEventListener('click', () => { state.scope = ''; state.ref.search = ''; state.ref.page = 1; state.custom.page = 1; reloadAll(); });
- view.querySelectorAll('[data-cview]').forEach((b) => b.addEventListener('click', () => openCustomScriptView(Number(b.dataset.cview))));
+ /* R58:自定义选题「查看」并入统一 AI 脚本弹窗(openScriptModal rewriteId 分支单条展示),获得删除按钮/诊断面板能力;原 openCustomScriptView 简单弹窗移除 */
+ view.querySelectorAll('[data-cview]').forEach((b) => b.addEventListener('click', () => openScriptModal({ rewriteId: Number(b.dataset.cview) })));
}
}
@@ -1421,30 +1447,6 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
}
-/* 自定义选题脚本 查看弹窗(09-02 v25):GET /api/dsh/custom-script?id 拉全文(脚本不进列表以控体积) */
-async function openCustomScriptView(id) {
- let s = null;
- try { s = (await api('/api/dsh/custom-script?id=' + encodeURIComponent(id))).script; } catch (e) { s = null; }
- if (!s) { showToast('❌ 脚本加载失败'); return; }
- const mask = document.createElement('div'); mask.className = 'modal-mask';
- mask.innerHTML = `
`;
- document.body.appendChild(mask);
- const close = () => mask.remove();
- mask.addEventListener('click', (e) => { if (e.target === mask) close(); });
- const x = mask.querySelector('[data-x]'); if (x) x.addEventListener('click', close);
- const ok = mask.querySelector('[data-ok]'); if (ok) ok.addEventListener('click', close);
- const cp = mask.querySelector('[data-copy]');
- if (cp) cp.addEventListener('click', async () => { showToast((await copyText(s.script_text || '')) ? '✅ 已复制全文' : '❌ 复制失败'); });
-}
-
/* ================= AI脚本诊断 ================= */
async function renderReviews(view) {
const d = await api('/api/dsh/rewrites').catch((e) => ({ ok: false, error: e.message }));
@@ -1547,16 +1549,15 @@ async function renderReviews(view) {
const rv = d3.review;
const typeName = rv.type === 'source' ? '原视频脚本复盘' : rv.type === 'compare' ? '对比分析' : 'AI 脚本复盘';
let dimsObj = {}; try { dimsObj = JSON.parse(rv.dims_json || '{}'); } catch (e) {}
- const DIMS_ALL = ['创意看点', '设定承接', '框架节奏', '爆款开场', '叙事逻辑', '极致事件', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他'];
const scoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f');
- const scoreBar = (dims, prefix) => DIMS_ALL.map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`; }).join('');
+ const scoreBar = (dims) => diagVisibleDims(dims).map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`; }).join('');
let body = '';
if (rv.type === 'compare') {
const aiD = dimsObj.ai || {}, srcD = dimsObj.source || {};
let t = {}; try { t = JSON.parse(rv.total || '{}'); } catch (e) {}
- body = `
`;
}
let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
if (issues.length) body += `
`;
@@ -1585,6 +1586,9 @@ const DIAG_DIMS = ['创意看点', '设定承接', '框架节奏', '爆款开场
维度名兼作 dims_json 数据 key——历史复盘仍为旧 key,取值回退别名兼容(新 key 缺失时读旧 key),保证历史分数继续展示 */
const DIAG_KEY_ALIAS = { '爆款开场': '钩子设置', '创意看点': '共鸣和共情' };
const diagDimVal = (dims, k) => { const v = dims[k]; return (typeof v === 'number' && Number.isFinite(v)) ? v : (DIAG_KEY_ALIAS[k] ? dims[DIAG_KEY_ALIAS[k]] : v); };
+/* R56:教程完整链对当前脚本不适用(dims 中该维度为 null/缺失,如剧情类脚本)→ 该维度行整体不展示;其余维度无值时保持原「—」占位 */
+const diagDimHas = (dims, k) => { const v = diagDimVal(dims, k); return typeof v === 'number' && Number.isFinite(v); };
+const diagVisibleDims = (dims) => (diagDimHas(dims, '教程完整链') ? DIAG_DIMS : DIAG_DIMS.filter((k) => k !== '教程完整链'));
const diagScoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f');
const diagFmtScore = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : '-');
@@ -1608,14 +1612,14 @@ async function reportFullReview(reviewId) {
const rv = d3.review;
const typeName = rv.type === 'source' ? '原视频脚本复盘' : rv.type === 'compare' ? '对比分析' : 'AI 脚本复盘';
let dimsObj = {}; try { dimsObj = JSON.parse(rv.dims_json || '{}'); } catch (e) {}
- const scoreBar = (dims, prefix) => DIAG_DIMS.map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`; }).join('');
+ const scoreBar = (dims) => diagVisibleDims(dims).map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`; }).join('');
let body = '';
if (rv.type === 'compare') {
const aiD = dimsObj.ai || {}, srcD = dimsObj.source || {};
let t = {}; try { t = JSON.parse(rv.total || '{}'); } catch (e) {}
- body = `
`;
}
let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
if (issues.length) body += `
`;
@@ -1657,14 +1661,16 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
}
}
if (!rewrites.length) { showToast('该脚本不存在或已删除'); return; }
- let verIdx = rewriteId ? Math.max(0, rewrites.findIndex((x) => String(x.id) === String(rewriteId))) : 0;
- if (verIdx < 0) verIdx = 0;
+ /* R55:版本展示层 旧→新(最左=最初版、最右=最新版),版本条「新在右 旧在左」与方向词按钮一致 */
+ rewrites = rewrites.slice().reverse();
+ let verIdx = rewriteId ? rewrites.findIndex((x) => String(x.id) === String(rewriteId)) : rewrites.length - 1;
+ if (verIdx < 0) verIdx = rewrites.length - 1; /* 指定版本已不存在 → 回到最新版(最右) */
const title = metaTitle || 'AI 脚本';
const hasSource = vid > 0 && !!source;
let stab = 'diag'; /* 右侧 tab:diag=脚本诊断(默认)/ sb=分镜提示词 */
- let showSrc = false, sbIdx = 0, showSide = false; /* R41:showSide=右侧「脚本诊断|分镜提示词」面板是否展开(收起后只展示 AI 脚本);R50:默认收起=不显示复盘和分镜 */
+ let showSrc = false, sbIdx = -1, showSide = false; /* R41:showSide=右侧「脚本诊断|分镜提示词」面板是否展开(收起后只展示 AI 脚本);R50:默认收起=不显示复盘和分镜;R55:sbIdx=-1=未选分镜版本,sideContent 自动指向该 AI 版本的最新分镜(末位) */
const stb = vid > 0 ? await api('/api/dsh/storyboards?videoId=' + vid).catch(() => ({ items: [] })) : { items: [] };
- const storyboards = ((stb && stb.items) || []);
+ const storyboards = ((stb && stb.items) || []).slice().reverse(); /* 同版本条:旧→新,末位=最新分镜 */
const visibleSbOf = (curId) => storyboards.filter((s) => s.rewrite_id == null || String(s.rewrite_id) === String(curId));
/* 复盘任务触发:有源脚本 → 三模式(AI/源/对比);无源 → 独立分析仅评 AI 脚本(同脚本诊断页行按钮口径) */
@@ -1706,7 +1712,7 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
const diagHtml = (dg) => {
const { rv, dims, total, srcTotal } = dg;
const totalFin = Number.isFinite(total);
- const scoreBar = DIAG_DIMS.map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`; }).join('');
+ const scoreBar = diagVisibleDims(dims).map((k) => { const v = diagDimVal(dims, k); const has = typeof v === 'number' && Number.isFinite(v); return `
`;
let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {}
if (issues.length) html += `
`;
@@ -1719,12 +1725,11 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
切换类型与刷新范围:tab 脚本诊断/分镜提示词 / 分镜上下版 → 仅右侧列(patchSide);版本上下 → AI列+右侧列(patchAll);源折叠 → 纯 CSS toggle
弹窗 DOM(mask)生命周期内不被移除,仅替换受影响列内容 → 无整窗闪断 */
const curOf = () => rewrites[Math.min(verIdx, rewrites.length - 1)] || rewrites[0];
- const verCtrl = (p, idx, len, label, genLabel) => {
- const inner = len > 1
- ? `
` : '');
- return ``;
- };
+ /* R55:版本条 左旧右新——计数=旧→新第几版(idx+1/len,最右=len=最新);末位标「最新」徽标;按钮方向词 ← 较旧 / 较新 →;timeStr 显示该版创建时间辅助辨认 */
+ const verCtrlInner = (p, idx, len, label, timeStr) => (len > 1
+ ? `
` : ''));
+ const verCtrl = (p, idx, len, label, genLabel, timeStr) => ``;
const sideHeadHtml = () => `
|
@@ -1738,10 +1743,53 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
${renderMarkdown(source)}
` : '';
- const aiSecHtml = (c) => `
AI脚本${srcToggleHtml()}${sideToggleHtml()}
${renderMarkdown(c.script_text || '(暂无内容)')}
${rewrites.length > 1 ? verCtrl('v', verIdx, rewrites.length, '版本') : ''}`;
+ const aiSecHtml = (c) => `
AI脚本${srcToggleHtml()}${sideToggleHtml()}
${renderMarkdown(c.script_text || '(暂无内容)')}
+ `;
+ /* R58:删除当前脚本(左下角浅红按钮)——同时删除其诊断复盘与分镜提示词;确认后执行 API,多版本就地移除当前版并刷新,单版本关弹窗并重建页面 */
+ const delScriptBtnHtml = (c) => `
`;
+ const fireDel = async (btn) => {
+ if (btn.dataset.lock) return; btn.dataset.lock = '1';
+ try {
+ const c = curOf(); if (!c) return;
+ const isLast = rewrites.length <= 1;
+ const mask = document.createElement('div'); mask.className = 'modal-mask';
+ mask.innerHTML = `
+
+
${svgIcon('trash', 17, '#d9534f')} 删除当前脚本
+
将删除 AI 脚本${rewrites.length > 1 ? `第 ${verIdx + 1}/${rewrites.length} 版` : ''}(创建于 ${esc(fmtTime(c.created_time))}):
+
《${esc(fmtTitleQ(title))}》
+
⚠️ 该脚本的诊断复盘与分镜提示词将一并删除,不可恢复${isLast ? ';其视频若无其他 AI 脚本,视频级复盘也会清理' : ''}。
+
+
+
+
+
`;
+ document.body.appendChild(mask);
+ const close = () => mask.remove();
+ mask.addEventListener('click', (e) => { if (e.target === mask) close(); });
+ mask.querySelector('[data-cancel]').addEventListener('click', close);
+ mask.querySelector('[data-ok]').addEventListener('click', async () => {
+ close();
+ try {
+ const r = await api('/api/dsh/script-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: c.id }) });
+ if (!r || r.ok === false) { showToast('❌ 删除失败: ' + ((r && r.error) || '未知错误')); return; }
+ showToast('✅ 已删除该脚本及关联数据');
+ const host = document.querySelector('.rw-modal'); if (host) host.remove(); /* 先关当前弹窗 */
+ if (isLast) { router(); return; }
+ const i = rewrites.findIndex((x) => String(x.id) === String(c.id));
+ if (i >= 0) rewrites.splice(i, 1);
+ if (verIdx >= rewrites.length) verIdx = rewrites.length - 1;
+ sbIdx = -1;
+ renderModal(); /* 多版本:就地重建弹窗(剩余版本列表,定位相邻版本) */
+ } catch (e) { showToast('❌ 删除失败: ' + e.message); }
+ });
+ } finally { delete btn.dataset.lock; }
+ };
/* 右侧列内容计算(diag 空态按钮 R40 改「开始诊断」;stab=diag 才请求复盘接口——原实现无论什么 tab 都查,浪费) */
async function sideContent(c) {
const visibleSb = visibleSbOf(c.id);
+ /* R55:sbIdx=-1(未选/切过版本)或越界 → 自动指向该 AI 版本的最新分镜(末位);同版本内左右翻页保持当前位置 */
+ if (sbIdx < 0 || (visibleSb.length && sbIdx >= visibleSb.length)) sbIdx = Math.max(0, visibleSb.length - 1);
const sb = visibleSb[Math.min(sbIdx, visibleSb.length - 1)];
let sideBody = '', sideFoot = '';
if (stab === 'diag') {
@@ -1755,7 +1803,7 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
}
} else if (visibleSb.length) {
sideBody = sb ? renderSbBlocks(sb.script_text) : '' /* R51:空态提示清空——不再显示「(暂无分镜提示词)」 */;
- sideFoot = verCtrl('s', sbIdx, visibleSb.length, '分镜', '重新生成');
+ sideFoot = verCtrl('s', sbIdx, visibleSb.length, '分镜', '重新生成', sb ? sb.created_time : '');
} else if (vid > 0) {
sideBody = '' /* R51:空态提示清空——不再显示「(暂无分镜提示词)」 */;
sideFoot = ``;
@@ -1850,8 +1898,9 @@ async function openScriptModal({ rewriteId, videoId } = {}) {
sideT.title = showSide ? '收起脚本诊断/分镜提示词,只看 AI 脚本' : '展开脚本诊断/分镜提示词';
sideT.innerHTML = svgIcon(showSide ? 'chevrons-right' : 'chevrons-left', 16, 'currentColor');
});
- const vp = mask.querySelector('[data-vprev]'); if (vp) vp.addEventListener('click', async () => { if (verIdx > 0) { verIdx--; sbIdx = 0; await patchAll(mask); } });
- const vn = mask.querySelector('[data-vnext]'); if (vn) vn.addEventListener('click', async () => { if (verIdx < rewrites.length - 1) { verIdx++; sbIdx = 0; await patchAll(mask); } });
+ const vp = mask.querySelector('[data-vprev]'); if (vp) vp.addEventListener('click', async () => { if (verIdx > 0) { verIdx--; sbIdx = -1; await patchAll(mask); } }); /* R55:vprev=向左=较旧;sbIdx=-1 使右侧分镜自动定位新版本最新分镜 */
+ const vn = mask.querySelector('[data-vnext]'); if (vn) vn.addEventListener('click', async () => { if (verIdx < rewrites.length - 1) { verIdx++; sbIdx = -1; await patchAll(mask); } }); /* vnext=向右=较新 */
+ const dl = mask.querySelector('[data-del-script]'); if (dl) dl.addEventListener('click', () => fireDel(dl)); /* R58:删除当前脚本 */
bindSide(mask);
};
async function renderModal() {
diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/help-guide.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/help-guide.js
new file mode 100644
index 0000000..e184ddd
--- /dev/null
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/help-guide.js
@@ -0,0 +1,438 @@
+/* =========================================================================
+ 短视频工作台 - 帮助文档 + 模拟操作引导(09-04 新增 help-guide)
+ -------------------------------------------------------------------------
+ 板块化帮助:按工作台实际页面组织(首页/热点数据/账号列表/账号详情/视频详情/AI写脚本/脚本诊断)。
+ 每个板块卡提供:用途说明 + 功能清单 + 「▶ 模拟操作指引」+ 「进入板块」+ 常见问法(可复制)。
+
+ 模拟操作引导(startGuide):
+ - 跳转到该板块真实页面(详情类自动取库中第一条数据作示例);
+ - 全屏遮罩(拦截误点)+ 高亮目标控件 + 气泡讲解;
+ - 无副作用控件(tab/下拉/搜索/翻页)→ 引导你真实点击体验,点对即进下一步;
+ - 有副作用控件(更新数据/关注/AI创作/复盘等)→ 只讲解 + 「下一步」,防误触发;
+ - 步骤目标找不到(无示例数据)→ 自动跳过该步,不卡死。
+ ========================================================================= */
+(function () {
+ const $ = (s, r) => (r || document).querySelector(s);
+
+ /* ---------- 详情类示例路由:取库中第一条数据作引导样本 ---------- */
+ async function firstAccountRoute() {
+ try {
+ const d = await api('/api/dsh/accounts?type=all&search=&track=&page=1&pageSize=1');
+ const a = (d && d.items && d.items[0]);
+ if (a && a.id) return '#/account/' + a.id;
+ } catch (e) { /* 接口失败走兜底 */ }
+ return null;
+ }
+ async function firstVideoRoute() {
+ try {
+ const d = await api('/api/dsh/videos?search=&parsed=&days=&page=1&pageSize=1');
+ const v = (d && d.items && d.items[0]);
+ if (v && v.id) return '#/video/' + v.id;
+ } catch (e) { /* 接口失败走兜底 */ }
+ return null;
+ }
+
+ /* ---------- 板块定义 ----------
+ steps[].sel 目标选择器(页面渲染后查找;找不到自动跳过)
+ steps[].mode 'click' 真实点击推进(安全控件);'input' 输入回车推进;'info' 仅讲解 + 下一步
+ steps[].danger 危险/有副作用控件:加 pointer-events:none 防误触
+ steps[].emptyOk 目标缺失时仍展示文字(不跳过) */
+ const BOARDS = [
+ {
+ key: 'home', icon: 'home', title: '首页总览', route: '#/',
+ desc: '工作台主入口:数据预览 + 四大功能卡 + 榜单速览 + 最近 AI 脚本',
+ steps: [
+ { sel: '.home-stats', mode: 'info', title: '数据总览条', text: '顶部统计条一屏看全库规模:关注账号数、视频解析数、AI写脚本数、AI脚本诊断数、AI脚本平均评分。数据会随库中变化自动刷新。' },
+ { sel: '.data-cards', mode: 'info', title: '四大功能卡', text: '首页四张卡片 = 工作台四大板块:热点数据(榜单)、账号列表(达人库)、AI写脚本(脚本中心)、脚本诊断(复盘评分)。点击卡片即可进入对应板块。' },
+ { sel: '.rank-preview .rank-tab[data-rank-tab="hot"]', mode: 'click', title: '榜单速览', text: '首页内置榜单预览,无需进完整页即可扫一眼:账号周榜 / 视频热榜 / 点赞榜。' + '\n' + '👉 现在点一下「视频热榜」tab 试试切换(可来回点三个 tab)。' },
+ { sel: '#recentScriptsList .recent-card', mode: 'info', emptyOk: true, title: '最近 AI 脚本', text: '最近生成的 AI 脚本列表,点卡片可打开脚本详情(含源脚本对比/诊断/分镜)。若尚无脚本则为空。' },
+ ],
+ qa: [
+ ['"帮我写一个美妆种草脚本,30秒"', '全流程创作:人设→选题→框架→大纲→脚本 + 质检'],
+ ['"帮我想几个美妆赛道的爆款选题"', '按赛道生成差异化选题'],
+ ],
+ },
+ {
+ key: 'ranking', icon: 'trending-up', title: '热点数据', route: '#/ranking',
+ desc: '三榜浏览与更新:账号周榜 · 视频热榜 · 点赞榜(支持赛道筛选 / 历史期切换 / 点击表头排序)',
+ steps: [
+ { sel: '.rank-tab[data-rank-tab="hot"]', mode: 'click', title: '切换榜单', text: '页面上方三个 tab 对应三张榜:账号周榜(达人综合实力)、视频热榜(近7日最热作品)、点赞榜(近7日点赞飙升)。' + '\n' + '👉 点「视频热榜」tab 切换到该榜。' },
+ { sel: '.rank-tab[data-rank-tab="likes"]', mode: 'click', title: '再切一榜', text: '👉 再点「点赞榜」,感受榜单间切换。各榜列字段不同(周榜=粉丝/新增互动;热榜=点赞/评论/分享/收藏)。' },
+ { sel: '#rankCat', mode: 'click', title: '赛道筛选', text: '右侧「赛道筛选」下拉只保留你想看的赛道(如 亲子/美食)。' + '\n' + '👉 点开下拉任选一个赛道,再切回「全部赛道」。' },
+ { sel: '#rankPrev', mode: 'click', emptyOk: true, title: '历史期切换', text: '◀ ▶ 切换历史榜单期次,中间的日期即当前数据期(红狐日榜每日更新)。' + '\n' + '👉 点一下 ◀ 回看上一期。' },
+ { sel: '#rankUpdate', mode: 'info', danger: true, title: '更新榜单数据', text: '🌐 按钮触发后台任务,重新抓取三榜最新一期并落盘(红狐数据按次计费,30 分钟内重复点会被拦截)。此处仅讲解,不实际点击。' },
+ { sel: '.rank-tabs', mode: 'info', title: '榜单行操作', text: '账号周榜每行有「查看/关注」:已入库账号可点「查看」直达账号详情,未入库可「关注」纳入达人库(会触发抓取)。数值列表头可点击排序。' },
+ ],
+ qa: [
+ ['"更新一下热点数据三榜"', '触发后台任务抓取三榜最新一期并落盘'],
+ ['"看看上周账号周榜的亲子赛道"', '切换期次 + 赛道筛选'],
+ ],
+ },
+ {
+ key: 'accounts', icon: 'users', title: '账号列表', route: '#/accounts',
+ desc: '达人库:添加 / 关注 / 批量更新账号数据,行内直达账号详情与 AI 创作',
+ steps: [
+ { sel: '#accType', mode: 'click', title: '类型筛选', text: '按账号类型过滤:全部 / 内部(公司签约)/ 外部(对标观察)。' + '\n' + '👉 点开下拉切换「外部」再切回「全部」。' },
+ { sel: '#accSearch', mode: 'input', title: '搜索账号', text: '搜索框支持按账号名 / 简介内容 / 赛道检索。' + '\n' + '👉 输入关键词(如「亲子」)后按 回车 过滤,再看效果。' },
+ { sel: '#accImport', mode: 'click', title: '添加账号', text: '「添加」弹窗填入抖音账号名称 / 地址,AI 将通过抖音网页抓取账号信息 + 视频列表入库(新增账号入口)。' + '\n' + '👉 点击打开弹窗看看表单,再点「取消」关闭(不实际添加)。' },
+ { sel: '.modal-mask [data-cancel]', mode: 'click', title: '关闭弹窗', text: '👉 点「取消」关闭添加弹窗,回到账号列表。' },
+ { sel: '#accUpdate', mode: 'info', danger: true, title: '更新关注账号', text: '🌐 重新抓取所有 ⭐ 已关注账号的最新数据与视频列表(后台逐账号串行,每个数分钟)。此处仅讲解,不实际点击。' },
+ { sel: '.fav-btn', mode: 'info', danger: true, emptyOk: true, title: '关注 / 取消关注', text: '行首 ☆/⭐ 切换关注:关注的账号参与「更新」批量刷新,也是「关注账号优先」排榜的来源。此处仅讲解。' },
+ { sel: '[data-create]', mode: 'info', danger: true, emptyOk: true, title: '行内 AI 创作', text: '行尾「AI创作」= 基于该账号设定创作全新脚本(弹窗可填选题维度,完成后脚本自动入库到「AI写脚本」)。此处仅讲解。' },
+ ],
+ qa: [
+ ['"把抖音账号「俊希」导入账号列表"', '网页采集账号信息 + 视频列表入库'],
+ ['"更新一下所有关注账号的数据"', '后台逐账号重抓最新数据与视频'],
+ ],
+ },
+ {
+ key: 'account', icon: 'image', title: '账号详情', route: firstAccountRoute,
+ desc: '单账号档案:账号信息 + 账号设定(人设卡)+ 视频列表(趋势图)+ 数据分析',
+ steps: [
+ { sel: '.tabs [data-tab="persona"]', mode: 'click', emptyOk: true, title: '切到视频列表', text: '账号详情顶部三个 tab:账号信息 / 视频列表 / 数据分析。' + '\n' + '👉 点「视频列表」:上方是点赞/评论趋势折线图,下方是该账号全部视频表。' },
+ { sel: '.tabs [data-tab="info"]', mode: 'click', emptyOk: true, title: '账号信息与设定', text: '👉 点回「账号信息」:上半是基础档案(抖音号/粉丝/赛道/AI制作等,抖音号可跳主页),下半是「账号设定」(人设卡,支持卡片版/文字版切换)。' },
+ { sel: '.persona-vs-icons [data-vs="text"]', mode: 'click', emptyOk: true, title: '设定卡片切换', text: '账号设定右上角两个小图标:卡片版(设计感人设卡)/ 文字版(结构化全文)。' + '\n' + '👉 点文字版图标再点回卡片版。' },
+ { sel: '.tabs [data-tab="analysis"]', mode: 'click', emptyOk: true, title: '数据分析', text: '👉 点「数据分析」:账号内容结构拆解与定位诊断(由账号解析产物生成,无数据时为空态)。' },
+ { sel: '[data-rewrite]', mode: 'info', danger: true, emptyOk: true, title: '基于视频 AI 创作', text: '视频列表行尾「AI创作」= 以该视频为选题来源,结合账号设定创作全新脚本。此处仅讲解,不实际触发。' },
+ { sel: '[data-extract]', mode: 'info', danger: true, emptyOk: true, title: '提炼账号设定', text: '账号无设定时可点「提炼账号设定」按钮,AI 基于库中解析数据生成人设卡。此处仅讲解。' },
+ ],
+ qa: [
+ ['"帮我设计一个三农赛道的达人IP"', '提炼账号设定(人设卡)'],
+ ['"分析一下这个账号的内容套路"', '账号内容结构拆解 + 定位诊断'],
+ ],
+ },
+ {
+ key: 'video', icon: 'file-text', title: '视频详情', route: firstVideoRoute,
+ desc: '单视频档案:视频脚本(分镜头表)+ 视频拆解(结构化分析)+ 解析/刷新',
+ steps: [
+ { sel: '.tabs [data-tab="deconstruct"]', mode: 'click', emptyOk: true, title: '视频拆解 tab', text: '视频详情顶部两个 tab:视频脚本(原视频口播/分镜还原)与视频拆解(钩子/节奏/情绪/选题结构化分析)。' + '\n' + '👉 点「视频拆解」查看结构化拆解。' },
+ { sel: '.tabs [data-tab="script"]', mode: 'click', emptyOk: true, title: '视频脚本 tab', text: '👉 点回「视频脚本」:展示该视频的脚本/分镜头表(由 MCP 解析产出)。' },
+ { sel: '[data-parse]', mode: 'info', danger: true, emptyOk: true, title: '解析 / 刷新', text: '「解析」= 对该视频做 MCP 内容解析(约 3-5 分钟,产出脚本+拆解);已解析视频按钮变为「刷新」可覆盖更新。此处仅讲解。' },
+ { sel: '.info-card.video-info', mode: 'info', title: '视频信息卡', text: '页面顶部信息卡:所属账号(可点进账号详情)、视频ID、时长、发布时间与互动数据,右上「查看视频」直接打开抖音原页。' },
+ ],
+ qa: [
+ ['"解析这个抖音链接"', 'MCP 自动解析视频内容'],
+ ['"帮我拆解这个抖音视频"', '解析 + 钩子/节奏/情绪/选题拆解'],
+ ],
+ },
+ {
+ key: 'rewrites', icon: 'pen', title: 'AI写脚本', route: '#/rewrites',
+ desc: '脚本中心:参考选题创作(有原视频)/ 自定义选题创作(无原视频)/ ABC 融合创作',
+ steps: [
+ { sel: '.rw-tabs [data-tab="custom"]', mode: 'click', title: '自定义选题创作', text: 'AI写脚本 分三种创作入口,先看「自定义选题创作」:无原视频的脚本(来自账号列表/详情 AI创作弹窗)都收在这里,带徽标计数。' + '\n' + '👉 点「自定义选题创作」tab。' },
+ { sel: '.rw-tabs [data-tab="ref"]', mode: 'click', title: '参考选题创作', text: '「参考选题创作」:以库中视频为选题来源(关注账号优先、按发布时间倒序),每行视频可「AI创作」新脚本,完成后自动挂到该视频下。' + '\n' + '👉 点回「参考选题创作」。' },
+ { sel: '#rwSearch', mode: 'input', emptyOk: true, title: '搜索视频', text: '按视频标题 / 账号名搜索候选视频,配时间范围与「已解析/未解析」状态过滤。' + '\n' + '👉 输入关键词回车试试,或直接下一步。' },
+ { sel: '.rw-tabs [data-tab="abc"]', mode: 'info', title: 'ABC 融合创作', text: '「ABC融合创作」:将同账号多条视频的爆款元素融合成一条全新脚本(功能开发中,暂为占位说明页)。' },
+ { sel: '[data-rewrite]', mode: 'info', danger: true, emptyOk: true, title: '行内 AI 创作', text: '视频行尾「AI创作」:读取该账号人设 + 视频选题 → 创作全新故事脚本 → 自动入库挂到该视频下。此处仅讲解,不实际触发。' },
+ { sel: '[data-detail]', mode: 'info', danger: true, emptyOk: true, title: '查看脚本详情', text: '行内「N 查看」打开脚本详情弹窗:左侧源脚本(可折叠)+ AI脚本(多版本切换),右侧诊断评分 / 分镜提示词。此处仅讲解。' },
+ ],
+ qa: [
+ ['"基于「旧梦留声机」账号设定,替换事件写一个脚本"', '读账号设定,按账号套路写全新事件脚本'],
+ ['"把刚写的脚本展开成分镜"', '按分镜技能生成镜头级描述'],
+ ],
+ },
+ {
+ key: 'reviews', icon: 'flask', title: '脚本诊断', route: '#/reviews',
+ desc: '复盘评分中心:AI 脚本质量评分表 + 复盘任务 + 完整诊断报告',
+ steps: [
+ { sel: '.table thead', mode: 'info', title: '评分表结构', text: '列表展示全部 AI 脚本的复盘状态:每行含 生成方式(参考选题/输入选题)、脚本标题、以及各维度得分(创意看点/设定承接/框架节奏/爆款开场/叙事逻辑/极致事件/…/总分)。' },
+ { sel: '[data-open]', mode: 'info', danger: true, emptyOk: true, title: '打开脚本详情', text: '点脚本标题打开脚本详情弹窗(右侧默认收起诊断,可展开看该版本的评分与问题清单)。此处仅讲解。' },
+ { sel: '[data-review-row]', mode: 'info', danger: true, emptyOk: true, title: '发起复盘', text: '行尾「复盘 / 更新」按钮:对脚本发起评分式复盘(有原视频时含 AI/源/对比三模式,无则独立 10 维评分),后台任务数分钟完成。此处仅讲解。' },
+ { sel: '[data-review]', mode: 'info', danger: true, emptyOk: true, title: '完整复盘报告', text: '点「总分」数字可打开完整复盘报告:各维得分条 + 问题清单(高/中/低)+ 修改建议。此处仅讲解。' },
+ ],
+ qa: [
+ ['"帮我诊断这个脚本"', '评分式复盘:各维得分 + 问题清单 + 修改建议'],
+ ['"对比 AI脚本 和 原视频脚本"', 'AI 脚本 vs 源脚本对比分析(三段式)'],
+ ],
+ },
+ ];
+
+ const MODES = [
+ { icon: 'zap', name: '自动(默认)', desc: '静默执行全流程,只交付最终脚本' },
+ { icon: 'users', name: '共创', desc: '每个关键节点先确认再继续' },
+ { icon: 'wrench', name: '开发', desc: '每个步骤都落盘保存中间产物' },
+ ];
+
+ /* ============ 帮助页渲染 ============ */
+ function renderHelp() {
+ return `
+
+
+
+ 功能帮助
+
+
+
${svgIcon('help', 26)}
+
+
短视频工作台 · 使用帮助
+
跟着「模拟操作指引」在真实界面上一步步点一遍,比看文字快得多。也可以直接对 AI 说需求(点击问法即复制)。
+
+
+
+ ${BOARDS.map((b) => `
+
+
+ ${svgIcon(b.icon, 20)}
+ ${b.title}
+
+
${b.desc}
+
+
+
+
+
+ 常见问法(点击复制)
+
+ ${(b.qa || []).map(([q, a]) => `
${esc(q)}→ ${esc(a)}
`).join('')}
+
+
+
`).join('')}
+
+
+
三种创作模式(对 AI 说一声即切换)
+
+ ${MODES.map((m) => `| ${svgIcon(m.icon)} ${m.name} | ${m.desc} |
`).join('')}
+
+
例:「用共创模式写一个脚本」「开发模式跑一遍」。
+
+
`;
+ }
+
+ /* ============ 帮助页事件绑定(进入按钮 / 指引按钮 / 问法复制) ============ */
+ function bindHelp(view) {
+ view.querySelectorAll('.qa-row.copyable').forEach((row) => row.addEventListener('click', async () => {
+ const text = (row.dataset.q || '').replace(/^"|"$/g, '');
+ if (text) showToast((await copyText(text)) ? '✅ 已复制:' + text : '❌ 复制失败');
+ }));
+ view.querySelectorAll('[data-guide]').forEach((b) => b.addEventListener('click', () => {
+ startGuide(b.dataset.guide, b);
+ }));
+ view.querySelectorAll('[data-goto]').forEach((b) => b.addEventListener('click', async () => {
+ const board = BOARDS.find((x) => x.key === b.dataset.goto);
+ if (!board) return;
+ let r = board.route;
+ if (typeof r === 'function') { try { r = await r(); } catch (e) { r = null; } }
+ if (!r) {
+ showToast('📌 暂无示例数据,请先在库中添加账号 / 解析视频');
+ return;
+ }
+ location.hash = (typeof r === 'string' && r.startsWith('#')) ? r : '#/';
+ }));
+ }
+
+ /* ============ 模拟操作引导引擎 ============ */
+ let guide = null; // { board, steps, idx, mask, pop, targetEl, clearFns[] }
+
+ const GUIDE_Z = 1100; // 遮罩/气泡层级(modal 为 100,须高于它)
+ const WAIT_MS = 9000; // 步骤目标最长等待
+ const POLL_MS = 180;
+
+ function waitFor(sel, timeout) {
+ return new Promise((resolve) => {
+ const t0 = Date.now();
+ (function poll() {
+ const el = sel ? $(sel) : null;
+ if (el || Date.now() - t0 > timeout) resolve(el || null);
+ else setTimeout(poll, POLL_MS);
+ })();
+ });
+ }
+
+ /* 路由是否已生效(当前 hash 与目标一致,且页面已渲染出内容) */
+ function routedTo(targetHash) {
+ const cur = (location.hash || '#/').split('?')[0];
+ if (cur !== targetHash) return false;
+ const view = $('#view');
+ return !!(view && view.innerHTML && view.innerHTML.length > 200 && !view.querySelector('.view-empty'));
+ }
+
+ async function gotoRoute(targetHash) {
+ if (!targetHash) return false;
+ if (!routedTo(targetHash)) {
+ location.hash = targetHash;
+ const t0 = Date.now();
+ while (Date.now() - t0 < WAIT_MS) {
+ await new Promise((r) => setTimeout(r, POLL_MS));
+ if (routedTo(targetHash)) break;
+ }
+ }
+ return routedTo(targetHash);
+ }
+
+ function cleanupGuide() {
+ if (!guide) return;
+ if (guide.mask && guide.mask.parentNode) guide.mask.parentNode.removeChild(guide.mask);
+ if (guide.pop && guide.pop.parentNode) guide.pop.parentNode.removeChild(guide.pop);
+ if (guide.targetEl) {
+ guide.targetEl.style.outline = '';
+ guide.targetEl.style.boxShadow = '';
+ guide.targetEl.style.zIndex = '';
+ guide.targetEl.style.position = '';
+ guide.targetEl.style.pointerEvents = '';
+ }
+ (guide.clearFns || []).forEach((fn) => { try { fn(); } catch (e) {} });
+ guide = null;
+ }
+
+ /* 定位高亮 + 气泡位置(fixed,相对视口;容器滚动不重算——气泡跟随目标下方固定,超界自动收敛到可视区) */
+ function placePop() {
+ if (!guide || !guide.targetEl || !guide.pop) return;
+ const r = guide.targetEl.getBoundingClientRect();
+ if (r.width === 0 && r.height === 0) return;
+ const pw = guide.pop.offsetWidth || 420;
+ const ph = guide.pop.offsetHeight || 120;
+ let x = Math.min(Math.max(12, r.left + r.width / 2 - pw / 2), window.innerWidth - pw - 12);
+ let y = r.bottom + 14;
+ if (y + ph > window.innerHeight - 12) y = Math.max(12, r.top - ph - 14);
+ guide.pop.style.left = x + 'px';
+ guide.pop.style.top = y + 'px';
+ }
+
+ function highlightTarget(el) {
+ if (!el) return;
+ el.style.outline = '3px solid var(--primary)';
+ el.style.outlineOffset = '2px';
+ el.style.boxShadow = '0 0 0 6px rgba(47,111,237,.25)';
+ // 提升层级使其可点/可见于遮罩之上
+ const pos = getComputedStyle(el).position;
+ if (pos === 'static') el.style.position = 'relative';
+ el.style.zIndex = String(GUIDE_Z + 2);
+ if (guide.step.danger) el.style.pointerEvents = 'none'; // 防误触危险控件
+ guide.targetEl = el;
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
+ }
+
+ /* 渲染气泡并挂一次性交互 */
+ function showStep() {
+ const g = guide;
+ g.step = g.steps[g.idx];
+ g.clearFns.forEach((fn) => { try { fn(); } catch (e) {} });
+ g.clearFns = [];
+ if (g.targetEl) {
+ g.targetEl.style.outline = ''; g.targetEl.style.boxShadow = ''; g.targetEl.style.zIndex = '';
+ g.targetEl.style.position = ''; g.targetEl.style.pointerEvents = '';
+ g.targetEl = null;
+ }
+ const st = g.step;
+ const total = g.steps.length;
+ const isLast = g.idx >= total - 1;
+
+ const needClick = st.mode === 'click';
+ const needInput = st.mode === 'input';
+ const actionHint = needClick ? '👉 点击上面高亮的控件体验(点对自动下一步)'
+ : needInput ? '👉 在上面高亮输入框输入后按 回车(或直接下一步)'
+ : (st.danger ? '⚠️ 该操作有副作用,此处仅讲解,请勿点击' : '');
+
+ g.pop.innerHTML = `
+
${esc(g.board.title)}${g.idx + 1}/${total}
+
${esc(st.title || '')}
+
${esc(st.text || '')}
+ ${actionHint ? `
${esc(actionHint)}
` : ''}
+
+
+
+
`;
+ g.pop.style.display = 'block';
+ g.mask.style.display = 'block';
+
+ // 按钮
+ const prevBtn = g.pop.querySelector('[data-g-prev]');
+ if (prevBtn) prevBtn.addEventListener('click', () => { if (g.idx > 0) { g.idx--; showStep(); } });
+ g.pop.querySelector('[data-g-next]').addEventListener('click', () => {
+ if (isLast) return finishGuide();
+ g.idx++; showStep();
+ });
+
+ // 找目标控件
+ (async () => {
+ const el = await waitFor(st.sel, WAIT_MS);
+ if (!guide || guide !== g) return; // 已退出
+ if (!el) {
+ if (st.emptyOk) {
+ // 目标缺失但允许纯文字展示
+ g.pop.querySelector('.g-hint')?.remove();
+ placePop();
+ } else {
+ // 自动跳到下一步
+ if (g.idx < total - 1) { g.idx++; showStep(); }
+ else finishGuide();
+ }
+ return;
+ }
+ highlightTarget(el);
+ // 目标在弹窗(.modal-mask)内时隐藏全屏遮罩——modal z-index(100) < 遮罩(1100),留着会挡住弹窗操作
+ const inModal = !!el.closest('.modal-mask');
+ g.mask.style.display = inModal ? 'none' : 'block';
+ if (needClick && !st.danger) {
+ const fn = () => { if (guide === g) { if (g.idx < total - 1) { g.idx++; showStep(); } else finishGuide(); } };
+ el.addEventListener('click', fn, { once: true });
+ g.clearFns.push(() => el.removeEventListener('click', fn));
+ } else if (needInput) {
+ const fn = (e) => { if (e.key === 'Enter' && guide === g) { if (g.idx < total - 1) { g.idx++; showStep(); } else finishGuide(); } };
+ el.addEventListener('keydown', fn);
+ g.clearFns.push(() => el.removeEventListener('keydown', fn));
+ }
+ placePop();
+ // 页面内容可能异步替换(如切换 tab 重渲染),延迟后重定位一次
+ setTimeout(placePop, 500);
+ })();
+ }
+
+ function buildOverlay() {
+ cleanupGuide();
+ const mask = document.createElement('div');
+ mask.className = 'g-mask';
+ mask.style.zIndex = String(GUIDE_Z);
+ document.body.appendChild(mask);
+ const pop = document.createElement('div');
+ pop.className = 'g-pop';
+ pop.style.zIndex = String(GUIDE_Z + 1);
+ document.body.appendChild(pop);
+ guide.mask = mask;
+ guide.pop = pop;
+ // 点遮罩非高亮区 = 忽略(仅提示)
+ mask.addEventListener('click', () => showToast('请按气泡指引操作:点击高亮控件或气泡按钮'));
+ // Esc 随时退出引导
+ const escFn = (e) => { if (e.key === 'Escape') finishGuide(); };
+ document.addEventListener('keydown', escFn);
+ guide.clearFns.push(() => document.removeEventListener('keydown', escFn));
+ }
+
+ function finishGuide() {
+ const name = guide ? guide.board.title : '';
+ cleanupGuide();
+ showToast('🎉 已看完「' + name + '」指引,可自行继续操作');
+ }
+
+ /* 入口:boardKey 或 {route,title,steps} */
+ async function startGuide(boardKey, btn) {
+ const board = BOARDS.find((b) => b.key === boardKey);
+ if (!board) { showToast('❌ 未找到该板块指引'); return; }
+ if (guide) cleanupGuide();
+ // 详情类板块:先解析示例路由(取库中第一条)
+ let route = board.route;
+ if (typeof route === 'function') {
+ try { route = await route(); } catch (e) { route = null; }
+ }
+ if (typeof route === 'string' && !route.startsWith('#')) route = null;
+
+ if (!route) {
+ const needFirst = board.key === 'account' ? '还没有账号数据——请先到「账号列表」添加一个账号,再回来体验账号详情指引' : '还没有视频数据——请先在「热点数据/账号列表」解析或添加视频,再回来体验视频详情指引';
+ showToast('📌 ' + needFirst);
+ if (board.key === 'account') location.hash = '#/accounts';
+ else if (board.key === 'video') location.hash = '#/accounts';
+ return;
+ }
+
+ // 跳转 + 等渲染
+ const ok = await gotoRoute(route);
+ if (!ok) { showToast('❌ 页面加载超时,请重试'); return; }
+
+ guide = { board, steps: board.steps, idx: 0, clearFns: [] };
+ buildOverlay();
+ showStep();
+ }
+
+ /* 暴露到全局:renderHelp/bindHelp/startGuide/BOARDS(供 app.js 路由调用) */
+ window.HelpGuide = { renderHelp, bindHelp, startGuide, BOARDS };
+})();
diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
index 054c94d..085a155 100644
--- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
+++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html
@@ -4,7 +4,7 @@
短视频工作台
-
+
@@ -41,7 +41,8 @@