From 7f16eda57f466fa304679abb0947309b76d5f181 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Thu, 3 Sep 2026 17:27:20 +0800 Subject: [PATCH] =?UTF-8?q?R34:=20=E5=B7=A5=E4=BD=9C=E5=8F=B0=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E8=AF=A6=E6=83=85=E7=BB=9F=E4=B8=80=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=EF=BC=88=E4=B8=89=E5=85=A5=E5=8F=A3=E5=85=B1=E7=94=A8+?= =?UTF-8?q?=E5=8F=B3=E4=BE=A7=E8=AF=8A=E6=96=AD/=E5=88=86=E9=95=9Ctab?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E8=AF=8A=E6=96=AD+tab=E6=96=87=E5=AD=97?= =?UTF-8?q?=E5=8F=98=E8=89=B2+=E7=A9=BA=E6=80=81=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=BB=9F=E4=B8=80btn-view=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 三处入口(首页AI脚本卡/AI写脚本「N查看」/脚本诊断页标题)共用 openScriptModal 顶层组件 - 合并两套重复弹窗代码(openRewriteCompare/openScriptDetail) - 右侧列头部两 tab「脚本诊断|分镜提示词」默认显示脚本诊断 - 后端数据装配:ref 走 video-script+video-detail;custom 走 custom-script(失败回退 rewrites 全表定位);诊断按 review-full(ai||compare);分镜按 rewrite_id 归属过滤 - 分镜边界:仅 ref 视频支持(custom 无视频载体);无源 → 独立分析复盘;三模式复盘(AI/源/对比) - 报告完整复盘:reportFullReview 抽全局(跨页复用) - 场次滚动联动:仅分镜态生效(AI列滚动 → 分镜列对齐场块) - 样式打磨:tab 去掉深蓝底色(active 文字变色+加粗);空态开始复盘/生成分镜统一 btn btn-sm btn-view - CDP 冒烟 tmp/cdp-r34-modal-smoke.mjs 16/16 PASS(JS=v/w 两次) --- .workbuddy/memory/2026-09-03.md | 29 ++ .../V1.0/mcn-work-shop/public/app.js | 6 + .../V1.0/mcn-work-shop/public/data-pages.js | 443 ++++++++++-------- .../V1.0/mcn-work-shop/public/index.html | 6 +- .../V1.0/mcn-work-shop/public/style.css | 6 + 5 files changed, 293 insertions(+), 197 deletions(-) diff --git a/.workbuddy/memory/2026-09-03.md b/.workbuddy/memory/2026-09-03.md index 494591a..29b6274 100644 --- a/.workbuddy/memory/2026-09-03.md +++ b/.workbuddy/memory/2026-09-03.md @@ -290,3 +290,32 @@ - **验证**:node --check 3 JS ✓;curl --noproxy GET 状态正常(files=73/lastUpdateAgeMin=124/task=null);POST 空 prompt 400;CDP 冒烟 `tmp/cdp-rank-update-smoke.mjs`(9225)纯图标版 9 项全 PASS(text='' + svg + title + modal 标题/三榜范围/上次更新/按钮文案/取消) - **服务/工具坑(重踩)**:nohup+& 后台 node 随 bash 退出被回收 → 须 Bash run_in_background 直接托管前台;taskkill //PID Git Bash 语法错 → PowerShell Stop-Process -Force(返回 exit 1 但端口已释放);curl 全局 socks 代理 502 → 必须 --noproxy '*' - **待实测**:首次真实点击任务链路(调度拾取→技能会话→抓取执行→落盘→页面重建出最新期) + +## R34 工作台「脚本详情统一弹窗」三入口共用(09-03 晚间) +- **需求**:首页 AI脚本卡 / AI写脚本「N 查看」/ 脚本诊断页标题 三处点击后展示同一弹窗;右侧内容由「AI分镜提示词」改为可切换「脚本诊断 | 分镜提示词」两个 tab,默认显示「脚本诊断」(用户指定) +- **范围**:用户选「三处入口统一升级 (Recommended)」——合并两套重复弹窗代码(`openRewriteCompare` AI写脚本页 / `openScriptDetail` 诊断页)+ 首页 `openRecentScript` 接入 +- **顶层组件** `openScriptModal({ rewriteId, videoId })`(data-pages.js 取代两旧闭包): + - 数据装配:`vid>0` ref 走 `Promise.all([/api/dsh/video-script?videoId, /api/dsh/video-detail?videoId])`;`vid===0` custom 走 `/api/dsh/custom-script?id`(拒绝时回退 `/api/dsh/rewrites` 全表定位,若挂 video 则递归转 ref 形态);全空 → toast「该脚本不存在或已删除」 + - 状态:`stab='diag'`(默认诊断)、`showSrc`、`sbIdx`、`verIdx`;`visibleSbOf(curId)` 按 rewrite_id 归属过滤 + - 诊断口径:`review-full` 的 ai 或 compare 取一(compare 取 AI 视角 dims/total 与源对比分);11 维 DIAG_DIMS;无诊断 → 「该版本 AI 脚本尚未诊断」+ 开始复盘按钮 + - 分镜边界:仅 ref 有(storyboard_log 需 video_id);custom 无视频载体 → 空态「该脚本为自定义选题(无关联视频),暂不支持分镜提示词」+ 无生成按钮(禁止 confirmRun 空视频) + - 复盘触发:有源 → 三模式复盘(AI/源/对比);无源 → 独立分析仅评 AI 脚本(与诊断页行按钮同文案) + - 完整报告:`reportFullReview(reviewId)` 顶层函数副本(原 openReviewDetail 弹窗逻辑抽全局,跨页复用) + - 场次滚动联动:仅 `stab==='sb'` 且有分镜时(AI 列滚动 → 分镜列对齐场块,分镜列不反向联动),sideSec 用 `.rw-side-tabs` 存在判定、aiSec 用 head 文本 startsWith('AI脚本') 判定 +- **删除旧闭包**:`openRewriteCompare`(含内嵌 renderSbBlocks)+ `openScriptDetail`(node -e 锚点切割,各 -7139/-6799 字符);衔接点 grep 验证零残留 renderSbBlocks/openRewriteCompare/openScriptDetail 引用 +- **app.js 首页接入**:`openRecentScript(s)` 优先 `window.DataPages.openScriptModal({ rewriteId: s.id, videoId: s.video_id })`;原单列全文弹窗降级兜底(data-pages.js 未就绪时) +- **style.css 新增**:`.rw-side-head`/`.rw-side-tabs`/`.rw-side-tab`(pill tab 容器,默认透明) +- **版本**:index.html ?v=20260903u→v(CSS 有 tab 底色修复须强制刷) +- **CDP 冒烟** `tmp/cdp-r34-modal-smoke.mjs`(9225):reload ignoreCache→首页卡点击 → 弹窗三列 / 两 tab / 默认诊断 / AI列存在 / 源收起 / 诊断渲染 / 切分镜 / 分镜空态 / 关闭 / 诊断页入口同弹窗 / AI写脚本查看同弹窗;JS 版本=20260903v 时 **16 PASS 全绿** +- **CSS tab 底色修复(09-03 末)**:用户反馈「切换内容的按钮不要用深蓝色底色,文字变色就行」→ `.rw-side-tab.active` 去 `background: var(--primary); color: #fff;`,改 `color: var(--primary); font-weight: 600`;hover 同步去 rgba 浅蓝底;CDP 计算样式验证:active `bg=rgba(0,0,0,0) color=rgb(47,111,237) weight=600`,符合「文字变色就行」 +- **冒烟断言修复(关键)**:初版 C2 断言 `!![data-vnext] || !!.rw-sec-count` 对 videoId=185(仅 1 个 rewrite id=20,单版本)误判 FAIL——组件按 `rewrites.length>1` 才渲染版本切换控件,单版本无控件是正确行为;改为二分合法态:AI列正文渲染 + (无版本区 = 单版本合法) / (有 count = 验证「版本 x/y」格式);重跑全绿 +- **代码删除纪律再踩(教训固化)**:同文件多 Edit 并行执行会互相覆盖(本次 3 处 index.html 版本 bump 并行,只存活 app.js 一处)——**同文件 Edit 必须串行**,并行 Edit 仅适用于不同文件 + +## R34 样式打磨(09-03 末,w 版) +- **tab 底色修复**(用户反馈):切换内容的按钮不要用深蓝色的底色,文字变色就行 → `.rw-side-tab.active` 去 `background: var(--primary); color: #fff;`,改 `color: var(--primary); font-weight: 600`;hover 同步去 rgba 浅蓝底;CDP 计算样式验证:active `bg=rgba(0,0,0,0) color=rgb(47,111,237) weight=600` +- **空态主操作按钮统一**(用户反馈):开始复盘 + 生成分镜 两按钮「一个样式」 → 统一 `btn btn-sm btn-view`(白底蓝字蓝边,复用弹窗主操作家族「展开源脚本」「开始复盘」原有风格);CDP 实测两按钮计算样式完全一致 +- **版本**:data-pages.js ?v=20260903v→w(CSS 没改保持 v,app.js 保持 v) +- **冒烟回归**:w 资源下 `tmp/cdp-r34-modal-smoke.mjs` 16/16 PASS 无回归 + +## R34 同步 dsh 副本计划 +- dsh 副本 `C:\Users\maidou\.dsh\skills\short-video-script\`(独立副本,非软链)—— R34 改动 data-pages.js/app.js/style.css/index.html 同步评估后人工复制 diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js index ea300bf..69af401 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/app.js @@ -561,6 +561,12 @@ function setRankBoard(board, activeEl) { loadRankingPreview(); } function openRecentScript(s) { + // R34:首页「AI脚本」卡点击 → 打开与 AI写脚本页查看一致的三列统一弹窗(源脚本可折叠 | AI脚本版本 | 右侧 脚本诊断/分镜提示词 tab,默认脚本诊断) + if (window.DataPages && typeof window.DataPages.openScriptModal === 'function') { + window.DataPages.openScriptModal({ rewriteId: Number(s.id) || 0, videoId: Number(s.video_id) || 0 }); + return; + } + // 兜底(data-pages.js 未就绪时):单列全文弹窗 const title = cleanTitle(s.title || s.video_title) || 'AI 写剧本'; const mask = document.createElement('div'); mask.className = 'modal-mask'; mask.innerHTML = ``; bindBack(view); bindPager(view, (p) => { page = p; render(); }); - view.querySelectorAll('[data-open]').forEach((a) => a.addEventListener('click', () => openScriptDetail(a.dataset.open))); + view.querySelectorAll('[data-open]').forEach((a) => a.addEventListener('click', () => { /* R34:统一脚本详情弹窗(右侧 tab 脚本诊断/分镜,默认诊断) */ + const row = items.find((x) => String(x.id) === String(a.dataset.open)); + if (row) openScriptModal({ rewriteId: row.id, videoId: Number(row.video_id) || 0 }); + })); view.querySelectorAll('[data-review]').forEach((a) => a.addEventListener('click', () => openReviewDetail(a.dataset.review))); view.querySelectorAll('[data-review-row]').forEach((b) => b.addEventListener('click', () => { const row = items.find((x) => String(x.id) === String(b.dataset.reviewRow)); @@ -1640,88 +1534,6 @@ async function renderReviews(view) { } } - async function openScriptDetail(rewriteId) { - // 脚本详情弹窗:样式对齐「AI写脚本」查看弹窗(rw-stack 三列)——源脚本(可折叠) | AI脚本(版本切换) | AI诊断(复盘结果) - const row = items.find((x) => String(x.id) === String(rewriteId)); - if (!row) return; - const d2 = await api('/api/dsh/video-script?videoId=' + row.video_id).catch(() => ({ ok: false })); - const rewrites = (d2.rewrites || []); - const hasSource = !!(d2.source && String(d2.source).trim()); - let verIdx = Math.max(0, rewrites.findIndex((x) => x.id === row.id)); - let showSrc = false; // 源脚本列默认收起(与查看弹窗一致) - const DIMS_ALL = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他']; - const scoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f'); - // 诊断内容 HTML(取 AI 视角评分:compare 类型取 ai 侧 dims/total 与源对比结论,列表总分口径一致) - const diagBodyHtml = (dRow) => { - const rv = reviewOf(dRow); - if (!rv) return ''; - let dims = {}, total = NaN; - try { const j = JSON.parse(rv.dims_json || '{}'); dims = rv.type === 'compare' ? (j.ai || {}) : j; } catch (e) {} - try { const t = JSON.parse(rv.total || '{}'); total = rv.type === 'compare' ? Number(t.ai) : Number(rv.total); } catch (e) { total = Number(rv.total); } - const srcTotal = (() => { try { const t = JSON.parse(rv.total || '{}'); return rv.type === 'compare' ? Number(t.source) : NaN; } catch (e) { return NaN; } })(); - const scoreBar = DIMS_ALL.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `
${esc(k)}
${has ? v : '—'}
`; }).join(''); - let html = `
AI 总分 ${fmtScore(Number.isFinite(total) ? total : rv.total)}${rv.type === 'compare' && Number.isFinite(srcTotal) ? `(对比源脚本 ${fmtScore(srcTotal)})` : ''}${rv.verdict ? ` · ${esc(rv.verdict)}` : ''}
${scoreBar}
`; - let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {} - if (issues.length) html += `
问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`; - if (rv.summary) html += `
综合评语:${esc(rv.summary)}
`; - html += `
查看完整复盘报告 →
`; - return html; - }; - const diagEmptyHtml = (dRow) => (dRow && dRow.review_running) - ? `
复盘运行中…(完成后重新打开本弹窗即可查看评分)
` - : `
该版本 AI 脚本尚未诊断
-
`; - - function renderModal() { - const curRow = rewrites.length ? rewrites[Math.min(verIdx, rewrites.length - 1)] : null; - const dRow = curRow ? (items.find((x) => String(x.id) === String(curRow.id)) || curRow) : null; // items 行带 review_ai/compare;兜底 raw 行(无诊断) - const diag = dRow ? reviewOf(dRow) : null; - const srcSec = hasSource - ? `
-
源脚本
-
${renderMarkdown(d2.source)}
-
-
` : ''; - const srcToggle = hasSource ? `` : ''; - const verCtrl = (() => { - const len = rewrites.length; - if (len <= 1) return `${len === 1 ? '版本 1/1' : ''}`; - return `版本 ${verIdx + 1}/${len}`; - })(); - const running = !!(dRow && dRow.review_running); - const diagFoot = diag - ? `
${running ? '复盘运行中…' : ''}
` - : '
'; - const mask = document.createElement('div'); mask.className = 'modal-mask'; - mask.innerHTML = ``; - document.body.appendChild(mask); - mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); }); - mask.querySelector('[data-x]').addEventListener('click', () => mask.remove()); - const q = (s) => mask.querySelector(s); - const srcT = q('[data-src-toggle]'); if (srcT) srcT.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); }); - const vp = q('[data-vprev]'); if (vp) vp.addEventListener('click', () => { if (verIdx > 0) { verIdx--; mask.remove(); renderModal(); } }); - const vn = q('[data-vnext]'); if (vn) vn.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; mask.remove(); renderModal(); } }); - const diagBtn = q('[data-diag-review]'); - if (diagBtn) diagBtn.addEventListener('click', () => { const t = dRow; if (t) startReview(t, diagBtn); }); - const full = q('[data-review]'); if (full) full.addEventListener('click', () => openReviewDetail(full.dataset.review)); - } - renderModal(); - } async function openReviewDetail(reviewId) { const d3 = await api('/api/dsh/review-detail?id=' + reviewId).catch(() => ({ ok: false })); @@ -1755,4 +1567,247 @@ async function renderReviews(view) { } } -window.DataPages = { renderRanking, renderAccounts, renderAccountDetail, renderVideoDetail, renderRewrites, renderReviews }; +/* ================= R34 统一「脚本详情」弹窗(首页 AI脚本卡 / AI写脚本「N 查看」/ 脚本诊断 标题 三入口共用) ================= + 布局同 AI写脚本查看弹窗(rw-stack 横向列):源脚本(可折叠,有原视频时) | AI脚本(版本切换) | 右侧列 + 右侧列头部 tab:脚本诊断(默认) | 分镜提示词 + —— 诊断内容 = 当前版本 AI 脚本最新复盘(script_review type=ai 优先;compare 取 AI 视角总分并带源对比分) + —— 分镜内容 = 该视频下 storyboard_log 中属当前版本的记录 + 生成/重新生成入口(仅参考选题脚本;自定义选题无视频载体不支持) + 数据源:/api/dsh/video-script + /api/dsh/video-detail(参考选题)/ /api/dsh/custom-script(自定义选题) + + /api/dsh/review-full?rewriteId=(当前版本诊断)+ /api/dsh/storyboards?videoId=(分镜) */ +const DIAG_DIMS = ['设定承接', '框架节奏', '钩子设置', '叙事逻辑', '极致事件', '共鸣和共情', '格式规范性', '台词口语化', '内容合规', '教程完整链', '其他']; +const diagScoreColor = (v) => (v >= 75 ? '#3a9e6d' : v >= 60 ? '#d99a2b' : '#d9534f'); +const diagFmtScore = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : '-'); + +/* 分镜文本渲染:按「段 N(起-止 秒)|标题」分块(继承原 AI写脚本查看弹窗逻辑;分镜时间戳分段格式) */ +function renderSbBlocks(text) { + const blocks = String(text || '').split(/\n\s*---+\s*\n/).map((b) => b.trim()).filter(Boolean); + if (blocks.length <= 1) return renderMarkdown(text || ''); + return blocks.map((b, bi) => { + const lines = b.split('\n'); const first = lines[0].trim(); + const m = /^段\s*(\d+)\s*[((]\s*([\d.]+)\s*-\s*([\d.]+)\s*秒\s*[))]\s*[||]\s*(.+)$/.exec(first); + let head = m ? `${esc(m[4].trim())}${esc(m[2] + ' - ' + m[3] + '秒')}` : `场${bi + 1}`; + const content = m ? lines.slice(1).join('\n').trim() : b; + return `
${head}
${renderMarkdown(content)}
`; + }).join(''); +} + +/* 完整复盘报告弹窗(组件内副本:完整报告入口跨页复用;样式与脚本诊断页 openReviewDetail 一致) */ +async function reportFullReview(reviewId) { + const d3 = await api('/api/dsh/review-detail?id=' + reviewId).catch(() => ({ ok: false })); + if (!d3.ok) { showToast('复盘详情加载失败'); return; } + 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 = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `
${esc(k)}
${has ? v : '—'}
`; }).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 = `
AI 总分 ${diagFmtScore(Number(t.ai))}
${scoreBar(aiD, 'c-ai-')}
源脚本 总分 ${diagFmtScore(Number(t.source))}
${scoreBar(srcD, 'c-src-')}
`; + } else { + body = `
总分 ${diagFmtScore(Number(rv.total))}
${scoreBar(dimsObj, 'd-')}
`; + } + let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {} + if (issues.length) body += `
问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`; + if (rv.summary) body += `
综合评语:${esc(rv.summary)}
`; + if (rv.content) body += `
${renderMarkdown(rv.content)}
`; + const mask = document.createElement('div'); mask.className = 'modal-mask'; + mask.innerHTML = ``; + document.body.appendChild(mask); + mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); }); + mask.querySelector('[data-x]').addEventListener('click', () => mask.remove()); +} + +async function openScriptModal({ rewriteId, videoId } = {}) { + rewriteId = Number(rewriteId) > 0 ? Number(rewriteId) : 0; + const vid = Number(videoId) > 0 ? Number(videoId) : 0; + /* ---- 数据装配:版本列表 + 源脚本 + 标题 meta ---- */ + let rewrites = [], source = null, metaTitle = '', metaAcc = ''; + if (vid > 0) { /* 参考选题:以视频为容器,展示该视频下全部 AI 脚本版本(含源脚本) */ + const [vs, vd] = await Promise.all([ + api('/api/dsh/video-script?videoId=' + vid).catch(() => ({ ok: false, rewrites: [], source: null })), + api('/api/dsh/video-detail?videoId=' + vid).catch(() => ({ ok: false, video: null })), + ]); + rewrites = (vs && vs.rewrites) || []; + source = (vs && vs.source) || null; + if (vd && vd.video) { metaTitle = vd.video.video_title || ''; metaAcc = vd.video.account_name || ''; } + } else if (rewriteId) { /* 自定义选题(video_id 空):单条脚本;custom-script 拒绝则回退全量表定位(该脚本实际挂在视频下) */ + const cs = await api('/api/dsh/custom-script?id=' + rewriteId).catch(() => ({ ok: false, error: 1 })); + if (cs && !cs.error && cs.script) { + rewrites = [{ id: cs.script.id, script_text: cs.script.script_text, created_time: cs.script.created_time }]; + metaTitle = cs.script.title || ''; metaAcc = cs.script.account_name || ''; + } else { + const all = await api('/api/dsh/rewrites').catch(() => ({ items: [] })); + const row = ((all && all.items) || []).find((x) => String(x.id) === String(rewriteId)); + if (row && Number(row.video_id) > 0) { openScriptModal({ rewriteId, videoId: row.video_id }); return; } + if (row) { rewrites = [{ id: row.id, script_text: '', created_time: row.created_time }]; metaTitle = row.title || ''; } + } + } + 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; + const title = metaTitle || 'AI 脚本'; + const hasSource = vid > 0 && !!source; + let stab = 'diag'; /* 右侧 tab:diag=脚本诊断(默认)/ sb=分镜提示词 */ + let showSrc = false, sbIdx = 0; + const stb = vid > 0 ? await api('/api/dsh/storyboards?videoId=' + vid).catch(() => ({ items: [] })) : { items: [] }; + const storyboards = ((stb && stb.items) || []); + const visibleSbOf = (curId) => storyboards.filter((s) => s.rewrite_id == null || String(s.rewrite_id) === String(curId)); + + /* 复盘任务触发:有源脚本 → 三模式(AI/源/对比);无源 → 独立分析仅评 AI 脚本(同脚本诊断页行按钮口径) */ + const fireReview = (btn) => { + if (btn.dataset.lock) return; btn.dataset.lock = '1'; + try { + const [desc, prompt] = hasSource + ? ['将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式)。', + `${SKILL_HINT_REVIEW}\n请对这条 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式)。`] + : ['将对该 AI 脚本做质量复盘评分(该脚本无原视频脚本,仅评 AI 脚本:独立分析模式·10 维评分制)。', + `${SKILL_HINT_REVIEW}\n请对这条 AI 脚本做质量复盘评分(独立分析模式·10 维评分制)。该脚本无原视频脚本,只评 AI 脚本本身,不做源脚本复盘、不做对比分析。`]; + confirmRun({ title: hasSource ? '复盘评分' : 'AI脚本复盘', desc, prompt, label: '复盘指令', skills: ['脚本复盘'], btn }); + } finally { delete btn.dataset.lock; } + }; + /* 生成 / 重新生成分镜(仅参考选题脚本;自定义选题无视频载体不提供) */ + const fireGenSb = (btn, regenerate) => { + confirmRun({ + title: regenerate ? '重新生成分镜' : '生成分镜', + desc: '将基于该 AI 脚本(AI 创作的故事脚本),展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。', + target: `《${title}》`, + prompt: `${SKILL_HINT_STORYBOARD}\n请将视频《${title}》的 AI 脚本${regenerate ? '重新' : ''}展开成 AI 分镜提示词(F4 生成分镜,镜头级描述)。`, + label: '分镜指令', + skills: ['短视频分镜脚本'], + btn, + }); + }; + /* 当前版本诊断(review-full 仅返回已完成复盘;无结果 → null → 空态 + 开始复盘) */ + const diagOf = async (cur) => { + const rf = await api('/api/dsh/review-full?rewriteId=' + cur.id).catch(() => null); + const rv = (rf && (rf.ai || rf.compare)) || null; + if (!rv) return null; + let dims = {}, total = NaN, srcTotal = NaN; + try { const j = JSON.parse(rv.dims_json || '{}'); dims = rv.type === 'compare' ? (j.ai || {}) : j; } catch (e) {} + if (rv.type === 'compare') { try { const t = JSON.parse(rv.total || '{}'); total = Number(t.ai); srcTotal = Number(t.source); } catch (e) { total = Number(rv.total); } } + else { total = Number(rv.total); } + return { rv, dims, total, srcTotal }; + }; + const diagHtml = (dg) => { + const { rv, dims, total, srcTotal } = dg; + const totalFin = Number.isFinite(total); + const scoreBar = DIAG_DIMS.map((k) => { const v = dims[k]; const has = typeof v === 'number' && Number.isFinite(v); return `
${esc(k)}
${has ? v : '—'}
`; }).join(''); + let html = `
AI 总分 ${diagFmtScore(totalFin ? total : Number(rv.total))}${rv.type === 'compare' && Number.isFinite(srcTotal) ? `(对比源脚本 ${diagFmtScore(srcTotal)})` : ''}${rv.verdict ? ` · ${esc(rv.verdict)}` : ''}
${scoreBar}
`; + let issues = []; try { issues = JSON.parse(rv.issues_json || '[]'); } catch (e) {} + if (issues.length) html += `
问题清单
${issues.map((it) => `
${esc(it.severity || '低')}${esc(it.issue || '')}${it.suggestion ? `→ ${esc(it.suggestion)}` : ''}
`).join('')}
`; + if (rv.summary) html += `
综合评语:${esc(rv.summary)}
`; + html += `
查看完整复盘报告 →
`; + return html; + }; + + async function renderModal() { + const cur = rewrites[Math.min(verIdx, rewrites.length - 1)] || rewrites[0]; + if (!cur) return; + const visibleSb = visibleSbOf(cur.id); + const sb = visibleSb[Math.min(sbIdx, visibleSb.length - 1)]; + const dg = await diagOf(cur); + const verCtrl = (p, idx, len, label, genLabel) => { + const inner = len > 1 + ? `${label} ${idx + 1}/${len}` + : (len === 1 ? `${label} 1/1` : ''); + return `
${inner}${genLabel ? `` : ''}
`; + }; + const sideHead = `
+ + +
`; + let sideBody = '', sideFoot = ''; + if (stab === 'diag') { + if (dg) { + sideBody = diagHtml(dg); + sideFoot = `
`; + } else { + sideBody = '
该版本 AI 脚本尚未诊断
'; + sideFoot = `
`; + } + } else if (visibleSb.length) { + sideBody = sb ? renderSbBlocks(sb.script_text) : '
(暂无分镜提示词)
'; + sideFoot = verCtrl('s', sbIdx, visibleSb.length, '分镜', '重新生成'); + } else if (vid > 0) { + sideBody = '
(暂无分镜提示词)
'; + sideFoot = `
`; + } else { + sideBody = '
该脚本为自定义选题(无关联视频),暂不支持分镜提示词
'; + sideFoot = ''; + } + const srcSec = hasSource + ? `
+
源脚本
+
${renderMarkdown(source)}
+
+
` : ''; + const srcToggle = hasSource ? `` : ''; + const mask = document.createElement('div'); mask.className = 'modal-mask'; + mask.innerHTML = ``; + document.body.appendChild(mask); + mask.addEventListener('click', (e) => { if (e.target === mask) mask.remove(); }); + const q = (s) => mask.querySelector(s); + const xb = q('[data-x]'); if (xb) xb.addEventListener('click', () => mask.remove()); + const srcT = q('[data-src-toggle]'); if (srcT) srcT.addEventListener('click', () => { showSrc = !showSrc; mask.remove(); renderModal(); }); + const vp = q('[data-vprev]'); if (vp) vp.addEventListener('click', () => { if (verIdx > 0) { verIdx--; sbIdx = 0; mask.remove(); renderModal(); } }); + const vn = q('[data-vnext]'); if (vn) vn.addEventListener('click', () => { if (verIdx < rewrites.length - 1) { verIdx++; sbIdx = 0; mask.remove(); renderModal(); } }); + mask.querySelectorAll('[data-stab]').forEach((t) => t.addEventListener('click', () => { if (stab !== t.dataset.stab) { stab = t.dataset.stab; mask.remove(); renderModal(); } })); + const sp = q('[data-sprev]'); if (sp) sp.addEventListener('click', () => { if (sbIdx > 0) { sbIdx--; mask.remove(); renderModal(); } }); + const sn = q('[data-snext]'); if (sn) sn.addEventListener('click', () => { if (sbIdx < visibleSb.length - 1) { sbIdx++; mask.remove(); renderModal(); } }); + const genSb = q('[data-gen-sb]'); if (genSb) genSb.addEventListener('click', () => fireGenSb(genSb, false)); + const sgen = q('[data-sgen]'); if (sgen) sgen.addEventListener('click', () => fireGenSb(sgen, true)); + const dgBtn = q('[data-diag-review]'); if (dgBtn) dgBtn.addEventListener('click', () => fireReview(dgBtn)); + const full = q('[data-review]'); if (full) full.addEventListener('click', () => reportFullReview(full.dataset.review)); + /* AI脚本列滚动 ↔ 分镜列场次对齐(仅右侧为「分镜提示词」且有分镜时生效;分镜列滚动不反向联动) */ + if (stab === 'sb' && visibleSb.length) { + const secs = [...mask.querySelectorAll('.rw-stack > .rw-sec')]; + const sideSec = secs.find((s2) => s2.querySelector('.rw-side-tabs')); + const aiSec = secs.find((s2) => s2 !== sideSec && (s2.querySelector('.rw-sec-head') || {}).textContent && (s2.querySelector('.rw-sec-head') || {}).textContent.trim().startsWith('AI脚本')); + const aiBody = aiSec && aiSec.querySelector('.rw-sec-body'); + const sbBody = sideSec && sideSec.querySelector('.rw-sec-body'); + if (aiBody && sbBody) { + const syncSb = () => { + const heads = [...aiBody.querySelectorAll('strong, h1, h2, h3, h4')].filter((el) => /场\s*\d/.test(el.textContent)); + if (!heads.length) return; + const aiTop = aiBody.getBoundingClientRect().top; + const limit = aiBody.scrollTop + 60; /* 视口上缘往下 60px 内最后一个场标题 = 当前场 */ + let curH = heads[0]; + for (const h of heads) { + const t = h.getBoundingClientRect().top - aiTop + aiBody.scrollTop; + if (t <= limit) curH = h; else break; + } + const m = /场\s*(\d+)/.exec(curH.textContent); + if (!m) return; + const target = [...sbBody.querySelectorAll('.sb-block')].find((b) => new RegExp('场' + m[1] + '(?![0-9])').test((b.querySelector('.sb-head') || {}).textContent || '')); + if (target) { + const t = target.getBoundingClientRect().top - sbBody.getBoundingClientRect().top + sbBody.scrollTop; + if (Math.abs(sbBody.scrollTop - t) > 1) sbBody.scrollTop = t; /* 位置相同不重复设置,避免事件环 */ + } + }; + aiBody.addEventListener('scroll', syncSb, { passive: true }); + syncSb(); /* 打开弹窗时先对齐一次 */ + } + } + } + renderModal(); +} + +window.DataPages = { renderRanking, renderAccounts, renderAccountDetail, renderVideoDetail, renderRewrites, renderReviews, openScriptModal }; diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html index 70eb626..bc85093 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 @@ MCN工作台 - + @@ -41,7 +41,7 @@ - - + + diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css index 9e2b573..b78c814 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/style.css @@ -557,6 +557,12 @@ td.td-title { max-width: 240px; } .rw-sec-foot { flex: none; min-height: 46px; display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 8px 12px; border-top: 1px solid var(--border); } /* min-height 保证空 foot(源脚本列占位)与其他列等高,内容区一致 */ .rw-sec-count { margin-right: auto; color: var(--dim); font-size: 14px; } .rw-sec-hidden { display: none; } /* 源脚本列默认收起 */ +/* R34:脚本详情弹窗 右侧列头部 tab(脚本诊断 / 分镜提示词 切换) */ +.rw-side-head { padding: 4px 6px; gap: 0; } +.rw-side-tabs { display: inline-flex; gap: 2px; align-items: center; } +.rw-side-tab { border: none; background: transparent; color: var(--dim); font-size: 14px; font-weight: 500; padding: 5px 12px; border-radius: 6px; cursor: pointer; line-height: 1.4; transition: color .15s; } +.rw-side-tab:hover { color: var(--primary); } +.rw-side-tab.active { color: var(--primary); font-weight: 600; } .src-on { background: #e8f1fd; border-color: #4a90e2; color: #2f6fed; } /* 源脚本展开态高亮 */ .rw-scroll { padding: 4px 2px; max-height: 75vh; overflow: auto; } .sb-block { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; background: var(--panel); }