Files
dsh_mcn_agent/profiles/web/node_modules/dsh-plugin-mcn/lib/client.js
T

3046 lines
258 KiB
JavaScript
Raw Normal View History

window.__ModuleLoader__.load({
id: "dsh-plugin-mcn",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let react_jsx_runtime = require("react/jsx-runtime");
let react = require("react");
let react_dom = require("react-dom");
//#region plugin css
const css = ":root{--mcn-brand:var(--dsw-alias-state-business-primary,#4176e6);--mcn-brand-hover:#3359c9}.mcnNav_entry{flex:0 0 100%;width:100%;min-width:0;height:49px;margin-top:8px;display:flex}.mcnNav_trigger{width:100%;min-height:40px;color:var(--dsw-alias-label-primary);font:var(--dsw-font-s-14);cursor:pointer;touch-action:manipulation;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out), transform var(--ds-transition-duration-fast) var(--ds-ease-in-out);background:0 0;border:0;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;display:flex}.mcnNav_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.mcnNav_triggerActive{background:var(--dsw-specific-sidebar-nav-item-active,var(--dsw-alias-interactive-bg-active));color:var(--mcn-brand)}.mcnNav_trigger:active{transform:scale(.96)}.mcnNav_trigger:focus-visible{outline:2px solid var(--mcn-brand);outline-offset:2px}.mcnNav_rail{flex:0 0 36px;width:36px;height:36px;margin:0}.mcnNav_rail .mcnNav_trigger{border-radius:50%;justify-content:center;width:36px;height:36px;min-height:36px;padding:0}.mcnNav_page{position:relative;background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex:1;grid-template-rows:minmax(0,1fr);display:grid;overflow:hidden}.mcnNav_header{display:flex;align-items:center;gap:12px;margin-bottom:12px}.mcnNav_title{font-size:16px;font-weight:600;margin:0}.mcnNav_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.mcnNav_toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.mcnNav_input{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 10px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);min-width:200px}.mcnNav_select{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 8px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary)}.mcnNav_btn{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 14px;font:var(--dsw-font-s-14);cursor:pointer;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out);white-space:nowrap}.mcnNav_btnSm{padding:2px 10px;font-size:12px;line-height:1.8}.mcnNav_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.mcnNav_btn:disabled{opacity:.45;cursor:not-allowed}.mcnNav_btn:disabled:hover{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))}.mcnNav_btnPrimary{background:var(--mcn-brand);color:#fff;border-color:transparent}.mcnNav_btnPrimary:hover{background:var(--mcn-brand-hover,#3359c9)}.mcnNav_spacer{flex:1}.mcnNav_btnIcon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);cursor:pointer;transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.mcnNav_btnIcon:hover{background:var(--dsw-alias-interactive-bg-hover)}.mcnNav_btnIcon:disabled{opacity:.5;cursor:not-allowed}.mcnNav_tableWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px}.mcnNav_rankSection{margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));display:flex;flex-direction:column;gap:8px;max-height:780px;min-height:0}.mcnNav_link{color:var(--mcn-brand);text-decoration:none;cursor:pointer;font-weight:500}.mcnNav_link:hover{text-decoration:underline}.mcnNav_dateBtn{border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:var(--dsw
//#region V1.0 首页复刻样式(亮色令牌硬编码,与 V1.0 style.css 对齐)
const v1css = [
".v1wrap{max-width:960px;margin:0 auto;padding:2px 2px 8px;box-sizing:border-box}",
".v1stats{display:flex;width:100%;box-sizing:border-box;background:rgba(47,111,237,.07);border-radius:12px;padding:16px 0;margin:2px 0 8px}",
".v1stat{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:5px;padding:2px 12px;border-right:1px solid rgba(47,111,237,.16)}",
".v1stat:last-child{border-right:none}",
".v1statV{font-size:26px;font-weight:700;color:#2f6fed;line-height:1.2;font-variant-numeric:tabular-nums}",
".v1statL{font-size:13px;color:#6b7280}",
".v1cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0 16px}",
".v1card{display:flex;flex-direction:column;align-items:flex-start;gap:6px;background:#fff;border:1px solid #e3e6ea;border-radius:12px;padding:16px 18px;cursor:pointer;text-align:left;box-shadow:0 1px 3px rgba(0,0,0,.08);transition:all .15s;font-family:inherit}",
".v1card:hover{border-color:#2f6fed;transform:translateY(-2px);box-shadow:0 4px 12px rgba(47,111,237,.15)}",
".v1cardI{display:inline-flex}",
".v1cardT{font-size:17px;font-weight:600;color:#24292f}",
".v1cardD{font-size:14px;color:#6b7280}",
".v1extra{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:-8px 0 12px}",
".v1extraL{font-size:13px;color:#6b7280}",
".v1chip{border:none;background:#eaf1fe;color:#2f6fed;font-size:13px;padding:4px 12px;border-radius:12px;cursor:pointer;font-family:inherit}",
".v1chip:hover{background:#2f6fed;color:#fff}",
".v1rank{margin:2px 0 18px}",
".v1rkhead{display:flex;align-items:center;gap:6px;border-bottom:1px solid #e3e6ea;margin-bottom:10px;flex-wrap:wrap;min-height:34px}",
".v1rktabs{display:flex;gap:2px;align-items:center}",
".v1rktab{border:none;background:transparent;color:#6b7280;padding:6px 12px 5px;border-radius:8px 8px 0 0;cursor:pointer;font-size:14px;border-bottom:2px solid transparent;font-family:inherit}",
".v1rktab:hover{color:#2f6fed;background:#eaf1fe}",
".v1rktab.on{color:#2f6fed;font-weight:600;background:#eaf1fe;border-bottom:2px solid #2f6fed}",
".v1rkupdate{background:transparent;border:none;color:#6b7280;cursor:pointer;padding:4px 7px;display:inline-flex;align-items:center;border-radius:6px}",
".v1rkupdate:hover{color:#2f6fed}",
".v1rktools{display:flex;align-items:center;gap:6px;margin-left:auto}",
".v1rksel{padding:2px 6px;font-size:13px;border:1px solid #e3e6ea;border-radius:6px;background:#fff;color:#24292f;max-width:112px;font-family:inherit}",
".v1rkbtn{padding:2px 8px;font-size:13px;border:1px solid #e3e6ea;border-radius:6px;background:#fff;color:#24292f;cursor:pointer;font-family:inherit}",
".v1rkbtn:hover{border-color:#2f6fed;color:#2f6fed}",
".v1rkbtn:disabled{opacity:.45;cursor:default}",
".v1rkdate{font-size:14px;color:#6b7280;min-width:56px;text-align:center;white-space:nowrap}",
".v1rklist{border:1px solid #e3e6ea;border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.08)}",
".v1rkrow{display:grid;grid-template-columns:34px 84px minmax(80px,1fr) 76px repeat(4,76px) 76px;align-items:center;gap:6px;padding:6px 10px;font-size:14px;border-bottom:1px solid #e3e6ea}",
".v1rkrow.v1rkvid{grid-template-columns:34px 76px minmax(140px,1fr) minmax(80px,1.1fr) repeat(4,74px)}",
".v1rkrow:last-child{border-bottom:none}",
".v1rkrow:not(.v1rkhead){min-height:40px}",
".v1rkrow button.v1btn{justify-self:center;width:auto}",
".v1rkhead{background:#fff;color:#6b7280;font-weight:600;font-size:13px}",
".v1no{color:#6b7280;font-weight:600}",
".v1cat{color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
".v1name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
".v1score{text-align:right;font-weight:600;color:#e8590c}",
".v1num{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
".v1act{display:flex;align-items:center;justify-content:center;white-space:nowrap}",
".v1author{color:#6b7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
".v1tlink{color:inherit;cursor:pointer;text-decoration:none}",
".v1tlink:hover{color:#4a90e2;text-decoration:underline}",
".v1btn{border:1px solid #e3e6ea;background:#fff;color:#24292f;padding:3px 10px;border-radius:6px;font-size:14px;cursor:pointer;text-decoration:none;font-family:inherit}",
".v1btn:hover{border-color:#2f6fed;color:#2f6fed}",
".v1btnview{border-color:#2f6fed;color:#2f6fed;background:#fff}",
".v1btnimp{border-color:#d0d4da;color:#495057}",
".v1rkempty{border:1px dashed #e3e6ea;border-radius:10px;padding:28px 12px;text-align:center;color:#6b7280;background:#fff;font-size:14px}",
".v1recents{margin:2px 0 20px}",
".v1rhead{display:flex;align-items:center;gap:12px;margin-bottom:10px}",
".v1rhead h3{font-size:17px;color:#24292f;margin:0}",
".v1rhead .v1all{font-size:13px;margin-left:auto;margin-right:6px;color:#4a90e2;cursor:pointer;text-decoration:none;border:none;background:none;padding:0;font-family:inherit}",
".v1rlist{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:14px}",
".v1rcard{display:flex;flex-direction:column;align-items:flex-start;gap:6px;background:#fff;border:1px solid #e3e6ea;border-radius:10px;padding:14px 16px;cursor:pointer;text-align:left;min-height:168px;box-shadow:0 1px 3px rgba(0,0,0,.08);transition:all .15s;font-family:inherit}",
".v1rcard:hover{border-color:#2f6fed;box-shadow:0 2px 8px rgba(47,111,237,.12)}",
".v1rcT{font-size:15px;font-weight:600;color:#24292f;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
".v1rcP{width:100%;display:flex;flex-direction:column;gap:2px}",
".v1rcL{display:block;font-size:14px;color:#57606a;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%}",
".v1rcF{width:100%;margin-top:auto;display:flex;align-items:center;gap:8px}",
".v1rcM{font-size:14px;color:#6b7280;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
".v1rcS{flex:none;font-size:12px;font-weight:600;padding:2px 8px;border-radius:10px;line-height:1.5}",
".v1mdMeta{font-size:14px;color:#6b7280;margin-bottom:12px}",
".v1mdBody{max-height:66vh;overflow:auto;font-size:15px;color:#24292f;line-height:1.7}",
".v1top{display:flex;align-items:center;gap:6px;justify-content:flex-end;min-height:24px;margin:0 0 2px}",
".v1topbtn{border:1px solid #e3e6ea;background:#fff;color:#6b7280;padding:3px 9px;border-radius:13px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;font-size:13px;font-family:inherit}",
".v1topbtn:hover{border-color:#2f6fed;color:#2f6fed}",
".v1topbtn.on{border-color:#2f6fed;color:#2f6fed;background:#eaf1fe}",
".v1helpTitle{font-size:15px;font-weight:600;color:#24292f;margin:10px 0 2px}",
".v1helpDesc{font-size:13px;color:#6b7280;margin:0 0 6px}",
".v1helpQa{font-size:13px;color:#24292f;line-height:1.9;margin:0 0 8px}",
".v1helpQa b{color:#2f6fed;font-weight:600}",
".mcnNav_mask{align-items:center;padding-top:0;background:rgba(0,0,0,.35)}",
".mcnNav_modal{background:#fff;border-radius:14px;padding:22px 26px;min-width:420px;max-width:92vw;box-shadow:0 8px 30px rgba(0,0,0,.18)}",
".mcnNav_modalTitle{font-size:18px;font-weight:600;margin-bottom:14px}",
".mcnNav_btn{border:1px solid #e3e6ea;background:#fff;color:#24292f;border-radius:8px;padding:6px 14px;font-size:15px;cursor:pointer;font-family:inherit;text-decoration:none}",
".mcnNav_btn:hover{border-color:#2f6fed;color:#2f6fed;background:#fff}",
".mcnNav_btn:disabled{opacity:.5;cursor:not-allowed}",
".mcnNav_btn:disabled:hover{background:#fff;color:#24292f;border-color:#e3e6ea}",
".mcnNav_btnSm{padding:3px 10px;font-size:14px;border-radius:6px;line-height:1.5}",
".mcnNav_btnPrimary{background:#2f6fed;color:#fff;border-color:transparent}",
".mcnNav_btnPrimary:hover{background:#2a5fd0;color:#fff}",
".mcnNav_toast{position:fixed;left:50%;bottom:32px;transform:translateX(-50%) translateY(0);background:#24292f;color:#fff;padding:10px 18px;border-radius:8px;font-size:14px;max-width:70vw;z-index:200;box-shadow:0 4px 16px rgba(0,0,0,.2);word-break:break-all;border:none;top:auto}",
".vvMask{position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:1000;padding:16px}",
".vvSmall{background:#fff;border-radius:14px;padding:22px 26px;width:520px;max-width:calc(100vw - 40px);box-shadow:0 8px 30px rgba(0,0,0,.18);max-height:88vh;overflow:auto;box-sizing:border-box}",
".vvSmall h3{font-size:18px;margin-bottom:12px;display:flex;align-items:center;gap:8px;font-weight:600}",
".vvDim{color:#6b7280;font-size:15px;margin-bottom:12px;line-height:1.6}",
".vvTarget{background:#eaf1fe;border-radius:8px;padding:10px 12px;font-size:16px;font-weight:600;color:#2f6fed;margin:8px 0 6px;word-break:break-all}",
".vvUrl{color:#6b7280;font-size:13px;word-break:break-all;padding:0 2px 6px;margin-bottom:6px}",
".vvWarn{color:#b7791f;font-size:14px;margin:4px 0 0;line-height:1.6}",
".vvActions{text-align:right;margin-top:14px;display:flex;justify-content:flex-end;gap:8px}",
".vb{border:1px solid #e3e6ea;background:#fff;color:#24292f;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:15px;text-decoration:none;font-family:inherit;display:inline-flex;align-items:center;gap:5px;line-height:1.4;box-sizing:border-box}",
".vb:hover{border-color:#2f6fed;color:#2f6fed}",
".vb:disabled{opacity:.5;cursor:not-allowed}",
".vb:disabled:hover{border-color:#e3e6ea;color:#24292f}",
".vbSm{padding:3px 10px;font-size:14px;border-radius:6px}",
".vbView{background:#fff;border-color:#2f6fed;color:#2f6fed}",
".vbView:hover{background:#eaf1fe;color:#2f6fed}",
".vbPrimary{background:#2f6fed;color:#fff;border-color:transparent}",
".vbPrimary:hover{background:#2a5fd0;color:#fff}",
".vbDel{background:#fdf0ef;border-color:#f0c9c6;color:#d9534f}",
".vbDel:hover{background:#fbe0de;border-color:#d9534f;color:#d9534f}",
".vbRed{background:#d9534f;color:#fff;border-color:#d9534f}",
".vbRed:hover{background:#c93f3b;color:#fff}",
".vvModal{width:min(1600px,calc(100% - 20px));height:calc(100vh - 40px);max-width:calc(100vw - 20px);box-sizing:border-box;background:#fff;border-radius:14px;box-shadow:0 8px 30px rgba(0,0,0,.18);display:flex;flex-direction:column;overflow:hidden;padding:18px 20px}",
".vvTitleRow{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;font-size:17px;font-weight:600;color:#24292f;flex:none}",
".vvTitleRow .vvT{display:flex;align-items:center;gap:8px;min-width:0;white-space:pre-wrap;word-break:break-word}",
".vvTitleRow .vvIcon{display:inline-flex;color:#2f6fed;flex:none}",
".vvTitleRow .vvMeta{font-weight:400;color:#6b7280;font-size:15px}",
".vvStack{flex:1;min-height:0;display:flex;flex-direction:row;gap:12px}",
".vvSec{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;background:#fff}",
".vvSecHead{flex:none;display:flex;align-items:center;gap:10px;padding:8px 12px;font-weight:600;background:#fff;border-bottom:1px solid #e3e6ea;color:#24292f;font-size:14px;min-height:38px}",
".vvSecBody{flex:1;min-height:0;overflow:auto;padding:12px 14px;font-size:14px;color:#24292f;line-height:1.7;background:#fff}",
".vvSecFoot{flex:none;min-height:46px;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:7px 12px;border-top:1px solid #e3e6ea}",
".vvCount{margin-right:auto;color:#6b7280;font-size:13px;white-space:nowrap}",
".vvNewBadge{display:inline-block;margin-left:6px;padding:0 7px;border-radius:9px;background:#2f6fed;color:#fff;font-size:12px;font-weight:600;line-height:17px;vertical-align:1px}",
".vvHidden{display:none}",
".vvSrcOn{background:#e8f1fd;border-color:#4a90e2;color:#2f6fed}",
".vvSideHead{padding:4px 6px;gap:0}",
".vvTabs{display:inline-flex;gap:2px;align-items:center}",
".vvTab{border:none;background:transparent;color:#6b7280;font-size:14px;font-weight:500;padding:5px 12px;border-radius:6px;cursor:pointer;line-height:1.4;font-family:inherit}",
".vvTab:hover{color:#2f6fed}",
".vvTab.on{color:#2f6fed;font-weight:600}",
".vvSep{color:#e3e6ea;font-size:14px;line-height:1;user-select:none}",
".vvSideToggle{margin-left:auto;border:none;background:transparent;color:#6b7280;padding:4px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;line-height:1;font-family:inherit}",
".vvSideToggle:hover{color:#2f6fed;background:#eaf1fe}",
".vvSideToggle.off{color:#2f6fed;background:#eaf1fe}",
".vvMd{word-break:break-word}",
".vvMd p{margin:6px 0}",
".vvMd h1,.vvMd h2,.vvMd h3,.vvMd h4{margin:10px 0 6px;color:#24292f}",
".vvMd h1{font-size:19px;border-bottom:1px solid #e3e6ea;padding-bottom:4px}",
".vvMd h2{font-size:17px;border-bottom:1px solid #e3e6ea;padding-bottom:3px}",
".vvMd h3{font-size:15px}",
".vvMd ul,.vvMd ol{margin:6px 0 6px 20px}",
".vvMd pre{background:#f6f8fa;border:1px solid #e3e6ea;border-radius:8px;padding:12px;overflow:auto;font-family:Consolas,monospace;font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word}",
".vvMd code{background:#f0f2f5;padding:1px 5px;border-radius:4px;font-size:13px}",
".vvMd pre code{background:none;padding:0}",
".vvMd blockquote{border-left:3px solid #2f6fed;margin:6px 0;padding:2px 10px;color:#57606a;background:#f6f9ff;border-radius:0 6px 6px 0}",
".vvMd hr{border:none;border-top:1px solid #e3e6ea;margin:10px 0}",
".vvMd strong{font-weight:600}",
".vvScoreBox{padding:10px;border:1px solid #e3e6ea;border-radius:8px;margin-bottom:10px;background:#fff}",
".vvScoreBoxTitle{font-weight:600;margin-bottom:8px;font-size:14px;color:#24292f}",
".vvScoreRow{display:flex;align-items:center;gap:8px;margin-bottom:4px}",
".vvScoreName{width:88px;font-size:13px;color:#6b7280;white-space:nowrap;flex-shrink:0}",
".vvScoreBar{flex:1;height:6px;border-radius:3px;background:#eef1f5;overflow:hidden}",
".vvScoreFill{height:100%}",
".vvScoreVal{width:36px;text-align:right;font-size:13px;font-weight:600;flex-shrink:0}",
".vvIssues{margin-bottom:10px}",
".vvIssueTitle{font-weight:600;margin-bottom:6px;font-size:14px;color:#24292f}",
".vvIssueRow{display:flex;gap:6px;font-size:13px;margin-bottom:4px;color:#24292f;line-height:1.6}",
".vvSummary{font-size:13px;color:#24292f;margin-bottom:8px;line-height:1.7}",
".vvLink{color:#4a90e2;cursor:pointer;font-size:13px;border:none;background:none;padding:0;font-family:inherit;text-decoration:none}",
".vvLink:hover{text-decoration:underline}",
".vvEmpty{color:#6b7280;font-size:14px;padding:14px 2px 10px;line-height:1.6}",
".vvLoading{color:#6b7280;font-size:13px;padding:12px 2px}",
".vvHint{font-size:12px;color:#6b7280;padding:4px 2px 0;line-height:1.6}",
".vvSbBlock{border:1px solid #e3e6ea;border-radius:8px;margin-bottom:8px;overflow:hidden;background:#fff}",
".vvSbHead{display:flex;align-items:center;gap:10px;padding:6px 12px;background:#f6f8fa;border-bottom:1px solid #e3e6ea}",
".vvSbTitle{font-weight:600;color:#2f6fed;font-size:14px}",
".vvSbMeta{margin-left:auto;font-size:13px;color:#6b7280}",
".mcnNav_rankSection .rkAcc thead th:nth-child(n+4):nth-child(-n+9),.mcnNav_rankSection .rkAcc tbody td:nth-child(n+4):nth-child(-n+9){text-align:right}",
".mcnNav_rankSection .rkAcc thead th:nth-child(10),.mcnNav_rankSection .rkAcc tbody td:nth-child(10){text-align:center}",
".mcnNav_page{overflow:auto}",
".mcnNav_page::-webkit-scrollbar{width:10px;height:10px}",
".mcnNav_page::-webkit-scrollbar-thumb{background:rgba(60,70,90,.28);border-radius:6px;border:2px solid transparent;background-clip:content-box}",
".mcnNav_page::-webkit-scrollbar-track{background:transparent}",
".mcnNav_page{overflow:hidden;grid-template-columns:minmax(0,1fr)}",
".mcnNav_body{overflow-y:auto;overflow-x:hidden;min-width:0;scrollbar-width:thin}",
".mcnNav_body::-webkit-scrollbar{width:10px}",
".mcnNav_body::-webkit-scrollbar-thumb{background:rgba(60,70,90,.28);border-radius:6px;border:2px solid transparent;background-clip:content-box}",
".mcnNav_header{position:sticky;top:0;z-index:6;background:var(--dsw-alias-bg-base,#f5f6f8);flex-wrap:wrap}",
".mcnNav_tableWrap{min-width:0}",
".mcnNav_body{overflow:visible}",
".mcnNav_rankSection .rkNum thead th:nth-child(n+5):nth-child(-n+8),.mcnNav_rankSection .rkNum tbody td:nth-child(n+5):nth-child(-n+8){text-align:right}",
".vvSbBody{padding:8px 12px;font-size:13px;color:#24292f;line-height:1.7;white-space:pre-wrap;word-break:break-word}",
].join("");
if (!document.querySelector("style[data-plugin-css=\"mcn-v1\"]")) {
const el = document.createElement("style");
el.dataset.plugin = "dsh-plugin-mcn";
el.dataset.pluginCss = "mcn-v1";
el.textContent = v1css;
document.head.appendChild(el);
}
//#endregion
const key = "dsh-plugin-mcn/McnNav.module.css";
if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) {
const el = document.createElement("style");
el.dataset.plugin = "dsh-plugin-mcn";
el.dataset.pluginCss = key;
el.textContent = css;
document.head.appendChild(el);
}
if (!document.querySelector('style[data-plugin-css="mcn-layout"]')) {
const el2 = document.createElement("style");
el2.dataset.pluginCss = "mcn-layout";
el2.textContent = ".mcnNav_page{overflow:hidden;grid-template-columns:minmax(0,1fr)}.mcnNav_body{overflow-y:auto!important;overflow-x:hidden!important;min-width:0;scrollbar-width:thin}.mcnNav_header{position:sticky!important;top:0!important;z-index:6;background:var(--dsw-alias-bg-base,#f5f6f8)}.mcnNav_tableWrap{min-width:0}.tblAiVid thead th:nth-child(n+3):nth-child(-n+6),.tblAiVid tbody td:nth-child(n+3):nth-child(-n+6){text-align:right}.tblAiVid thead th:nth-child(9),.tblAiVid thead th:nth-child(10),.tblAiVid tbody td:nth-child(9),.tblAiVid tbody td:nth-child(10){text-align:center}.tblAiCus thead th:nth-child(3),.tblAiCus tbody td:nth-child(3){text-align:right}.tblAiVid thead th:nth-child(n+3):nth-child(-n+6){color:#e11d48!important}";
document.head.appendChild(el2);
}
var McnNav_module_css_default = {
"entry": "mcnNav_entry",
"triggerActive": "mcnNav_triggerActive",
"tabs": "mcnNav_tabs",
"tab": "mcnNav_tab",
"tabActive": "mcnNav_tabActive",
"page": "mcnNav_page",
"trigger": "mcnNav_trigger",
"frameWrap": "mcnNav_frameWrap",
"back": "mcnNav_back",
"body": "mcnNav_body",
"header": "mcnNav_header",
"title": "mcnNav_title",
"toolbar": "mcnNav_toolbar",
"input": "mcnNav_input",
"select": "mcnNav_select",
"btn": "mcnNav_btn",
"btnSm": "mcnNav_btnSm",
"btnPrimary": "mcnNav_btnPrimary",
"spacer": "mcnNav_spacer",
"btnIcon": "mcnNav_btnIcon",
"tableWrap": "mcnNav_tableWrap",
"table": "mcnNav_table",
"cellName": "mcnNav_cellName",
"cellTrack": "mcnNav_cellTrack",
"cellContent": "mcnNav_cellContent",
"cellM": "mcnNav_cellM",
"pager": "mcnNav_pager",
"pagerInfo": "mcnNav_pagerInfo",
"pagerTotal": "mcnNav_pagerTotal",
"rankSection": "mcnNav_rankSection",
"link": "mcnNav_link",
"dateBtn": "mcnNav_dateBtn",
"mask": "mcnNav_mask",
"modal": "mcnNav_modal",
"modalTitle": "mcnNav_modalTitle",
"modalText": "mcnNav_modalText",
"modalAccount": "mcnNav_modalAccount",
"modalAccountName": "mcnNav_modalAccountName",
"modalAccountTrack": "mcnNav_modalAccountTrack",
"textarea": "mcnNav_textarea",
"modalBtns": "mcnNav_modalBtns",
"loading": "mcnNav_loading",
"error": "mcnNav_error",
"rail": "mcnNav_rail",
"split": "mcnNav_split",
"panel": "mcnNav_panel",
"panelLeft": "mcnNav_panelLeft",
"resizer": "mcnNav_resizer",
"resizerActive": "mcnNav_resizerActive",
"sideBtn": "mcnNav_sideBtn",
"themeBtn": "mcnNav_themeBtn",
"grid": "mcnNav_grid",
"stats": "mcnNav_stats",
"statItem": "mcnNav_statItem",
"statValue": "mcnNav_statValue",
"statLabel": "mcnNav_statLabel",
"card": "mcnNav_card",
"cardIcon": "mcnNav_cardIcon",
"entryIcon": "mcnNav_entryIcon",
"titleDot": "mcnNav_titleDot",
"cardTitle": "mcnNav_cardTitle",
"cardDesc": "mcnNav_cardDesc",
"cardPlaceholder": "mcnNav_cardPlaceholder",
"crumb": "mcnNav_crumb",
"crumbLink": "mcnNav_crumbLink",
"crumbSep": "mcnNav_crumbSep",
"crumbCurrent": "mcnNav_crumbCurrent",
"close": "mcnNav_close",
"rwModal": "mcnNav_rwModal",
"rwSplit": "mcnNav_rwSplit",
"rwPane": "mcnNav_rwPane",
"rwPaneTitle": "mcnNav_rwPaneTitle",
"rwContent": "mcnNav_rwContent",
"md": "mcnNav_md",
"spinner": "mcnNav_spinner",
"scanning": "mcnNav_scanning",
"toast": "mcnNav_toast"
};
//#endregion
//#region McnNav
// 用 hash 路由(/#/mcn):DSH 静态服务无 SPA fallback,pathname 路由直接访问/刷新会 404;
// hash 只在客户端变化,服务端始终返回 / 的 index.html
const mcnPath = "/#/mcn";
function isMcnRoute() {
return window.location.hash === "#/mcn";
}
// 统一线性 SVG 图标(Lucide 风格:24 viewBox、stroke currentColor、圆角端点)
function SvgBase({ d, size = 18, color }) {
return (0, react_jsx_runtime.jsx)("svg", {
viewBox: "0 0 24 24",
width: size,
height: size,
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": "true",
style: color ? { color } : undefined,
children: d
});
}
function IconSun({ size }) {
return (0, react_jsx_runtime.jsx)(SvgBase, { size, color: "#f59e0b", d: [(0, react_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "4" }), (0, react_jsx_runtime.jsx)("path", { d: "M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" })] });
}
function IconMoon({ size }) {
return (0, react_jsx_runtime.jsx)(SvgBase, { size, color: "#6366f1", d: [(0, react_jsx_runtime.jsx)("path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" })] });
}
function IconPen({ size }) {
return (0, react_jsx_runtime.jsx)(SvgBase, { size, color: "#ff7a45", d: [(0, react_jsx_runtime.jsx)("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }), (0, react_jsx_runtime.jsx)("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" }), (0, react_jsx_runtime.jsx)("path", { d: "M10 9H8" }), (0, react_jsx_runtime.jsx)("path", { d: "M16 13H8" }), (0, react_jsx_runtime.jsx)("path", { d: "M16 17H8" })] });
}
function IconSparkles({ size }) {
return (0, react_jsx_runtime.jsx)(SvgBase, { size, color: "#8b5cf6", d: [(0, react_jsx_runtime.jsx)("path", { d: "M9.9 2.4 11.2 6l3.6 1.3-3.6 1.3-1.3 3.6-1.3-3.6L4.9 7.3 8.5 6 9.9 2.4Z" }), (0, react_jsx_runtime.jsx)("path", { d: "M17.5 12.5 18.4 15l2.5.9-2.5.9-.9 2.5-.9-2.5-2.5-.9 2.5-.9.9-2.5Z" }), (0, react_jsx_runtime.jsx)("path", { d: "M5 14.5 5.7 16.5 7.7 17.2 5.7 17.9 5 19.9 4.3 17.9 2.3 17.2 4.3 16.5 5 14.5Z" })] });
}
function McnIcon() {
return (0, react_jsx_runtime.jsx)("svg", {
viewBox: "0 0 24 24",
width: "18",
height: "18",
"aria-hidden": "true",
style: { color: "var(--mcn-brand)" },
children: (0, react_jsx_runtime.jsx)("path", {
d: "M14.4 3.5c.2 1.6 1.1 3 2.4 3.9l-1 2.3c-1.7-.9-3-2.3-3.8-4.1L14.4 3.5Zm3.5 5.2c.3 1.4 1.5 2.4 3 2.4l.1 2.5c-2.6-.1-4.9-1.4-6.2-3.4l3.1-1.5ZM4 5.5h6.3c.2 1.3 1 2.4 2.1 3.1l-1 2.4c-1.9-1.1-3.3-2.8-4-4.9L4 5.5Zm7.4 5.6h6.5c.2 1.2 1 2.3 2.1 3l-1 2.4c-1.8-1-3.2-2.7-3.9-4.6l-3.7-.8ZM4 12.4h5.9c.1 1.1.8 2 1.8 2.6l-.9 2.3c-1.5-.9-2.6-2.3-3.1-4l-3.7-.9Zm6.8 3.2h5.9c.1 1 .8 1.9 1.7 2.4l-.9 2.3c-1.3-.8-2.3-2-2.7-3.4l-4-.9-.6-.4Z",
fill: "currentColor"
})
});
}
function McnNavTrigger({ wide, useSessions, openSession }) {
const sessions = useSessions((state) => state);
const [open, setOpen] = (0, react.useState)(isMcnRoute);
const [pageHost, setPageHost] = (0, react.useState)(null);
const splitRef = (0, react.useRef)(null);
const sideRef = (0, react.useRef)("right");
const widthRef = (0, react.useRef)(480);
const [panelSide, setPanelSide] = (0, react.useState)(() => {
try { return localStorage.getItem("mcnNav.panelSide") === "left" ? "left" : "right"; } catch (e) { return "right"; }
});
const [panelWidth, setPanelWidth] = (0, react.useState)(() => {
// 百分比宽度:默认 50%(对话区/面板各半),范围 30%~70%(各自最少保留 30%)
try { const w = parseFloat(localStorage.getItem("mcnNav.panelWidth") || "50"); return Number.isFinite(w) && w >= 30 && w <= 70 ? w : 50; } catch (e) { return 50; }
});
sideRef.current = panelSide;
widthRef.current = panelWidth;
const previousSession = (0, react.useRef)(sessions.current);
(0, react.useEffect)(() => {
if (!wide) return;
const host = document.querySelector("[data-slot=\"sidebar.footer.action\"]")?.parentElement;
if (host === void 0 || host === null) return;
const previous = host.style.flexWrap;
host.style.flexWrap = "wrap";
return () => {
host.style.flexWrap = previous;
};
}, [wide]);
(0, react.useEffect)(() => {
const syncRoute = () => {
const next = isMcnRoute();
if (next) previousSession.current = sessions.current;
else if (open && sessions.current === void 0 && previousSession.current !== void 0) openSession(previousSession.current);
setOpen(next);
};
window.addEventListener("popstate", syncRoute);
return () => window.removeEventListener("popstate", syncRoute);
}, [
open,
openSession,
sessions.current
]);
const applySide = (next) => {
const s = splitRef.current;
if (s !== null && s.wrapper.isConnected) {
const conv = s.conversation;
if (next === "left") {
// 顺序: [panel, resizer, conversation]
s.wrapper.insertBefore(s.panel, conv);
s.wrapper.insertBefore(s.resizer, conv);
} else {
// 顺序: [conversation, resizer, panel]
s.wrapper.insertBefore(conv, s.wrapper.firstChild);
s.wrapper.insertBefore(s.resizer, conv.nextSibling);
s.wrapper.appendChild(s.panel);
}
s.panel.classList.toggle(McnNav_module_css_default.panelLeft, next === "left");
}
setPanelSide(next);
sideRef.current = next;
try { localStorage.setItem("mcnNav.panelSide", next); } catch (e) {}
};
const startResize = (e) => {
e.preventDefault();
const startX = e.clientX;
const startW = widthRef.current;
const side = sideRef.current;
const s0 = splitRef.current;
if (s0 !== null) s0.resizer.classList.add(McnNav_module_css_default.resizerActive);
let cur = startW;
const onMove = (ev) => {
// 分隔条跟随鼠标;宽度按百分比:对话区与面板各自最少 30%
const s = splitRef.current;
if (s === null) return;
const wrapW = s.wrapper.getBoundingClientRect().width || 1;
const deltaPx = side === "left" ? (ev.clientX - startX) : (startX - ev.clientX);
const deltaPct = (deltaPx / wrapW) * 100;
cur = Math.min(70, Math.max(30, startW + deltaPct));
s.panel.style.width = cur + "%";
setPanelWidth(cur);
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
const s = splitRef.current;
if (s !== null) s.resizer.classList.remove(McnNav_module_css_default.resizerActive);
try { localStorage.setItem("mcnNav.panelWidth", String(cur)); } catch (e) {}
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
};
(0, react.useEffect)(() => {
if (!open) {
setPageHost(null);
return;
}
const conversation = document.querySelector("[data-slot=\"conversation\"]");
const host = conversation?.parentElement;
if (conversation === void 0 || conversation === null || host === void 0 || host === null) return;
let s = splitRef.current;
if (s === null) {
const previousCss = conversation.style.cssText;
// 嵌入式分栏:conversation 移入 flex-row wrapper,与面板同流左右排列
// flex 布局全部由 CSS 规则驱动(.mcnNav_split 及子选择器 !important),
// 避免 React 重渲染对话区时重置 inline style 导致 flex 丢失
const wrapper = document.createElement("div");
wrapper.setAttribute("data-mcn-split", "1");
wrapper.className = McnNav_module_css_default.split;
host.insertBefore(wrapper, conversation);
wrapper.appendChild(conversation);
// 对话区铺满与 flex 伸缩均由 CSS 规则驱动(见 .mcnNav_split 子选择器)
const resizer = document.createElement("div");
resizer.setAttribute("data-mcn-resizer", "1");
resizer.className = McnNav_module_css_default.resizer;
resizer.addEventListener("mousedown", startResize);
const panel = document.createElement("div");
panel.setAttribute("data-mcn-panel", "1");
panel.className = McnNav_module_css_default.panel;
panel.style.width = panelWidth + "%";
wrapper.append(resizer, panel);
s = { wrapper, panel, resizer, conversation, host, previousCss };
splitRef.current = s;
}
applySide(panelSide);
setPageHost(s.panel);
return () => {
const cur = splitRef.current;
if (cur !== null && cur.wrapper.isConnected) {
cur.host.insertBefore(cur.conversation, cur.wrapper);
cur.conversation.style.cssText = cur.previousCss;
cur.wrapper.remove();
}
splitRef.current = null;
};
}, [open]);
const showMcn = () => {
if (open) {
closeMcn();
return;
}
previousSession.current = sessions.current;
window.history.pushState({
...window.history.state,
mcn: true
}, "", mcnPath);
setOpen(true);
};
// 暴露"打开工作台"给独立入口(如 AI会话 侧边栏项):只打开不切换视图
(0, react.useEffect)(() => {
window.__mcnNavOpen = () => {
if (!open) {
previousSession.current = sessions.current;
window.history.pushState({ ...window.history.state, mcn: true }, "", mcnPath);
setOpen(true);
}
};
return () => { if (window.__mcnNavOpen) delete window.__mcnNavOpen; };
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [open]);
const closeMcn = () => {
// 清空 hash 回到根路径(replaceState 会移除 hash 段)
window.history.replaceState(window.history.state, "", "/");
setOpen(false);
if (sessions.current === void 0 && previousSession.current !== void 0) openSession(previousSession.current);
};
return (0, react_jsx_runtime.jsxs)("div", {
className: `${McnNav_module_css_default.entry} ${wide ? "" : McnNav_module_css_default.rail}`,
children: [(0, react_jsx_runtime.jsxs)("button", {
type: "button",
className: `${McnNav_module_css_default.trigger} ${open ? McnNav_module_css_default.triggerActive : ""}`,
"aria-label": "MCN工作台",
"aria-current": open ? "page" : void 0,
onClick: showMcn,
children: [(0, react_jsx_runtime.jsx)(McnIcon, {}), wide && (0, react_jsx_runtime.jsx)("span", { children: "MCN工作台" })]
}), open && pageHost !== null && (0, react_dom.createPortal)((0, react_jsx_runtime.jsx)(McnPage, { onClose: closeMcn, side: panelSide, onToggleSide: () => applySide(panelSide === "left" ? "right" : "left") }), pageHost)]
});
}
/** 独立侧边栏入口:MCN任务会话(打开 MCN 工作台并切到 MCN任务会话 视图) */
function AiSessionsTrigger({ wide }) {
const openAiSessions = () => {
try { window.__mcnNavOpen?.(); } catch (e) { /* 打开失败忽略 */ }
setTimeout(() => { try { if (window.__mcnNav) window.__mcnNav.openView("ai-sessions"); } catch (e) {} }, 60);
};
return (0, react_jsx_runtime.jsxs)("div", {
className: `${McnNav_module_css_default.entry} ${wide ? "" : McnNav_module_css_default.rail}`,
children: [(0, react_jsx_runtime.jsxs)("button", {
type: "button",
className: McnNav_module_css_default.trigger,
"aria-label": "MCN任务会话",
title: "查看插件创建的 MCN 任务会话",
onClick: openAiSessions,
children: [(0, react_jsx_runtime.jsx)("span", { children: "💬" }), wide && (0, react_jsx_runtime.jsx)("span", { children: "MCN任务会话" })]
})]
});
}
//#endregion
//#region 极简 markdown 渲染(无第三方依赖,渲染为 React 元素避免 XSS;移植自 dsh-plugin-douyin-account-detail)
const INLINE_RE = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g;
function textWithBreaks(seg, keyBase) {
if (!seg.includes("\n")) return [seg];
const parts = seg.split("\n");
const out = [];
parts.forEach((p, i) => {
if (p) out.push(p);
if (i < parts.length - 1) out.push(react.createElement("br", { key: keyBase + "-br" + i }));
});
return out;
}
function parseInline(text, keyBase) {
if (!text) return [text];
const nodes = [];
let last = 0, k = 0;
for (const m of text.matchAll(INLINE_RE)) {
if (m.index > last) nodes.push(...textWithBreaks(text.slice(last, m.index), keyBase + "-t" + k));
const tok = m[0];
if (tok.startsWith("**") && tok.endsWith("**") && tok.length > 4) {
nodes.push(react.createElement("strong", { key: keyBase + "-b" + k }, tok.slice(2, -2)));
} else if (tok.startsWith("*") && tok.endsWith("*") && tok.length > 2) {
nodes.push(react.createElement("em", { key: keyBase + "-i" + k }, tok.slice(1, -1)));
} else if (tok.startsWith("`") && tok.endsWith("`") && tok.length > 2) {
nodes.push(react.createElement("code", { key: keyBase + "-c" + k }, tok.slice(1, -1)));
} else if (tok.startsWith("[")) {
const mm = tok.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
const href = mm && /^(https?:|mailto:)/i.test(mm[2]) ? mm[2] : null;
nodes.push(href
? react.createElement("a", { key: keyBase + "-a" + k, href: href, target: "_blank", rel: "noopener noreferrer" }, mm[1])
: (mm ? mm[1] : tok));
} else {
nodes.push(tok);
}
last = m.index + tok.length;
k++;
}
if (last < text.length) nodes.push(...textWithBreaks(text.slice(last), keyBase + "-t" + k));
return nodes;
}
function mdTable(rows, keyBase) {
const split = (r) => r.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
const head = split(rows[0]);
const body = rows.slice(1).map(split);
const firstColStyle = { width: "72px", maxWidth: "72px", minWidth: 0, wordBreak: "break-word" };
return react.createElement("table", { key: keyBase, style: { width: "100%" } },
react.createElement("thead", null,
react.createElement("tr", null, head.map((h, i) => react.createElement("th", { key: "h" + i, style: i === 0 ? firstColStyle : undefined }, parseInline(h, keyBase + "-h" + i))))),
react.createElement("tbody", null,
body.map((row, ri) => react.createElement("tr", { key: "r" + ri },
row.map((c, ci) => react.createElement("td", { key: "c" + ci, style: ci === 0 ? firstColStyle : undefined }, parseInline(c, keyBase + "-c" + ri + "-" + ci)))))));
}
function mdToReact(md) {
if (typeof md !== "string" || !md.trim()) return null;
const lines = md.replace(/\r\n/g, "\n").split("\n");
const out = [];
let i = 0, k = 0;
while (i < lines.length) {
const t = lines[i].trim();
const fence = t.match(/^```(\S*)\s*$/);
if (fence) {
const buf = [];
i++;
while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) { buf.push(lines[i]); i++; }
i++;
out.push(react.createElement("pre", { key: "md" + k++ }, react.createElement("code", null, buf.join("\n"))));
continue;
}
const h = t.match(/^(#{1,4})\s+(.*)$/);
if (h) {
out.push(react.createElement("h" + h[1].length, { key: "md" + k++ }, parseInline(h[2], "h" + k)));
i++;
continue;
}
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(t)) {
out.push(react.createElement("hr", { key: "md" + k++ }));
i++;
continue;
}
if (t.startsWith(">")) {
const buf = [];
while (i < lines.length && lines[i].trim().startsWith(">")) { buf.push(lines[i].trim().replace(/^>\s?/, "")); i++; }
out.push(react.createElement("blockquote", { key: "md" + k++ }, mdToReact(buf.join("\n"))));
continue;
}
if (t.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) {
const sep = lines[i + 1].replace(/\s/g, "");
if (/^\|?(:?-+:?\|)+:?-+:?\|?$/.test(sep)) {
const buf = [t];
i += 2;
while (i < lines.length && lines[i].trim().includes("|")) { buf.push(lines[i].trim()); i++; }
out.push(mdTable(buf, "md" + k++));
continue;
}
}
const ul = t.match(/^[-*+]\s+(.*)$/);
if (ul) {
const items = [];
while (i < lines.length) {
const mt = lines[i].trim().match(/^[-*+]\s+(.*)$/);
if (mt) { items.push(mt[1]); i++; continue; }
break;
}
out.push(react.createElement("ul", { key: "md" + k++ }, items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii)))));
continue;
}
const ol = t.match(/^\d+[.)]\s+(.*)$/);
if (ol) {
const items = [];
while (i < lines.length) {
const mt = lines[i].trim().match(/^\d+[.)]\s+(.*)$/);
if (mt) { items.push(mt[1]); i++; continue; }
break;
}
out.push(react.createElement("ol", { key: "md" + k++ }, items.map((it, ii) => react.createElement("li", { key: ii }, parseInline(it, "li" + k + "-" + ii)))));
continue;
}
if (t === "") { i++; continue; }
const para = [t];
i++;
while (i < lines.length) {
const nt = lines[i].trim();
if (nt === "" || /^(#{1,4}\s|```|>|[-*+]\s|\d+[.)]\s|.*\|)/.test(nt) || /^(-{3,}|\*{3,}|_{3,})\s*$/.test(nt)) break;
para.push(nt);
i++;
}
out.push(react.createElement("p", { key: "md" + k++ }, parseInline(para.join("\n"), "p" + k)));
}
return out;
}
//#endregion
//#region AI写作提升:全部视频列表(已解析优先 → 点赞降序)
function AiVideosPanel({ videos, total, page, loading, search, parsed, onSearch, onParsedChange, onPage, onBack, onCreate, onAnalyze, onRewritesChanged, days, account, onDaysChange, onClearAccount }) {
const totalPages = Math.max(1, Math.ceil(total / 20));
// 分镜提示词分块渲染:按 `---` 分隔成「分镜」块;每块拆分为 header(分镜号/标题/时间段/总秒数)+ content(4层结构内容)
// 首行格式:段N(开始-结束秒)|标题(如「段1(0-6秒)|场1:二爸家卧室·深夜」)
const renderStoryboardBlocks = (text) => {
const blocks = String(text || "").split(/\n\s*---+\s*\n/).map((b) => b.trim()).filter(Boolean);
if (blocks.length <= 1) return mdToReact(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 header = null;
let content = b;
if (m) {
const num = m[1];
const start = parseFloat(m[2]);
const end = parseFloat(m[3]);
const title = m[4].trim();
const total = (Number.isFinite(start) && Number.isFinite(end)) ? (end - start) : NaN;
header = {
num,
title,
range: `${m[2]} - ${m[3]}秒`,
total: Number.isFinite(total) ? `${total}秒` : null,
};
content = lines.slice(1).join("\n").trim();
}
return (0, react_jsx_runtime.jsxs)("div", {
style: { border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, marginBottom: 8, overflow: "hidden", background: "var(--dsw-alias-bg-base)" },
children: [
(0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 12px", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", flexWrap: "wrap" },
children: [
(0, react_jsx_runtime.jsx)("span", { style: { fontWeight: 600, color: "var(--mcn-brand,#4176e6)", whiteSpace: "nowrap" }, children: header && header.title ? header.title : `场${header ? header.num : bi + 1}` }),
(0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto", display: "flex", gap: 10, fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: [(0, react_jsx_runtime.jsx)("span", { children: header ? header.range : "" }), header && header.total && (0, react_jsx_runtime.jsx)("span", { children: `总${header.total}` })] })
]
}),
(0, react_jsx_runtime.jsx)("div", { style: { padding: "8px 12px" }, children: mdToReact(content) })
]
}, "sb" + bi);
});
};
// 功能 TAB:rewrite=参考选题创作(现有列表+按账号预筛);custom=原创选题创作(本页实现);abc=ABC融合创作(待开发)
const [tab, setTab] = (0, react.useState)("rewrite");
const TAB_TITLES = { rewrite: "参考选题创作", custom: "原创选题创作", abc: "ABC融合创作" };
// ===== 原创选题创作中心(custom tab):列表/徽标/查看/复盘/删除(本组件自管理) =====
const custAcc = account && (account.name || account.account_name) ? String(account.name || account.account_name) : ""; // 服务端 custom-scripts/rewrite-stats 按账号名过滤
const [custStats, setCustStats] = (0, react.useState)({ videoScripts: 0, customScripts: 0 });
const [custRows, setCustRows] = (0, react.useState)([]);
const [custTotal, setCustTotal] = (0, react.useState)(0);
const [custPage, setCustPage] = (0, react.useState)(1);
const [custLoading, setCustLoading] = (0, react.useState)(true); // 初始 true:首次进入 custom tab 先显示加载态
// 原创脚本单列查看弹窗 / 删除确认 / 提交复盘
const [csDetail, setCsDetail] = (0, react.useState)(null);
const [csDetailData, setCsDetailData] = (0, react.useState)(null);
const [csDel, setCsDel] = (0, react.useState)(null);
const [csBusy, setCsBusy] = (0, react.useState)(false);
const [cToast, setCToast] = (0, react.useState)(null);
const custLandRef = (0, react.useRef)("");
(0, react.useEffect)(() => {
if (!cToast) return;
const t = setTimeout(() => setCToast(null), 3000);
return () => clearTimeout(t);
}, [cToast]);
const custFmtTime = (t) => String(t || "").slice(0, 16).replace("T", " ");
const loadCustStats = () => {
fetch("/mcn/api/rewrite-stats?account=" + encodeURIComponent(custAcc))
.then((r) => r.json())
.then((d) => { if (d.ok) setCustStats({ videoScripts: d.videoScripts || 0, customScripts: d.customScripts || 0 }); })
.catch(() => {});
};
const loadCustom = (p, silent) => {
const pageNo = p || 1;
if (!silent) setCustLoading(true);
fetch("/mcn/api/custom-scripts?search=" + encodeURIComponent(search || "") + "&days=" + encodeURIComponent(String(days || "")) + "&account=" + encodeURIComponent(custAcc) + "&page=" + pageNo + "&pageSize=20")
.then((r) => r.json())
.then((d) => { if (d.ok) { setCustRows(d.items || []); setCustTotal(d.total || 0); setCustPage(d.page || 1); } })
.catch(() => {})
.finally(() => { if (!silent) setCustLoading(false); });
};
// 徽标计数:scope 变化(含清空)都拉一次
(0, react.useEffect)(() => { loadCustStats(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [custAcc]);
// custom 列表数据:切到 custom 或 筛选(搜索/天数/账号)变化时刷新
(0, react.useEffect)(() => { if (tab !== "custom") return; loadCustom(1); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [tab, search, days, custAcc]);
// 账号级入口:该账号已有原创脚本 → 初始落在 custom tab(每个账号上下文仅一次)
(0, react.useEffect)(() => {
if (custAcc && custStats.customScripts > 0 && custLandRef.current !== custAcc) {
custLandRef.current = custAcc;
setTab("custom");
}
}, [custStats, custAcc]);
const openCsDetail = (row) => {
setCsDetail(row);
setCsDetailData(null);
fetch("/mcn/api/custom-script?id=" + row.id)
.then((r) => r.json())
.then((d) => { setCsDetailData(d.ok && d.script ? d.script : null); })
.catch(() => setCsDetailData(null));
};
const csStartReview = () => {
if (!csDetail || csBusy) return;
setCsBusy(true);
fetch("/mcn/api/review/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ logId: csDetail.id, type: "ai" }),
})
.then((r) => r.json())
.then((d) => { if (!d.ok) throw new Error(d.error || "提交失败"); setCToast(d.message || "已提交 AI 脚本复盘"); loadCustom(custPage, true); })
.catch((e) => setCToast("提交失败: " + e.message))
.finally(() => setCsBusy(false));
};
const csConfirmDelete = () => {
if (!csDel) return;
const id = csDel.id;
setCsDel(null);
fetch("/mcn/api/rewrite/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setCToast(d.error || "删除失败"); return; }
setCToast("已删除原创脚本");
if (csDetail && csDetail.id === id) { setCsDetail(null); setCsDetailData(null); }
setCustRows((prev) => prev.filter((x) => x.id !== id));
loadCustom(custPage, true);
loadCustStats();
})
.catch(() => setCToast("删除失败,请重试"));
};
const fmt = (v) => (v == null || v === "") ? "-" : String(v);
const fmtDuration = (v) => {
const s = Number(v);
if (!Number.isFinite(s) || s <= 0) return "-";
const m = Math.floor(s / 60);
const sec = Math.round(s % 60);
return m > 0 ? `${m}分${sec}秒` : `${sec}秒`;
};
// 改写对比弹窗:detail=当前视频;detailData={source, rewrites, storyboards};verIdx=改写版本索引(0=最新);sbVerIdx=分镜提示词版本索引(0=最新)
const [detail, setDetail] = (0, react.useState)(null);
const [detailData, setDetailData] = (0, react.useState)(null);
const [verIdx, setVerIdx] = (0, react.useState)(0);
const [sbVerIdx, setSbVerIdx] = (0, react.useState)(0);
// 当前 AI脚本版本对应的分镜提示词列表(按 rewrite_id 过滤,切换 AI脚本版本时分镜内容联动)
const curRewrite = detailData && detailData.rewrites && detailData.rewrites[verIdx] ? detailData.rewrites[verIdx] : null;
const visibleStoryboards = ((detailData && detailData.storyboards) || []).filter((s) => !curRewrite || s.rewrite_id === curRewrite.id);
// 渲染用分镜索引(防越界:切版本/删除后 visibleStoryboards 长度变化)
const sbIdx = Math.min(sbVerIdx, Math.max(0, visibleStoryboards.length - 1));
// 切换 AI脚本版本时回到该版本分镜的第一条
(0, react.useEffect)(() => { setSbVerIdx(0); }, [verIdx]);
// 解析中状态(videoId):点击「未解析」后置位,防重复点击;列表刷新后已解析则显示已解析
const [analyzing, setAnalyzing] = (0, react.useState)(null);
const startAnalyze = (row) => { setAnalyzing(row.id); onAnalyze(row); };
const openDetail = (row) => {
setDetail(row);
setDetailData(null);
setVerIdx(0);
setSbVerIdx(0);
fetch("/mcn/api/video/script?videoId=" + row.id)
.then((r) => r.json())
.then((d) => { if (d.ok) setDetailData((prev) => ({ ...prev, source: d.source, rewrites: d.rewrites || [] })); })
.catch(() => setDetailData((prev) => ({ ...prev, source: null, rewrites: [] })));
// 分镜提示词列表(并行加载,供右侧「AI分镜提示词」面板展示与版本切换)
fetch("/mcn/api/storyboard/list?videoId=" + row.id)
.then((r) => r.json())
.then((d) => { if (d.ok) setDetailData((prev) => ({ ...prev, storyboards: d.items || [] })); })
.catch(() => setDetailData((prev) => ({ ...prev, storyboards: [] })));
};
// 分镜提示词生成确认:confirmStoryboardRow={rewriteId, videoTitle}(null 关闭);确认后提交生成并刷新分镜列表
const [confirmStoryboardRow, setConfirmStoryboardRow] = (0, react.useState)(null);
// 分镜提示词生成中:sbBusy=true 时「AI提示词」按钮禁用并显示「生成中…」;轮询检测到新 done 版本后恢复
const [sbBusy, setSbBusy] = (0, react.useState)(false);
const sbPollRef = (0, react.useRef)(null);
const sbBaseCountRef = (0, react.useRef)(0);
const stopSbPoll = () => {
if (sbPollRef.current) { clearInterval(sbPollRef.current); sbPollRef.current = null; }
};
const refreshStoryboards = (videoId) => {
return fetch("/mcn/api/storyboard/list?videoId=" + videoId)
.then((r) => r.json())
.then((dd) => { if (dd.ok) setDetailData((prev) => ({ ...prev, storyboards: dd.items || [] })); return dd; })
.catch(() => null);
};
const confirmStoryboard = () => {
if (!confirmStoryboardRow) return;
fetch("/mcn/api/storyboard/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rewriteId: confirmStoryboardRow.rewriteId }),
})
.then((r) => r.json())
.then((d) => {
setConfirmStoryboardRow(null);
if (!d.ok) throw new Error(d.error || "生成失败");
// 进入生成中:按钮禁用;轮询直到出现新的 done 版本(任务完成)后恢复
if (detail) {
const videoId = detail.id;
sbBaseCountRef.current = (detailData && detailData.storyboards || []).length;
setSbBusy(true);
refreshStoryboards(videoId);
stopSbPoll();
sbPollRef.current = setInterval(() => {
refreshStoryboards(videoId).then((dd) => {
if (dd && dd.ok) {
const done = (dd.items || []).filter((x) => x.status === "done" && x.script_text);
if (done.length > sbBaseCountRef.current) {
stopSbPoll();
setSbBusy(false);
setSbVerIdx(0);
}
}
});
}, 5000);
}
})
.catch((e) => { setConfirmStoryboardRow(null); });
};
// 弹窗关闭/组件卸载时停止轮询并重置生成中状态(避免重开弹窗时按钮残留「生成中…」禁用)
(0, react.useEffect)(() => {
if (detail === null) {
stopSbPoll();
setSbBusy(false);
}
return () => stopSbPoll();
}, [detail]);
// 分镜提示词删除确认:storyboardDeleteConfirm={id}(null 关闭);确认后删除当前查看的版本并刷新
const [storyboardDeleteConfirm, setStoryboardDeleteConfirm] = (0, react.useState)(null);
const confirmStoryboardDelete = () => {
if (!storyboardDeleteConfirm) return;
fetch("/mcn/api/storyboard/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: storyboardDeleteConfirm.id }),
})
.then((r) => r.json())
.then((d) => {
setStoryboardDeleteConfirm(null);
if (!d.ok) throw new Error(d.error || "删除失败");
const removedId = storyboardDeleteConfirm.id;
setDetailData((prev) => {
const storyboards = (prev?.storyboards || []).filter((s) => s.id !== removedId);
setSbVerIdx((v) => Math.max(0, Math.min(v, storyboards.length - 1)));
return { ...prev, storyboards };
});
})
.catch(() => { setStoryboardDeleteConfirm(null); });
};
// 改写确认:confirmRow=待确认改写的视频(null 关闭);确认后执行改写并关闭对比弹窗
const [confirmRow, setConfirmRow] = (0, react.useState)(null);
// 重新解析确认:reanalyzeRow=待确认重新解析的视频(null 关闭)
const [reanalyzeRow, setReanalyzeRow] = (0, react.useState)(null);
// 未解析确认:unParseRow=待确认触发解析的视频(null 关闭)
const [unParseRow, setUnParseRow] = (0, react.useState)(null);
// 删除改写脚本确认:deleteConfirm={id}(null 关闭)
const [deleteConfirm, setDeleteConfirm] = (0, react.useState)(null);
const confirmDelete = () => {
if (!deleteConfirm) return;
fetch("/mcn/api/rewrite/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: deleteConfirm.id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "删除失败");
const removedId = deleteConfirm.id;
setDeleteConfirm(null);
setDetailData((prev) => {
const rewrites = (prev?.rewrites || []).filter((r) => r.id !== removedId);
setVerIdx((v) => Math.max(0, Math.min(v, rewrites.length - 1)));
return { ...prev, rewrites };
});
if (onRewritesChanged) onRewritesChanged();
})
.catch(() => { setDeleteConfirm(null); });
};
const confirmRewrite = () => {
if (!confirmRow) return;
onCreate(confirmRow);
setConfirmRow(null);
if (detail) setDetail(null);
};
return (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.body,
children: [(0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.toolbar, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, title: "返回工作台", onClick: onBack, children: "←" }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.title, style: { fontSize: 16, fontWeight: 600 }, children: "AI写脚本" })] }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.tabs, style: { flexWrap: "wrap" }, children: [[["rewrite", "参考选题创作"], ["custom", "原创选题创作"], ["abc", "ABC融合创作"]].map(([id, label]) => {
const badgeCnt = id === "rewrite" ? custStats.videoScripts : (id === "custom" ? custStats.customScripts : 0);
return (0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.tab + (tab === id ? " " + McnNav_module_css_default.tabActive : ""),
onClick: () => setTab(id),
children: [(0, react_jsx_runtime.jsx)("span", { children: label }), id !== "abc" && (0, react_jsx_runtime.jsx)("span", { style: { marginLeft: 4, padding: "0 6px", borderRadius: 9, fontSize: 11, lineHeight: "16px", minWidth: 16, textAlign: "center", background: "var(--mcn-brand,#4176e6)", color: "#fff" }, children: badgeCnt })]
}, id);
}), (0, react_jsx_runtime.jsxs)("div", { style: { marginLeft: "auto", flex: "1 1 0%", minWidth: 0, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }, children: [account && (0, react_jsx_runtime.jsx)("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, padding: "1px 4px 1px 10px", borderRadius: 14, border: "1px solid var(--mcn-brand,#4176e6)", fontSize: 12, color: "var(--mcn-brand,#4176e6)", maxWidth: 260 }, children: [(0, react_jsx_runtime.jsx)("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 150 }, children: (account.name || ("#" + account.id)) + " 的视频/原创" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, style: { border: 0, background: "transparent", color: "var(--mcn-brand,#4176e6)" }, title: "清除账号筛选", onClick: onClearAccount, children: "清除筛选" })] }), (0, react_jsx_runtime.jsx)("input", { className: McnNav_module_css_default.input, placeholder: "搜索视频标题 / 账号", value: search, onChange: (e) => onSearch(e.target.value) }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => onSearch(search), children: "搜索" }), (0, react_jsx_runtime.jsx)("select", { className: McnNav_module_css_default.select, value: String(days || ""), title: "发布时间范围", onChange: (e) => onDaysChange(e.target.value), children: [(0, react_jsx_runtime.jsx)("option", { value: "", children: "全部时间" }), (0, react_jsx_runtime.jsx)("option", { value: "30", children: "近一月" }), (0, react_jsx_runtime.jsx)("option", { value: "90", children: "近三月" }), (0, react_jsx_runtime.jsx)("option", { value: "180", children: "近半年" }), (0, react_jsx_runtime.jsx)("option", { value: "365", children: "近一年" })] }), (0, react_jsx_runtime.jsx)("select", { className: McnNav_module_css_default.select, value: parsed || "", title: "解析状态筛选", onChange: (e) => onParsedChange(e.target.value), children: [(0, react_jsx_runtime.jsx)("option", { value: "", children: "全部状态" }), (0, react_jsx_runtime.jsx)("option", { value: "1", children: "已解析" }), (0, react_jsx_runtime.jsx)("option", { value: "0", children: "未解析" })] })] }), (0, react_jsx_runtime.jsx)("div", { style: { flexBasis: "100%", fontSize: 12, color: "#6b7280", margin: "0 2px 8px", lineHeight: 1.6 }, children: tab === "rewrite" ? (custAcc ? "账号「" + custAcc + "」· 参考选题创作:以该账号视频为选题来源,行尾「AI创作」= 基于该视频创作新脚本(完成后自动保存并挂到该视频下)" : "参考选题创作:以库中视频为选题来源(关注账号优先 · 按发布时间倒序),行尾「AI创作」= 基于该视频创作新脚本(完成后自动保存并挂到该视频下)") : (tab === "custom" ? (custAcc ? "账号「" + custAcc + "」· 原创选题创作:无原视频的脚本(来自账号列表「AI创作」弹窗等),保存后自动入库显示在这里" : "原创选题创作:无原视频的脚本(来自账号列表 / 账号详情「AI创作」弹窗等),保存后自动入库显示在这里") : "ABC 融合创作:将同账号多条视频的爆款元素融合成一条全新脚本。功能开发中,敬请期待。") })] }), tab === "rewrite" && (loading ? react.createElement("div", { className: McnNav_module_css_default.loading }, "加载中…") : videos.length === 0 ? react.createElement("div", { className: McnNav_module_css_default.empty, style: { padding: 26, textAlign: "center", color: "#6b7280" } }, "暂无视频数据") : (() => {
const tmpl = "110px minmax(150px,1fr) 70px 70px 70px 70px 116px 96px 96px 108px";
const CS = (al, clip) => ({ padding: "9px 10px", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", textAlign: al || "left", minWidth: 0, whiteSpace: "nowrap", overflow: clip ? "hidden" : undefined, textOverflow: clip ? "ellipsis" : undefined, boxSizing: "border-box" });
const H = (t, al, i) => react.createElement("div", { key: "h" + i, style: Object.assign(CS(al), { color: "var(--dsw-alias-label-secondary,#6b7280)", fontWeight: 600, fontSize: 13 }) }, t);
const heads = ["账号", "视频标题", "点赞", "评论", "分享", "时长", "发布时间", "状态", "AI脚本", "操作"].map((t, i) => H(t, i >= 2 && i <= 5 ? "right" : i >= 7 ? "center" : "left", i));
const rowEl = (row) => react.createElement("div", { key: row.id, style: { display: "grid", gridTemplateColumns: tmpl, alignItems: "center" } },
react.createElement("div", { style: CS("left", true) }, react.createElement("span", { className: McnNav_module_css_default.link, title: "查看账号详情", style: { cursor: "pointer" }, onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-account-detail", { accountId: row.account_id, accountName: row.account_name }); } catch (e) {} } }, fmt(row.account_name))),
react.createElement("div", { style: CS("left", true) }, react.createElement("span", { className: McnNav_module_css_default.link, title: "查看视频详情", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)" }, onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-video-detail", { videoId: row.id, awemeId: row.aweme_id, accountId: row.account_id }); } catch (e) {} } }, fmt(row.video_title))),
react.createElement("div", { style: CS("right") }, fmt(row.like_display || row.like_count)),
react.createElement("div", { style: CS("right") }, fmt(row.comment_count)),
react.createElement("div", { style: CS("right") }, fmt(row.share_count)),
react.createElement("div", { style: CS("right") }, fmtDuration(row.duration)),
react.createElement("div", { style: CS("left") }, fmt(row.publish_time)),
react.createElement("div", { style: CS("center") }, (row.has_source || row.has_analysis) ? ((row.analyzing || analyzing === row.id) ? react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm + " " + McnNav_module_css_default.scanning, disabled: true, title: "视频解析中,请勿重复提交" }, "解析中") : react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, style: { color: "var(--dsw-alias-state-success-primary,#22c55e)", fontWeight: 600 }, title: "点击可重新解析该视频", onClick: () => setReanalyzeRow(row) }, "已解析")) : (row.analyzing || analyzing === row.id) ? react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm + " " + McnNav_module_css_default.scanning, disabled: true, title: "视频解析中,请勿重复提交" }, "解析中") : react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, title: "触发视频解析", onClick: () => setUnParseRow(row) }, "未解析")),
react.createElement("div", { style: CS("center") }, react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: !(row.rewrite_count > 0), title: (row.rewrite_count > 0) ? "查看该视频下的 AI 脚本(源脚本对比/分镜)" : "暂无 AI 脚本", onClick: () => openDetail(row) }, (row.rewrite_count > 0) ? (String(row.rewrite_count) + " 查看") : "—")),
react.createElement("div", { style: CS("center") }, (row.has_source || row.has_analysis) ? (row.rewrite_running ? react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm + " " + McnNav_module_css_default.scanning, disabled: true, title: "AI 正在改写中,请勿重复提交" }, "执行中") : react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, title: "基于账号设定与视频选题改写全新 S9 故事脚本", onClick: () => setConfirmRow(row) }, "AI创作")) : react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: true, title: "需先完成视频解析后才能创作" }, "AI创作")));
return react.createElement("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
react.createElement("div", { style: { display: "grid", gridTemplateColumns: tmpl, alignItems: "center", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))" } }, heads),
videos.map((row) => rowEl(row)));
})()), tab === "rewrite" && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.pager,
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerTotal, children: `共 ${total} 条` }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${page} / ${totalPages} 页` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page <= 1 || loading, onClick: () => onPage(page - 1), children: "上一页" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page >= totalPages || loading, onClick: () => onPage(page + 1), children: "下一页" })]
}),
tab === "custom" && (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [(custLoading ? react.createElement("div", { className: McnNav_module_css_default.loading }, "加载中…") : custRows.length === 0 ? react.createElement("div", { className: McnNav_module_css_default.empty, style: { padding: 26, textAlign: "center", color: "#6b7280" } }, "暂无原创选题脚本(可从账号列表/账号详情行内 AI创作 创作,完成后自动入库)") : (() => {
const tmpl = "120px minmax(150px,1fr) 70px 140px 110px";
const CS = (al, clip) => ({ padding: "9px 10px", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", textAlign: al || "left", minWidth: 0, whiteSpace: "nowrap", overflow: clip ? "hidden" : undefined, textOverflow: clip ? "ellipsis" : undefined, boxSizing: "border-box" });
const H = (t, al, i) => react.createElement("div", { key: "h" + i, style: Object.assign(CS(al), { color: "var(--dsw-alias-label-secondary,#6b7280)", fontWeight: 600, fontSize: 13 }) }, t);
const heads = ["账号", "脚本标题", "字数", "创建时间", "操作"].map((t, i) => H(t, i === 2 ? "right" : i === 4 ? "center" : "left", i));
const rowEl = (row) => react.createElement("div", { key: row.id, style: { display: "grid", gridTemplateColumns: tmpl, alignItems: "center" } },
react.createElement("div", { style: CS("left", true) }, row.account_name || "-"),
react.createElement("div", { style: CS("left", true) }, row.title ? react.createElement("span", { className: McnNav_module_css_default.link, title: "查看原创选题脚本", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)" }, onClick: () => openCsDetail(row) }, row.title) : "-"),
react.createElement("div", { style: CS("right") }, row.script_len ? row.script_len : "-"),
react.createElement("div", { style: CS("left") }, custFmtTime(row.created_time)),
react.createElement("div", { style: CS("center") }, react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm + " " + McnNav_module_css_default.btnPrimary, onClick: () => openCsDetail(row), title: "查看完整脚本(含复盘/删除入口)" }, "查看")));
return react.createElement("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
react.createElement("div", { style: { display: "grid", gridTemplateColumns: tmpl, alignItems: "center", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))" } }, heads),
custRows.map((row) => rowEl(row)));
})()) ,, !custLoading && custRows.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.pager,
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerTotal, children: `共 ${custTotal} 条` }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${custPage} / ${Math.max(1, Math.ceil(custTotal / 20))} 页` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: custPage <= 1 || custLoading, onClick: () => loadCustom(custPage - 1), children: "上一页" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: custPage >= Math.max(1, Math.ceil(custTotal / 20)) || custLoading, onClick: () => loadCustom(custPage + 1), children: "下一页" })]
}), csDetail && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setCsDetail(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: "原创选题脚本" + (csDetail && csDetail.title ? " — " + csDetail.title : "") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setCsDetail(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", padding: "0 2px 8px" }, children: [(0, react_jsx_runtime.jsx)("span", { style: { padding: "1px 8px", borderRadius: 10, fontSize: 12, background: "var(--mcn-brand,#4176e6)", color: "#fff", whiteSpace: "nowrap" }, children: "原创选题创作" }), csDetail && csDetail.id != null && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "AI脚本 #" + csDetail.id }), csDetail && csDetail.account_name && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: csDetail.account_name })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, style: { maxHeight: "60vh", overflow: "auto" }, children: csDetailData ? mdToReact(csDetailData.script_text || "(暂无脚本内容)") : "加载中…" }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, paddingTop: 10, borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))" }, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, style: { color: "#d9534f", borderColor: "#d9534f" }, onClick: () => setCsDel(csDetail), children: "删除" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, disabled: csBusy || !csDetailData, title: "发起 AI 脚本复盘(无源模式,独立 10 维评分)", onClick: csStartReview, children: csBusy ? "提交中…" : "AI 脚本复盘" })] })]
})]
}), csDel && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setCsDel(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除原创选题脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: csDel ? `将删除原创选题 AI脚本 #${csDel.id}(无源视频)及其关联复盘数据,删除后不可恢复。` : "" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setCsDel(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: csConfirmDelete, children: "确认删除" })] })]
})]
}), cToast && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.toast, children: cToast })]
}),
tab === "abc" && (0, react_jsx_runtime.jsx)("div", {
className: McnNav_module_css_default.empty,
children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, fontSize: 17, marginBottom: 8 }, children: "ABC 融合创作" }), (0, react_jsx_runtime.jsx)("div", { style: { color: "#6b7280", fontSize: 14 }, children: "将同账号多条视频的爆款元素融合成一条全新脚本,功能开发中。" })]
}),
detail && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setDetail(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: `源脚本 vs AI写脚本 — ${detail.video_title || ""}` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setDetail(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwSplit, children: [(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0, gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "源脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: mdToReact(detailData ? (detailData.source || "(暂无源脚本数据)") : "加载中…") })] }), (0, react_jsx_runtime.jsx)("div", { style: { height: 32, display: "flex", alignItems: "center", justifyContent: "flex-end" } })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0, gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "AI脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: mdToReact(!detailData ? "加载中…" : ((detailData.rewrites || []).length === 0 ? "(暂无改写脚本,点击下方「AI脚本」生成)" : (detailData.rewrites[verIdx] || {}).script_text)) }), detailData && (detailData.rewrites || []).length > 0 && (0, react_jsx_runtime.jsx)("button", { type: "button", title: "删除当前展示的改写脚本", onClick: () => setDeleteConfirm({ id: detailData.rewrites[verIdx].id }), style: { position: "absolute", right: "14px", bottom: "14px", display: "inline-flex", alignItems: "center", justifyContent: "center", width: 30, height: 30, background: "var(--dsw-alias-bg-base)", border: "1px solid rgba(229,72,77,.35)", borderRadius: 8, color: "#d9534f", cursor: "pointer", zIndex: 2 }, children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 15, height: 15, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }) }) })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8 }, children: [detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: verIdx >= detailData.rewrites.length - 1, onClick: () => setVerIdx(verIdx + 1), children: "上一版" }), detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${detailData.rewrites.length - verIdx} / ${detailData.rewrites.length} 版` }), detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: verIdx <= 0, onClick: () => setVerIdx(verIdx - 1), children: "下一版" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_d
})]}) , confirmStoryboardRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setConfirmStoryboardRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认生成 AI 分镜提示词" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: `确定基于「${confirmStoryboardRow.videoTitle || ""}」的当前改写脚本生成分镜提示词?将调用 storyboard-prompt 技能生成可直接用于 AI 生视频平台的分镜提示词序列。` }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setConfirmStoryboardRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, onClick: confirmStoryboard, children: "确认生成" })] })]
})]
}) , confirmRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setConfirmRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认发起 AI 改写" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: `确定对「${confirmRow.video_title || ""}」发起 AI 改写?将基于账号设定与视频选题生成全新 S9 故事脚本。` }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setConfirmRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, onClick: confirmRewrite, children: "确认改写" })] })]
})]
}), reanalyzeRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setReanalyzeRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认重新解析" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: `确定对「${reanalyzeRow.video_title || ""}」重新解析?将重新获取并分析该视频的源脚本与拆解内容。` }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setReanalyzeRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, onClick: () => { startAnalyze(reanalyzeRow); setReanalyzeRow(null); }, children: "确认解析" })] })]
})]
}), unParseRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setUnParseRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认解析" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: `确定对「${unParseRow.video_title || ""}」触发解析?将获取并分析该视频的源脚本与拆解内容,完成后自动入库。` }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setUnParseRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, onClick: () => { startAnalyze(unParseRow); setUnParseRow(null); }, children: "确认解析" })] })]
})]
}), deleteConfirm && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setDeleteConfirm(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除改写脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: "确定删除当前展示的这版改写脚本?删除后不可恢复。" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setDeleteConfirm(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: confirmDelete, children: "确认删除" })] })]
})]
}), storyboardDeleteConfirm && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setStoryboardDeleteConfirm(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除分镜提示词" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: "确定删除当前展示的这版分镜提示词?删除后不可恢复。" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setStoryboardDeleteConfirm(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: confirmStoryboardDelete, children: "确认删除" })] })]
})]
})]
});
}
//#endregion
//#region 脚本诊断:AI写脚本列表(按生成时间倒序)+ 写作弹窗 + 复盘入口
function ScriptReviewPanel({ onBack }) {
const GEN_METHODS = { rewrite: "参考选题创作", new: "原创选题创作", abc: "ABC融合创作" };
// 生成方式标签键:行内 genMethod/gen_method 优先;video_id 为空 = 原创选题(new)
const methodKeyOf = (row) => row.genMethod || row.gen_method || (row.video_id > 0 ? "rewrite" : "new");
const DIMS = [["共鸣和共情", "创意看点"], ["设定承接", "设定承接"], ["框架节奏", "框架节奏"], ["钩子设置", "爆款开场"], ["叙事逻辑", "叙事逻辑"], ["极致事件", "极致事件"], ["其他", "其他"], ["总分", "总分"]];
const PAGE_SIZE = 20;
// 生成时间格式化:ISO → YYYY-MM-DD HH:mm(本地时区)
const fmtTime = (t) => {
if (!t) return "-";
const d = new Date(t);
if (Number.isNaN(d.getTime())) return String(t);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
};
// 列表:rewrite_log(AI 写脚本),仅已生成脚本,按生成时间倒序
const [rows, setRows] = (0, react.useState)([]);
const [loading, setLoading] = (0, react.useState)(false);
const [page, setPage] = (0, react.useState)(1);
const loadList = (silent) => {
if (!silent) setLoading(true);
fetch("/mcn/api/rewrite/list")
.then((r) => r.json())
.then((d) => {
if (!d.ok) return;
const items = (d.items || [])
.filter((x) => x.status === "done" && x.script_len > 0)
.sort((a, b) => String(b.created_time || "").localeCompare(String(a.created_time || "")) || b.id - a.id);
// 并行补充账号名称(video_id → 视频详情;同一视频多版复用一次请求)
const vidSet = [...new Set(items.map((x) => x.video_id).filter((v) => v > 0))];
Promise.all(vidSet.map((vid) => fetch("/mcn/api/video/detail?videoId=" + vid)
.then((r) => r.json())
.then((vd) => ({ vid, name: vd.ok && vd.video ? (vd.video.account_name || "") : "" }))
.catch(() => ({ vid, name: "" }))))
.then((pairs) => {
const nameMap = {};
pairs.forEach((p) => { nameMap[p.vid] = p.name; });
setRows(items.map((x) => ({ ...x, account_name: nameMap[x.video_id] || "" })));
})
.catch(() => setRows(items));
setPage(1);
})
.catch(() => {})
.finally(() => { if (!silent) setLoading(false); });
};
(0, react.useEffect)(() => { loadList(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []);
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
// 写作弹窗:detail=当前行;detailData={videoTitle,source,rewrites,storyboards}
const [detail, setDetail] = (0, react.useState)(null);
const [detailData, setDetailData] = (0, react.useState)(null);
const [verIdx, setVerIdx] = (0, react.useState)(0);
const [sbVerIdx, setSbVerIdx] = (0, react.useState)(0);
// 原创选题脚本(video_id 空,gen_method=new)单列查看 / AI 复盘 / 删除
const [csDetail, setCsDetail] = (0, react.useState)(null);
const [csDetailData, setCsDetailData] = (0, react.useState)(null);
const [csDel, setCsDel] = (0, react.useState)(null);
const [csBusy, setCsBusy] = (0, react.useState)(false);
const [csToast, setCsToast] = (0, react.useState)(null);
(0, react.useEffect)(() => {
if (!csToast) return;
const t = setTimeout(() => setCsToast(null), 3000);
return () => clearTimeout(t);
}, [csToast]);
const csPollRef = (0, react.useRef)(null);
(0, react.useEffect)(() => () => { if (csPollRef.current) clearInterval(csPollRef.current); }, []);
const openCsDetail = (row) => {
setCsDetail(row);
setCsDetailData(null);
fetch("/mcn/api/custom-script?id=" + row.id)
.then((r) => r.json())
.then((d) => { setCsDetailData(d.ok && d.script ? d.script : null); })
.catch(() => setCsDetailData(null));
};
const csStartReview = () => {
if (!csDetail || csBusy) return;
setCsBusy(true);
fetch("/mcn/api/review/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ logId: csDetail.id, type: "ai" }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setCsToast(d.message || "已提交 AI 脚本复盘");
loadList(true);
// 与源脚本复盘一致:轮询列表直到完成(5s × ≤2 分钟)
if (csPollRef.current) clearInterval(csPollRef.current);
let n = 0;
csPollRef.current = setInterval(() => {
n++;
loadList(true);
if (n >= 24) { clearInterval(csPollRef.current); csPollRef.current = null; }
}, 5000);
})
.catch((e) => setCsToast("提交失败: " + e.message))
.finally(() => setCsBusy(false));
};
const csConfirmDelete = () => {
if (!csDel) return;
const id = csDel.id;
setCsDel(null);
fetch("/mcn/api/rewrite/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "删除失败");
setCsToast("已删除原创脚本");
if (csDetail && csDetail.id === id) { setCsDetail(null); setCsDetailData(null); }
setRows((prev) => prev.filter((x) => x.id !== id));
setPage(1);
})
.catch((e) => { setCsDel(null); setCsToast("删除失败: " + e.message); });
};
const openScript = (row) => {
// 原创选题(无源视频):单列查看(custom-script);不触发源/对比/分镜接口
if (!(row.video_id > 0)) { openCsDetail(row); return; }
setDetail(row);
setDetailData(null);
setVerIdx(0);
setSbVerIdx(0);
fetch("/mcn/api/video/detail?videoId=" + row.video_id)
.then((r) => r.json())
.then((d) => { if (d.ok && d.video) setDetailData((prev) => ({ ...(prev || {}), videoTitle: d.video.video_title || "" })); })
.catch(() => {});
fetch("/mcn/api/video/script?videoId=" + row.video_id)
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setDetailData((prev) => ({ ...(prev || {}), source: null, rewrites: [] })); return; }
const rewrites = d.rewrites || [];
const idx = rewrites.findIndex((x) => x.id === row.id);
setDetailData((prev) => ({ ...(prev || {}), source: d.source, rewrites }));
if (idx >= 0) setVerIdx(idx);
})
.catch(() => setDetailData((prev) => ({ ...(prev || {}), source: null, rewrites: [] })));
fetch("/mcn/api/storyboard/list?videoId=" + row.video_id)
.then((r) => r.json())
.then((d) => { if (d.ok) setDetailData((prev) => ({ ...(prev || {}), storyboards: d.items || [] })); })
.catch(() => {});
};
const curRewrite = detailData && detailData.rewrites && detailData.rewrites[verIdx] ? detailData.rewrites[verIdx] : null;
const visibleStoryboards = ((detailData && detailData.storyboards) || []).filter((s) => !curRewrite || s.rewrite_id === curRewrite.id);
const sbIdx = Math.min(sbVerIdx, Math.max(0, visibleStoryboards.length - 1));
(0, react.useEffect)(() => { setSbVerIdx(0); }, [verIdx]);
// 分镜提示词分块渲染:按 --- 分隔 + 段首行 header(与 AI写脚本 页一致)
const renderStoryboardBlocks = (text) => {
const blocks = String(text || "").split(/\n\s*---+\s*\n/).map((b) => b.trim()).filter(Boolean);
if (blocks.length <= 1) return mdToReact(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 header = null;
let content = b;
if (m) {
const num = m[1];
const start = parseFloat(m[2]);
const end = parseFloat(m[3]);
const title = m[4].trim();
const total = (Number.isFinite(start) && Number.isFinite(end)) ? (end - start) : NaN;
header = { num, title, range: `${m[2]} - ${m[3]}秒`, total: Number.isFinite(total) ? `${total}秒` : null };
content = lines.slice(1).join("\n").trim();
}
return (0, react_jsx_runtime.jsxs)("div", {
style: { border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, marginBottom: 8, overflow: "hidden", background: "var(--dsw-alias-bg-base)" },
children: [
(0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 12px", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", flexWrap: "wrap" },
children: [
(0, react_jsx_runtime.jsx)("span", { style: { fontWeight: 600, color: "var(--mcn-brand,#4176e6)", whiteSpace: "nowrap" }, children: header && header.title ? header.title : `场${header ? header.num : bi + 1}` }),
(0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto", display: "flex", gap: 10, fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: [(0, react_jsx_runtime.jsx)("span", { children: header ? header.range : "" }), header && header.total && (0, react_jsx_runtime.jsx)("span", { children: `总${header.total}` })] })
]
}),
(0, react_jsx_runtime.jsx)("div", { style: { padding: "8px 12px" }, children: mdToReact(content) })
]
}, "sb" + bi);
});
};
// 复盘:三模式(ai=AI脚本复盘 / source=原视频脚本复盘 / compare=对比分析),三份独立存储
const [reviewRow, setReviewRow] = (0, react.useState)(null); // 当前操作行(打开模式选择弹窗)
const [reviewBusy, setReviewBusy] = (0, react.useState)(false); // 提交中
const [reviewDetail, setReviewDetail] = (0, react.useState)(null); // 查看详情的 review id
const [reviewDetailData, setReviewDetailData] = (0, react.useState)(null);
const [delReviewRow, setDelReviewRow] = (0, react.useState)(null); // 确认删除复盘数据的行
const [fullReport, setFullReport] = (0, react.useState)(false);
const [fullReportData, setFullReportData] = (0, react.useState)(null);
const [toast, setToast] = (0, react.useState)(null);
const pollRef = (0, react.useRef)(null);
// 列表行的评分数据:优先 AI 复盘,回退对比分析的 ai 段(无则空)
const dimsOf = (row) => {
const pick = (rv) => {
try { return { dims: JSON.parse(rv.dims_json || "{}"), total: Number(rv.total) }; }
catch (e) { return { dims: {}, total: NaN }; }
};
if (row.review_ai && row.review_ai.status === "done") return pick(row.review_ai);
if (row.review_compare && row.review_compare.status === "done") {
try {
const j = JSON.parse(row.review_compare.dims_json || "{}");
const t = JSON.parse(row.review_compare.total || "{}");
return { dims: j.ai || {}, total: Number(t.ai) };
} catch (e) { return { dims: {}, total: NaN }; }
}
return { dims: {}, total: NaN };
};
const fmtScore = (v) => (typeof v === "number" && Number.isFinite(v) ? v : "-");
// 列表行的复盘记录(详情弹窗用):优先 AI 复盘,回退对比分析
const reviewOf = (row) => (row.review_ai && row.review_ai.status === "done" ? row.review_ai : (row.review_compare && row.review_compare.status === "done" ? row.review_compare : null));
// 提交复盘 + 轮询列表直到完成(最多 2 分钟)
const startReview = (type) => {
if (!reviewRow || reviewBusy) return;
const logId = reviewRow.id;
setReviewBusy(true);
fetch("/mcn/api/review/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ logId, type }),
})
.then((r) => r.json())
.then((d) => {
setReviewRow(null);
if (d.ok) {
setToast(d.message || "已提交复盘");
loadList(true);
if (pollRef.current) clearInterval(pollRef.current);
let n = 0;
pollRef.current = setInterval(() => {
n++;
loadList(true);
if (n >= 24) { clearInterval(pollRef.current); pollRef.current = null; }
}, 5000);
} else {
setToast(d.error || "提交失败");
if (d.review && d.review.id) { setReviewDetail(d.review.id); setReviewDetailData(null); }
}
})
.catch(() => { setReviewRow(null); setToast("提交失败,请重试"); })
.finally(() => setReviewBusy(false));
};
// 打开复盘详情(含"重新复盘"入口:关闭详情后回到模式弹窗)
const openReviewDetail = (id) => {
setReviewDetail(id);
setReviewDetailData(null);
fetch("/mcn/api/review/detail?id=" + id)
.then((r) => r.json())
.then((d) => { if (d.ok) setReviewDetailData(d.review); })
.catch(() => {});
};
// 删除复盘数据:AI 复盘 + 对比分析(源视频复盘为视频级共享保留,脚本保留)
const delReview = () => {
if (!delReviewRow) return;
const rewriteId = delReviewRow.id;
setDelReviewRow(null);
fetch("/mcn/api/review/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rewriteId }),
})
.then((r) => r.json())
.then((d) => { setToast(d.ok ? "复盘数据已删除" : (d.error || "删除失败")); if (d.ok) loadList(true); })
.catch(() => setToast("删除失败,请重试"));
};
const reviewTypeName = (t) => t === "source" ? "原视频脚本复盘" : t === "compare" ? "对比分析" : "AI 脚本复盘";
const fmtRevTime = (t) => { if (!t) return ""; const d = new Date(t); if (Number.isNaN(d.getTime())) return ""; return fmtTime(t); };
// 打开完整报告:拉取该脚本三份完整复盘(左=源 / 右=AI / 下=对比)
const openFullReport = (rv) => {
setFullReport(true);
setFullReportData(null);
const rid = rv && rv.rewrite_id;
if (!rid) { setFullReportData({ empty: true }); return; }
fetch("/mcn/api/review/full?rewriteId=" + rid)
.then((r) => r.json())
.then((d) => { if (d.ok) setFullReportData(d.reviews); else setFullReportData({ empty: true }); })
.catch(() => setFullReportData({ empty: true }));
};
(0, react.useEffect)(() => {
if (!toast) return;
const t = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(t);
}, [toast]);
return (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.body,
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.toolbar,
children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, title: "返回工作台", onClick: onBack, children: "←" }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.title, children: "脚本诊断" }), (0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto", fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: `按生成时间倒序 · 共 ${rows.length} 条` })]
}), loading ? (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.loading, children: "加载中…" }) : rows.length === 0 ? (0, react_jsx_runtime.jsx)("div", { style: { padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)", font: "var(--dsw-font-s-14)" }, children: "暂无 AI 写脚本(完成改写的脚本会出现在这里)" }) : (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.tableWrap,
children: [(0, react_jsx_runtime.jsx)("table", {
className: McnNav_module_css_default.table,
children: [(0, react_jsx_runtime.jsx)("thead", {
children: (0, react_jsx_runtime.jsx)("tr", {
children: ["ID", "账号名称", "生成方式", "脚本标题", "生成时间"].concat(DIMS.map((x) => x[1]), ["操作"]).map((h, i) => (0, react_jsx_runtime.jsx)("th", {
style: h === "操作" ? { textAlign: "center" } : (i === 0 ? { minWidth: "35px" } : i === 1 ? { maxWidth: "80px" } : i === 2 ? { maxWidth: "80px" } : i === 3 ? { minWidth: "117px" } : i === 4 ? { maxWidth: "80px" } : undefined),
children: h
}, i))
})
}), (0, react_jsx_runtime.jsx)("tbody", {
children: pageRows.map((row) => (0, react_jsx_runtime.jsx)("tr", {
children: [(0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellTrack, children: "#" + row.id }), (0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellContent, style: { maxWidth: "80px" }, children: row.account_name || "-" }), (0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellContent, style: { maxWidth: "80px" }, children: GEN_METHODS[methodKeyOf(row)] }), (0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellContent, style: { maxWidth: "117px" }, children: row.title ? (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.link, title: row.video_id > 0 ? "打开写作弹窗查看 源脚本 vs AI写脚本" : "打开原创选题脚本(单列展示)", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)" }, onClick: () => openScript(row), children: row.title }) : "-" }), (0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellContent, style: { maxWidth: "80px" }, children: fmtTime(row.created_time) }), DIMS.map((pair) => {
const sd = dimsOf(row);
const key = pair[0];
const v = key === "总分" ? sd.total : sd.dims[key];
const rv = key === "总分" ? reviewOf(row) : null;
const clickable = key === "总分" && rv && typeof v === "number";
return (0, react_jsx_runtime.jsx)("td", { style: { color: key === "总分" && typeof v === "number" ? "var(--mcn-brand,#4176e6)" : "var(--dsw-alias-label-secondary,#8a8f98)", textAlign: "center", fontWeight: key === "总分" ? 600 : undefined }, children: clickable ? (0, react_jsx_runtime.jsx)("span", { style: { cursor: "pointer", textDecoration: "underline", textDecorationStyle: "dotted" }, title: "点击查看复盘详情", onClick: () => openReviewDetail(rv.id), children: fmtScore(v) }) : fmtScore(v) }, pair[1]);
}), (0, react_jsx_runtime.jsx)("td", { className: McnNav_module_css_default.cellTrack, style: { textAlign: "center" }, children: (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 4, justifyContent: "center" }, children: [row.review_running ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: true, title: "复盘任务执行中", children: "复盘运行中…" }) : (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, title: "发起复盘:AI脚本 / 原视频脚本 / 对比分析", onClick: () => setReviewRow(row), children: "复盘" }), ((row.review_ai && row.review_ai.status === "done") || (row.review_compare && row.review_compare.status === "done")) ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, title: "删除该脚本的复盘数据(AI 复盘 + 对比分析;源视频复盘与脚本保留)", onClick: () => setDelReviewRow(row), children: "删除" }) : null] }) })]
}, row.id))
})]
}), rows.length > PAGE_SIZE && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.pager,
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerTotal, children: `共 ${rows.length} 条` }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${page} / ${totalPages} 页` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page <= 1 || loading, onClick: () => setPage(page - 1), children: "上一页" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page >= totalPages || loading, onClick: () => setPage(page + 1), children: "下一页" })]
})]
}), detail && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setDetail(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: `源脚本 vs AI写脚本 — ${(detailData && detailData.videoTitle) || ""}` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setDetail(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwSplit, children: [(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0, gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "源脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: mdToReact(detailData ? (detailData.source || "(暂无源脚本数据)") : "加载中…") })] })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0, gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "AI脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: mdToReact(!detailData ? "加载中…" : ((detailData.rewrites || []).length === 0 ? "(暂无改写脚本)" : ((detailData.rewrites[verIdx] || {}).script_text || ""))) })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8 }, children: [detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: verIdx >= detailData.rewrites.length - 1, onClick: () => setVerIdx(verIdx + 1), children: "上一版" }), detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${detailData.rewrites.length - verIdx} / ${detailData.rewrites.length} 版` }), detailData && (detailData.rewrites || []).length > 1 && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: verIdx <= 0, onClick: () => setVerIdx(verIdx - 1), children: "下一版" })] })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", flex: 1, minWidth: 0, gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "AI提示词" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: !detailData ? "加载中…" : (visibleStoryboards.length === 0 ? "(暂无分镜提示词)" : renderStoryboardBlocks((visibleStoryboards[sbIdx] || {}).script_text)) })] }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8 }, children: [detailData && visibleStoryboards.length > 1 && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: sbIdx >= visibleStoryboards.length - 1, onClick: () => setSbVerIdx(sbIdx + 1), children: "上一版" }), detailData && visibleStoryboards.length > 1 && (0, react_jsx_runtime.jsx)("span", { class
})]
}), csDetail && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setCsDetail(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: "原创选题脚本" + (csDetail && csDetail.title ? " — " + csDetail.title : "") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setCsDetail(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", padding: "0 2px 8px" }, children: [(0, react_jsx_runtime.jsx)("span", { style: { padding: "1px 8px", borderRadius: 10, fontSize: 12, background: "var(--mcn-brand,#4176e6)", color: "#fff", whiteSpace: "nowrap" }, children: "原创选题创作" }), csDetail && csDetail.id != null && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "AI脚本 #" + csDetail.id }), csDetail && csDetail.account_name && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: csDetail.account_name })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, style: { maxHeight: "60vh", overflow: "auto" }, children: csDetailData ? mdToReact(csDetailData.script_text || "(暂无脚本内容)") : "加载中…" }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, paddingTop: 10, borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))" }, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, style: { color: "#d9534f", borderColor: "#d9534f" }, onClick: () => setCsDel(csDetail), children: "删除" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, disabled: csBusy || !csDetailData, title: "发起 AI 脚本复盘(无源模式,独立 10 维评分)", onClick: csStartReview, children: csBusy ? "提交中…" : "AI 脚本复盘" })] })]
})]
}),
csDel && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setCsDel(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除原创选题脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: csDel ? `将删除原创选题 AI脚本 #${csDel.id}(无源视频)及其关联复盘数据,删除后不可恢复。` : "" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setCsDel(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: csConfirmDelete, children: "确认删除" })] })]
})]
}),
csToast && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.toast, children: csToast }),
reviewRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setReviewRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: `发起复盘 — AI脚本 #${reviewRow.id}` }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: (reviewRow.video_id > 0 ? "三类复盘独立存储;同一对象已复盘的直接查看,不会重复发起。" : "原创选题脚本无原视频:仅支持 AI 脚本复盘(独立 10 维评分)。") }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 8, margin: "12px 0" }, children: (() => {
const ai = reviewRow.review_ai;
const src = reviewRow.review_source;
const cmp = reviewRow.review_compare;
// 原创选题(video_id 空,无源视频):仅支持 AI 复盘;source/compare 模式隐藏(后端会 400)
const noSource = !(reviewRow.video_id > 0);
const modes = [
{ key: "ai", name: "AI 脚本复盘", desc: noSource ? "对原创选题脚本独立分析(独立 10 维评分制)" : "对 AI 写脚本独立分析(10 维评分制)", done: !!(ai && ai.status === "done"), running: !!(ai && ai.status === "running"), rev: ai || null },
...(noSource ? [] : [
{ key: "source", name: "原视频脚本复盘", desc: "对源脚本独立分析(按视频共享一份,避免重复复盘)", done: !!(src && src.status === "done"), running: !!(src && src.status === "running"), rev: src || null },
{ key: "compare", name: "对比分析", desc: "AI vs 源 — 复用两份独立复盘做三段式对比(不重新分析)", done: !!(cmp && cmp.status === "done"), running: !!(cmp && cmp.status === "running"), rev: cmp || null, prereq: !(ai && ai.status === "done" && src && src.status === "done") },
]),
];
return modes.map((m) => (0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, background: "var(--dsw-alias-bg-base)" },
children: [(0, react_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, fontSize: 13 }, children: m.name }), (0, react_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: m.done ? `已复盘(${fmtRevTime(m.rev && m.rev.updated_time)})· 点击查看` : (m.running ? "复盘进行中…" : m.desc) })] }), m.running ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: true, children: "进行中" }) : m.done ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, onClick: () => { const rid = m.rev && m.rev.id; if (rid) { setReviewRow(null); openReviewDetail(rid); } }, children: "查看" }) : m.prereq ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: true, title: "需先完成 AI 脚本复盘 + 原视频脚本复盘", children: "待前置" }) : (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm + " " + McnNav_module_css_default.btnPrimary, disabled: reviewBusy, onClick: () => startReview(m.key), children: "发起" })]
}, m.key));
})() }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setReviewRow(null), children: "关闭" })] })]
})]
}), reviewDetail && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setReviewDetail(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: `${reviewDetailData ? reviewTypeName(reviewDetailData.type) : "复盘详情"}${reviewDetailData && reviewDetailData.verdict ? " · " + reviewDetailData.verdict : ""}` }), (0, react_jsx_runtime.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [reviewDetailData && reviewDetailData.content ? (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, title: "查看完整复盘报告(左=源视频 / 右=AI脚本 / 下=对比)", onClick: () => openFullReport(reviewDetailData), children: "完整报告" }) : null, (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setReviewDetail(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] })] }), (0, react_jsx_runtime.jsx)("div", { style: { padding: "12px 16px", maxHeight: "72vh", overflow: "auto" }, children: !reviewDetailData ? "加载中…" : (() => {
let dimsObj = {};
try { dimsObj = JSON.parse(reviewDetailData.dims_json || "{}"); } catch (e) {}
const DIMS_ALL = [["共鸣和共情", "创意看点"], ["设定承接", "设定承接"], ["框架节奏", "框架节奏"], ["钩子设置", "爆款开场"], ["叙事逻辑", "叙事逻辑"], ["极致事件", "极致事件"], ["格式规范性", "格式规范性"], ["台词口语化", "台词口语化"], ["内容合规", "内容合规"], ["教程完整链", "教程完整链"], ["其他", "其他"]];
const scoreColor = (v) => (v >= 75 ? "var(--mcn-ok,#3a9e6d)" : v >= 60 ? "#d99a2b" : "#d9534f");
const scoreBar = (dims, keyPrefix) => DIMS_ALL.map((k) => {
const v = dims[k[0]];
const has = typeof v === "number" && Number.isFinite(v);
return (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }, children: [(0, react_jsx_runtime.jsx)("span", { style: { width: 96, fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: k[1] }), (0, react_jsx_runtime.jsx)("div", { style: { flex: 1, height: 6, borderRadius: 3, background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", overflow: "hidden" }, children: has && (0, react_jsx_runtime.jsx)("div", { style: { width: v + "%", height: "100%", background: scoreColor(v) } }) }), (0, react_jsx_runtime.jsx)("span", { style: { width: 36, textAlign: "right", fontSize: 12, fontWeight: 600, color: has ? scoreColor(v) : "var(--dsw-alias-label-secondary,#8a8f98)" }, children: has ? v : "—" })] }, keyPrefix + k[0]);
});
let issues = [];
try { issues = JSON.parse(reviewDetailData.issues_json || "[]"); } catch (e) {}
const sevColor = { 高: "#d9534f", 中: "#d99a2b", 低: "#3a9e6d" };
if (reviewDetailData.type === "compare") {
const aiD = dimsObj.ai || {};
const srcD = dimsObj.source || {};
let t = {};
try { t = JSON.parse(reviewDetailData.total || "{}"); } catch (e) {}
return (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 12, marginBottom: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { style: { flex: 1, padding: 10, border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8 }, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: `AI 总分 ${fmtScore(t.ai)}` }), scoreBar(aiD, "c-ai-")] }) }), (0, react_jsx_runtime.jsx)("div", { style: { flex: 1, padding: 10, border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8 }, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: `源脚本 总分 ${fmtScore(t.source)}` }), scoreBar(srcD, "c-src-")] }) })] }), reviewDetailData.summary && (0, react_jsx_runtime.jsx)("div", { style: { marginBottom: 10, fontSize: 13 }, children: "综合判断:" + reviewDetailData.summary }), issues.length > 0 && (0, react_jsx_runtime.jsxs)("div", { style: { marginBottom: 10 }, children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "问题清单(AI 差的点)" }), issues.map((it, i) => (0, react_jsx_runtime.jsxs)("div", { style: { fontSize: 12, marginBottom: 3, display: "flex", gap: 6 }, children: [(0, react_jsx_runtime.jsx)("span", { style: { color: sevColor[it.severity] || "#8a8f98", fontWeight: 600 }, children: it.severity || "低" }), (0, react_jsx_runtime.jsx)("span", { children: it.issue }), it.suggestion && (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "→ " + it.suggestion })] }, "iss" + i)) ] }), reviewDetailData.content && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.md, style: { borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", paddingTop: 10 }, children: mdToReact(reviewDetailData.content) })] });
}
return (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 12, marginBottom: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { style: { flex: 1, padding: 10, border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8 }, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: `总分 ${fmtScore(Number(reviewDetailData.total))}` }), scoreBar(dimsObj, "d-")] }) })] }), reviewDetailData.summary && (0, react_jsx_runtime.jsx)("div", { style: { marginBottom: 10, fontSize: 13 }, children: "综合评语:" + reviewDetailData.summary }), issues.length > 0 && (0, react_jsx_runtime.jsxs)("div", { style: { marginBottom: 10 }, children: [(0, react_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "问题清单" }), issues.map((it, i) => (0, react_jsx_runtime.jsxs)("div", { style: { fontSize: 12, marginBottom: 3, display: "flex", gap: 6 }, children: [(0, react_jsx_runtime.jsx)("span", { style: { color: sevColor[it.severity] || "#8a8f98", fontWeight: 600 }, children: it.severity || "低" }), (0, react_jsx_runtime.jsx)("span", { children: it.issue }), it.locate && (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "[" + it.locate + "]" }), it.suggestion && (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "→ " + it.suggestion }), it.step && (0, react_jsx_runtime.jsx)("span", { style: { color: "var(--mcn-brand,#4176e6)" }, children: it.step })] }, "iss" + i)) ] }), reviewDetailData.content && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.md, style: { borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", paddingTop: 10 }, children: mdToReact(reviewDetailData.content) })] });
})() })]
})]
}), fullReport && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "30px" },
onClick: () => setFullReport(false),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: `完整报告 — AI脚本 #${(reviewDetailData && reviewDetailData.rewrite_id) || ""}(左=源视频 / 右=AI脚本 / 下=对比)` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setFullReport(false), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { style: { padding: "12px 16px", maxHeight: "85vh", overflow: "auto" }, children: !fullReportData ? "加载中…" : (fullReportData.empty ? "(未能加载完整报告数据)" : (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 12 }, children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, style: { flex: 1, minWidth: 0 }, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "源视频复盘" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: fullReportData.source ? mdToReact(fullReportData.source.content || "(该复盘无报告内容)") : "(该视频源脚本尚未复盘)" })] }) }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, style: { flex: 1, minWidth: 0 }, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "AI脚本复盘" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: fullReportData.ai ? mdToReact(fullReportData.ai.content || "(该复盘无报告内容)") : "(该脚本尚未复盘)" })] }) })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPane, children: (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwPaneTitle, children: "对比分析" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, children: fullReportData.compare ? mdToReact(fullReportData.compare.content || "(该对比分析无报告内容)") : "(尚未做对比分析,需先完成 AI 脚本复盘 + 原视频脚本复盘)" })] }) })] })) })]
})]
}), delReviewRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setDelReviewRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除复盘数据" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: `将删除 AI脚本 #${delReviewRow.id} 的 AI 复盘与对比分析数据(源视频复盘为视频级共享保留,脚本本身保留)。删除后如需可重新发起复盘。` }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setDelReviewRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, onClick: delReview, children: "确认删除" })] })]
})]
}), toast && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.toast, children: toast })]
});
}
//#endregion
//#region AI会话:MCN 工作台内查看插件创建的 dsh_* 任务会话(列表分组 + 对话详情,读会话文件 zstd 解压)
function AiSessionsView({ onBack }) {
const [items, setItems] = (0, react.useState)([]);
const [detail, setDetail] = (0, react.useState)(null); // {id, messages, error}
const detailScrollRef = (0, react.useRef)(null);
// 详情打开/消息变化 → 自动滚动到最新(底部)
(0, react.useEffect)(() => {
if (detail && detailScrollRef.current) {
detailScrollRef.current.scrollTop = detailScrollRef.current.scrollHeight;
}
}, [detail]);
// 详情打开时轮询刷新(每 5 秒,执行中的会话新内容自动加载到查看窗口)
(0, react.useEffect)(() => {
const id = detail && detail.id;
if (!id) return;
const t = setInterval(() => {
fetch("/mcn/api/session/detail?id=" + encodeURIComponent(id))
.then((r) => r.json())
.then((d) => { if (d.ok) setDetail({ id, messages: d.messages || [], error: d.error || null }); })
.catch(() => {});
}, 5000);
return () => clearInterval(t);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [detail && detail.id]);
const load = () => {
fetch("/mcn/api/sessions")
.then((r) => r.json())
.then((d) => { if (d.ok) setItems(d.items || []); })
.catch(() => {});
};
(0, react.useEffect)(() => {
load();
const t = setInterval(load, 8000);
return () => clearInterval(t);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);
const openDetail = (id) => {
setDetail({ id, messages: null, error: null });
fetch("/mcn/api/session/detail?id=" + encodeURIComponent(id))
.then((r) => r.json())
.then((d) => { if (d.ok) setDetail({ id, messages: d.messages || [], error: d.error || null }); else setDetail({ id, messages: [], error: d.error || "加载失败" }); })
.catch(() => setDetail({ id, messages: [], error: "加载失败" }));
};
const fmtAgo = (iso) => {
if (!iso) return "";
const diff = Date.now() - new Date(iso).getTime();
if (diff < 60 * 1000) return "刚刚";
if (diff < 3600 * 1000) return Math.round(diff / 60000) + " 分钟前";
if (diff < 86400 * 1000) return Math.round(diff / 3600000) + " 小时前";
return Math.round(diff / 86400000) + " 天前";
};
const groups = {};
for (const it of items) { (groups[it.group] = groups[it.group] || []).push(it); }
const groupOrder = ["创作任务", "解析任务", "分镜", "计划任务", "对标创作", "其他"];
return (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.body,
children: [(0, react_jsx_runtime.jsx)("style", { children: "@keyframes mcnSpin{to{transform:rotate(360deg)}}" }), (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.toolbar,
children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, title: "返回工作台", onClick: onBack, children: "←" }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.title, children: "MCN任务会话" }), (0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto", fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: "插件创建的 MCN 任务会话 · 共 " + items.length + " 个" })]
}), detail ? (0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0 },
children: [(0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 4px" },
children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, onClick: () => setDetail(null), children: "← 返回列表" }), (0, react_jsx_runtime.jsx)("span", { style: { fontWeight: 600 }, children: detail.id }), detail.messages && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: `共 ${detail.messages.length} 条消息` })]
}), detail.error && (0, react_jsx_runtime.jsx)("div", { style: { padding: 12, color: "#d9534f", fontSize: 13 }, children: detail.error }), (0, react_jsx_runtime.jsx)("div", { ref: detailScrollRef, style: { flex: 1, overflow: "auto", display: "flex", flexDirection: "column", gap: 10, padding: "4px 4px 20px" }, children: !detail.messages ? "加载中…" : detail.messages.length === 0 ? (0, react_jsx_runtime.jsx)("div", { style: { color: "var(--dsw-alias-label-secondary,#8a8f98)", padding: 20, textAlign: "center" }, children: "(暂无对话消息)" }) : detail.messages.map((m, i) => (0, react_jsx_runtime.jsxs)("div", {
style: m.role === "assistant" ? { background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 10, padding: "10px 14px" } : m.role === "user" ? { background: "var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-1))", borderRadius: 10, padding: "10px 14px" } : { borderRadius: 8, padding: "4px 10px", fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))" },
children: [(0, react_jsx_runtime.jsx)("div", { style: { fontSize: 11, fontWeight: 600, marginBottom: 4, color: m.role === "user" ? "var(--mcn-brand,#4176e6)" : m.role === "assistant" ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-secondary,#8a8f98)" }, children: m.role === "user" ? "用户" : m.role === "assistant" ? "AI" : ("工具: " + (m.name || "")) }), m.role === "tool" ? (0, react_jsx_runtime.jsx)("div", { style: { fontFamily: "monospace", fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" }, children: m.args }) : (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.md, children: mdToReact(m.text || "") })]
}, "m" + i)) })]
}) : items.length === 0 ? (0, react_jsx_runtime.jsx)("div", { style: { padding: "40px 20px", textAlign: "center", color: "var(--dsw-alias-label-secondary)", font: "var(--dsw-font-s-14)" }, children: "暂无插件创建的 AI 会话(改写/复盘/解析任务执行后出现)" }) : (0, react_jsx_runtime.jsx)("div", {
style: { display: "flex", flexDirection: "column", gap: 10, overflow: "auto", paddingBottom: 20 },
children: groupOrder.filter((g) => groups[g]).map((g) => (0, react_jsx_runtime.jsxs)("div", {
children: [(0, react_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-secondary,#8a8f98)", margin: "10px 0 6px" }, children: g }), groups[g].map((it) => (0, react_jsx_runtime.jsx)("div", {
style: { display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, background: "var(--dsw-alias-bg-base)", cursor: "pointer" },
onClick: () => openDetail(it.id),
children: [(0, react_jsx_runtime.jsx)("span", { style: { width: 8, height: 8, borderRadius: "50%", background: it.status === "running" ? "#3a9e6d" : "var(--dsw-alias-border)", flexShrink: 0 } }), (0, react_jsx_runtime.jsx)("span", { style: { fontWeight: 500, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: it.id }), it.status === "running" && (0, react_jsx_runtime.jsxs)("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, whiteSpace: "nowrap" }, children: [(0, react_jsx_runtime.jsx)("span", { style: { width: 12, height: 12, borderRadius: "50%", border: "2px solid rgba(58,158,109,.25)", borderTopColor: "#3a9e6d", animation: "mcnSpin 0.8s linear infinite", display: "inline-block", flexShrink: 0 } }), (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 11, color: "#3a9e6d" }, children: "执行中" })] }), (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: fmtAgo(it.updated) })]
}, it.id))]
}, g))
})]
});
}
//#endregion
//#region 抖音账号日榜板块({工作区}/抖音日榜 数据:日期/赛道筛选 + 数值排序)
/** djb2 哈希(与宿主端 index.js hashKey 一致),用于匹配导入任务 key */
function mcnHashKey(s) {
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
return h.toString(16).padStart(8, "0");
}
/** 周榜行对应的导入任务 key(与 /mcn/api/external/stage 提交参数一致:name + url,抖音号缺省为空串) */
function importTaskKey(row) {
return "acc:" + mcnHashKey(row.accountName + "|" + "|" + (row.profileUrl || ""));
}
function RankingSection() {
// 四榜:week=账号周榜 / day=账号日榜 / hot=视频热榜 / likes=点赞榜;默认排序键随榜
const BOARDS = [{ id: "week", name: "账号周榜" }, { id: "day", name: "账号日榜" }, { id: "hot", name: "视频热榜" }, { id: "likes", name: "点赞榜" }];
const defSort = (b) => (b === "hot" ? "likeCount" : b === "likes" ? "add_digg_count" : "comprehensiveScore");
const [board, setBoard] = (0, react.useState)("week");
const boardRef = (0, react.useRef)("week");
boardRef.current = board;
// 各榜独立记忆 date/category/sortBy/order/page(简单 map,无需缓存完整数据)
const memRef = (0, react.useRef)({});
const memOf = (b) => {
if (!memRef.current[b]) memRef.current[b] = { date: "", category: "", sortBy: defSort(b), order: "desc", page: 1 };
return memRef.current[b];
};
const [dates, setDates] = (0, react.useState)([]);
const [categories, setCategories] = (0, react.useState)([]);
const [date, setDate] = (0, react.useState)("");
const [category, setCategory] = (0, react.useState)("");
const [items, setItems] = (0, react.useState)([]);
const [total, setTotal] = (0, react.useState)(0);
const [loading, setLoading] = (0, react.useState)(false);
const [sortBy, setSortBy] = (0, react.useState)("comprehensiveScore");
const [order, setOrder] = (0, react.useState)("desc");
const [page, setPage] = (0, react.useState)(1);
const [importing, setImporting] = (0, react.useState)(null);
const [runningKeys, setRunningKeys] = (0, react.useState)(new Set());
const runningKeysRef = (0, react.useRef)(new Set());
const [msg, setMsg] = (0, react.useState)(null);
// 榜单更新流程(红狐 API 按次计费;30 分钟冷却防重)
const [upd, setUpd] = (0, react.useState)(null); // GET /ranking/update 预检结果
const [updForce, setUpdForce] = (0, react.useState)(null); // 409 → 「仍要更新(force)」二次确认(含 error 文本)
const [updRunning, setUpdRunning] = (0, react.useState)(false);
const updPollRef = (0, react.useRef)(null);
const stopUpdPoll = () => { if (updPollRef.current) { clearInterval(updPollRef.current); updPollRef.current = null; } };
const syncRunning = () => {
fetch("/mcn/api/task/status")
.then((r) => r.json())
.then((res) => {
const set = new Set();
for (const t of res.imports || []) {
if (t.status === "running" && t.id.startsWith("acc:")) set.add(t.id);
}
let finished = false;
for (const k of runningKeysRef.current) if (!set.has(k)) finished = true;
runningKeysRef.current = set;
setRunningKeys(set);
if (finished) loadRankRef.current();
})
.catch(() => {});
};
const importRank = (row) => {
const key = importTaskKey(row);
if (importing || runningKeysRef.current.has(key)) return;
setImporting(row.rank);
setMsg(null);
runningKeysRef.current = new Set(runningKeysRef.current).add(key);
setRunningKeys(new Set(runningKeysRef.current));
fetch("/mcn/api/external/stage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ account: { name: row.accountName, url: row.profileUrl, category: row.category || "" } }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setMsg("已提交,AI 正在获取账号信息与视频列表,请到对话中确认导入");
})
.catch((e) => setMsg("提交失败: " + e.message))
.finally(() => { setImporting(null); syncRunning(); });
};
const openAccountDetail = (row) => {
try { if (window.__mcnNav) window.__mcnNav.open("douyin-account-detail", { accountId: row.dbId }); } catch (e) {}
};
const loadRank = (d = date, c = category, s = sortBy, o = order, p = page, b = board) => {
setLoading(true);
fetch("/mcn/api/ranking?board=" + encodeURIComponent(b) + "&date=" + encodeURIComponent(d || "") + "&category=" + encodeURIComponent(c || "") + "&sortBy=" + encodeURIComponent(s) + "&order=" + encodeURIComponent(o) + "&page=" + p + "&pageSize=20")
.then((r) => r.json())
.then((res) => {
if (!res.ok) return;
if (b !== boardRef.current) return; // 切榜竞态:丢弃过期响应
setDates(res.dates || []);
setCategories(res.categories || []);
setDate(res.date || "");
setCategory(res.category || "");
setItems(res.items || []);
setTotal(res.total || 0);
setPage(res.page || 1);
const m = memOf(b);
m.date = res.date || ""; m.category = res.category || ""; m.sortBy = s; m.order = o; m.page = res.page || 1;
})
.catch(() => {})
.finally(() => { if (b === boardRef.current) setLoading(false); });
};
// 供 5s 导入状态轮询 / 更新完成后「刷新当前榜」使用:始终取最新一次渲染的 loadRank(避免闭包使用旧榜单/旧筛选)
const loadRankRef = (0, react.useRef)(loadRank);
loadRankRef.current = loadRank;
const switchBoard = (b) => {
if (b === board) return;
const mOld = memOf(board);
mOld.date = date; mOld.category = category; mOld.sortBy = sortBy; mOld.order = order; mOld.page = page;
const m = memOf(b);
setBoard(b);
setDate(m.date); setCategory(m.category); setSortBy(m.sortBy); setOrder(m.order); setPage(m.page);
loadRank(m.date, m.category, m.sortBy, m.order, m.page, b);
};
(0, react.useEffect)(() => {
loadRankRef.current();
syncRunning();
const t = setInterval(syncRunning, 5000);
return () => { clearInterval(t); stopUpdPoll(); };
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);
const toggleSort = (key) => {
const nextOrder = sortBy === key ? (order === "asc" ? "desc" : "asc") : (key === "rank" ? "asc" : "desc");
setSortBy(key);
setOrder(nextOrder);
loadRank(date, category, key, nextOrder, 1);
};
// 日期切换(左右箭头):dates 已按日期倒序,index 0 = 最新
const dateIdx = dates.indexOf(date);
const shiftDate = (delta) => {
const idx = dateIdx + delta;
if (idx < 0 || idx >= dates.length) return;
const nd = dates[idx];
setDate(nd);
loadRank(nd, category, sortBy, order, 1);
};
const totalPages = Math.max(1, Math.ceil(total / 20));
const num = (v) => (v == null || v === "") ? "-" : String(v);
const isAccBoard = board === "week" || board === "day";
// 各榜表头 + 可排序键(null=不可排序列)
const headers = isAccBoard
? [{ t: "排名", k: "rank", w: 60 }, { t: "赛道", k: null }, { t: "账号", k: null, w: 140 }, { t: "综合得分", k: "comprehensiveScore", w: 90 }, { t: "粉丝", k: "followers" }, { t: "新增粉丝", k: "newFans" }, { t: "新增点赞", k: "newLikes" }, { t: "新增评论", k: "newComments" }, { t: "新增分享", k: "newShares" }, { t: "操作", k: null, center: true }]
: board === "hot"
? [{ t: "排名", k: "rank", w: 60 }, { t: "赛道", k: null }, { t: "作品标题", k: null }, { t: "作者", k: null }, { t: "点赞", k: "likeCount" }, { t: "评论", k: "comment_count" }, { t: "分享", k: "share_count" }, { t: "收藏", k: "collect_count" }, { t: "发布时间", k: "publish_time" }]
: [{ t: "排名", k: "rank", w: 60 }, { t: "赛道", k: null }, { t: "作品", k: null }, { t: "作者", k: null }, { t: "新增点赞", k: "add_digg_count" }, { t: "新增收藏", k: "add_collect_count" }, { t: "新增评论", k: "add_comment_count" }, { t: "新增分享", k: "add_share_count" }, { t: "发布时间", k: "publish_time" }];
// 榜单更新:预检 → 确认 → POST → 轮询直至完成(5s × ≤15 分钟)→ 刷新当前榜
const precheckUpdate = () => {
if (updRunning) return;
fetch("/mcn/api/ranking/update")
.then((r) => r.json())
.then((d) => { if (d && d.ok) setUpd(d); else setUpd({ ok: true, error: (d && d.error) || "状态查询失败" }); })
.catch(() => setUpd({ ok: true, error: "状态查询失败" }));
};
const submitUpdate = (force) => {
setUpd(null);
setUpdForce(null);
fetch("/mcn/api/ranking/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(force ? { force: true } : {}),
})
.then((r) => r.json().then((d) => ({ status: r.status, d })))
.then(({ status, d }) => {
if (d && d.ok) {
setMsg(d.message || "已开始更新榜单(红狐 API 按次计费)");
startUpdPoll();
return;
}
if (status === 409) {
const err = (d && d.error) || "榜单更新冷却中或已在执行";
setMsg(err);
setUpdForce({ error: err, remainingMs: d && d.remainingMs });
return;
}
setMsg((d && d.error) || "发起更新失败");
})
.catch(() => { setMsg("发起更新失败"); setUpd(null); });
};
const startUpdPoll = () => {
stopUpdPoll();
setUpdRunning(true);
let n = 0;
updPollRef.current = setInterval(() => {
n++;
fetch("/mcn/api/ranking/update")
.then((r) => r.json())
.then((d) => {
if (!d || !d.ok) return;
if (!d.running) {
stopUpdPoll();
setUpdRunning(false);
setMsg("榜单更新完成");
loadRankRef.current();
} else if (n >= 180) {
stopUpdPoll();
setUpdRunning(false);
setMsg("等待更新超过 15 分钟,已停止自动刷新,请稍后手动查看");
loadRankRef.current();
}
})
.catch(() => {});
}, 5000);
};
const fmtUpdAt = (t) => t ? String(t).slice(0, 16).replace("T", " ") : "";
const updText = upd ? (upd.running ? "榜单更新进行中…(不可重复触发)" : (upd.canUpdate === false || upd.remainingMs > 0 ? "距上次更新仍在冷却中,剩余约 " + Math.ceil((upd.remainingMs || 0) / 60000) + " 分钟;如需提前抓取可强制更新" : "上次更新时刻:" + (upd.lastAt ? fmtUpdAt(upd.lastAt) : "首次抓取") + (upd.lastError ? "(上次失败:" + upd.lastError + ")" : ""))) : "";
// ===== V1.1 热点数据:账号列表/AI写脚本 同款弹性行 grid(替换原 <table>) =====
// 弹性列=账号名/作品标题(吃余量);数值右对齐、文字左对齐、排名与操作居中;表头不换行,点击排序保留
const rkTpl = isAccBoard
? "40px 84px minmax(150px,1fr) 88px 88px 88px 88px 88px 88px 104px"
: "40px 84px minmax(160px,1fr) 108px 88px 88px 88px 88px 150px";
const rkAl = isAccBoard
? ["center", "left", "left", "right", "right", "right", "right", "right", "right", "center"]
: ["center", "left", "left", "left", "right", "right", "right", "right", "left"];
const rkCS = (al, clip) => ({ padding: "9px 10px", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", textAlign: al || "left", minWidth: 0, whiteSpace: "nowrap", overflow: clip ? "hidden" : undefined, textOverflow: clip ? "ellipsis" : undefined, boxSizing: "border-box" });
const rkHeadEls = headers.map((h, i) => react.createElement("div", { key: "h" + i, title: h.k ? "点击排序" : undefined, style: Object.assign(rkCS(rkAl[i]), { color: "var(--dsw-alias-label-secondary,#6b7280)", fontWeight: 600, fontSize: 13, cursor: h.k ? "pointer" : "default" }), onClick: h.k ? () => toggleSort(h.k) : undefined }, h.k ? h.t + (sortBy === h.k ? (order === "asc" ? " ↑" : " ↓") : "") : h.t));
const rkRowEls = items.map((row, idx) => {
const cellEls = [];
const pushCell = (al, clip, node, ck, extra) => cellEls.push(react.createElement("div", { key: "c" + (ck == null ? cellEls.length : ck), style: Object.assign(rkCS(al, clip), extra || {}) }, node));
if (isAccBoard) {
const rowKey = importTaskKey(row);
const rowImporting = importing === row.rank || runningKeys.has(rowKey);
pushCell("center", false, row.rank ?? idx + 1, 0);
pushCell("left", true, row.category || "-", 1);
pushCell("left", false, row.profileUrl ? react.createElement("a", { className: McnNav_module_css_default.link, href: row.profileUrl, target: "_blank", rel: "noopener noreferrer", title: "打开账号主页" }, row.accountName || "-") : (row.accountName || "-"), 2, { whiteSpace: "normal", wordBreak: "break-all" });
pushCell("right", false, num(row.comprehensiveScore), 3);
pushCell("right", false, num(row.followers), 4);
pushCell("right", false, num(row.newFans), 5);
pushCell("right", false, num(row.newLikes), 6);
pushCell("right", false, num(row.newComments), 7);
pushCell("right", false, num(row.newShares), 8);
pushCell("center", false, row.inDb ? react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, onClick: () => openAccountDetail(row), title: "查看账号详情" }, "查看") : react.createElement("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, disabled: rowImporting, onClick: () => importRank(row), title: "导入到账号数据库" }, rowImporting ? "导入中…" : "导入"), 9);
} else {
pushCell("center", false, row.rank ?? idx + 1, 0);
pushCell("left", true, row.category || "-", 1);
pushCell("left", true, react.createElement("span", { title: row.workUrl ? "打开作品链接" : String(row.workTitle || "") }, row.workTitle || "-"), 2);
pushCell("left", true, row.author || "-", 3);
const metricKeys = board === "hot" ? ["likeCount", "comment_count", "share_count", "collect_count"] : ["add_digg_count", "add_collect_count", "add_comment_count", "add_share_count"];
metricKeys.forEach((k, ki) => pushCell("right", false, num(row[k]), 4 + ki));
pushCell("left", false, num(row.publish_time), 8);
}
return react.createElement("div", { key: isAccBoard ? ("rkacc" + (row.dbId || row.accountName || idx)) : ("rkvid" + (row.rank ?? idx + 1) + "-" + (row.workTitle || row.author || idx)), style: { display: "grid", gridTemplateColumns: rkTpl, alignItems: "center" } }, cellEls);
});
const rkGridList = react.createElement("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
react.createElement("div", { style: { display: "grid", gridTemplateColumns: rkTpl, alignItems: "center", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", position: "sticky", top: 0, zIndex: 2 } }, rkHeadEls),
rkRowEls);
return (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rankSection,
children: [
(0, react_jsx_runtime.jsx)("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, title: "返回工作台", onClick: () => { try { window.__mcnNav && window.__mcnNav.back(); } catch (e) {} }, children: "←" }), (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 16, fontWeight: 600 }, children: "热点数据" })]}),
(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.toolbar,
children: [(0, react_jsx_runtime.jsx)("div", { style: { display: "inline-flex", gap: 4, flexWrap: "wrap" }, children: BOARDS.map((b) => (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.tab + (board === b.id ? " " + McnNav_module_css_default.tabActive : ""), onClick: () => switchBoard(b.id), children: b.name }, b.id)) }), (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }), (0, react_jsx_runtime.jsx)("select", {
className: McnNav_module_css_default.select,
value: category,
onChange: (e) => { setCategory(e.target.value); loadRank(date, e.target.value, sortBy, order, 1); },
title: "赛道筛选(白名单赛道)",
children: [(0, react_jsx_runtime.jsx)("option", { value: "", children: "全部赛道" })].concat(categories.map((c) => (0, react_jsx_runtime.jsx)("option", { value: c, key: c, children: c })))
}), (0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.dateBtn,
title: "前一天",
disabled: dateIdx <= 0,
onClick: () => shiftDate(-1),
children: "◀"
}), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, title: "选择日期", children: date || "-" }), (0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.dateBtn,
title: "后一天",
disabled: dateIdx < 0 || dateIdx >= dates.length - 1,
onClick: () => shiftDate(1),
children: "▶"
}), (0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm,
disabled: updRunning,
title: "更新榜单数据(账号日榜 1 次 + 账号周榜 5 赛道 + 视频热榜/点赞榜;红狐 API 按次计费,30 分钟冷却防重)",
onClick: precheckUpdate,
children: updRunning ? "更新中…" : "⟳ 更新榜单"
})]
}), (msg || updRunning) && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.pagerInfo, style: { margin: 0, color: String(msg || "").includes("失败") || String(msg || "").includes("冷却") ? "#d9534f" : undefined }, children: updRunning ? "榜单更新中…系统会自动检测完成状态(最长等待 15 分钟)" : msg }), loading ? (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.loading, children: "加载中…" }) : items.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.empty, children: "暂无数据——点击右上角「更新榜单」抓取最新一期(账号日榜 1 次 + 账号周榜 5 赛道 + 视频热榜/点赞榜;红狐 API 按次计费)" }) : (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.tableWrap,
children: rkGridList
}), items.length > 0 && !loading && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.pager,
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerTotal, children: `共 ${total} 条` }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.pagerInfo, children: `第 ${page} / ${totalPages} 页` }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page <= 1 || loading, onClick: () => loadRank(date, category, sortBy, order, page - 1), children: "上一页" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, disabled: page >= totalPages || loading, onClick: () => loadRank(date, category, sortBy, order, page + 1), children: "下一页" })]
}), upd && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setUpd(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认更新榜单数据" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: updText }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: "预计调用范围:账号日榜全品类 1 次 + 账号周榜 5 赛道 + 视频热榜/点赞榜。红狐 API 按次计费,触发后 30 分钟内不可重复(冷却)。" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setUpd(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, disabled: upd.running, onClick: () => submitUpdate(false), children: upd.running ? "已在执行" : "确认更新" })] })]
})]
}), updForce && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setUpdForce(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "仍要更新?(将强制触发)" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: (updForce.error || "榜单更新冷却中或已在执行") + (updForce.remainingMs ? ",剩余约 " + Math.ceil(updForce.remainingMs / 60000) + " 分钟" : "") + "。强制更新将忽略冷却/防重直接调用红狐 API(按次计费),请确认成本。仍要更新请点「仍要更新」。若提示已在执行,请等待完成即可。" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setUpdForce(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: () => submitUpdate(true), children: "仍要更新" })] })]
})]
})]
});
}
//#endregion
//#region 首页「最近 AI 脚本」:/mcn/api/rewrite/list 前 8 条 done+标题非空;点击打开脚本(video_id>0 走 video/script,空走 custom-script 单列);原创支持 AI 复盘/删除
function RecentScriptsSection({ onOpenAll }) {
const GEN_LABELS = { rewrite: "参考选题创作", new: "原创选题创作", abc: "ABC融合创作" };
const [items, setItems] = (0, react.useState)([]);
const [viewRow, setViewRow] = (0, react.useState)(null);
const [viewData, setViewData] = (0, react.useState)(null);
const [delRow, setDelRow] = (0, react.useState)(null);
const [busy, setBusy] = (0, react.useState)(false);
const [toast, setToast] = (0, react.useState)(null);
const load = () => {
fetch("/mcn/api/rewrite/list")
.then((r) => r.json())
.then((d) => {
if (!d.ok) return;
const list = (d.items || [])
.filter((x) => x.status === "done" && x.title && x.script_len > 0)
.sort((a, b) => String(b.created_time || "").localeCompare(String(a.created_time || "")) || b.id - a.id)
.slice(0, 8);
setItems(list);
})
.catch(() => {});
};
(0, react.useEffect)(() => { load(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []);
(0, react.useEffect)(() => {
if (!toast) return;
const t = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(t);
}, [toast]);
const isOrig = (row) => !(row.video_id > 0);
const methodOf = (row) => row.genMethod || row.gen_method || (isOrig(row) ? "new" : "rewrite");
// 评分:优先 AI 复盘 total,回退 对比分析 的 ai 段
const scoreOf = (row) => {
if (row.review_ai && row.review_ai.status === "done") return Number(row.review_ai.total);
if (row.review_compare && row.review_compare.status === "done") {
try { return Number(JSON.parse(row.review_compare.total || "{}").ai); } catch (e) { return NaN; }
}
return NaN;
};
const fmtT = (t) => String(t || "").slice(0, 16).replace("T", " ");
const scoreColor = (v) => (v >= 75 ? "var(--mcn-ok,#3a9e6d)" : v >= 60 ? "#d99a2b" : "#d9534f");
const openScript = (row) => {
setViewRow(row);
setViewData(null);
if (isOrig(row)) {
// 原创选题(无源视频):custom-script 单列
fetch("/mcn/api/custom-script?id=" + row.id)
.then((r) => r.json())
.then((d) => { setViewData({ orig: true, script: d.ok && d.script ? d.script.script_text : null }); })
.catch(() => setViewData({ orig: true, script: null }));
return;
}
fetch("/mcn/api/video/script?videoId=" + row.video_id)
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setViewData({ orig: false, script: null }); return; }
const rew = (d.rewrites || []).find((x) => x.id === row.id);
setViewData({ orig: false, script: rew ? rew.script_text : null });
})
.catch(() => setViewData({ orig: false, script: null }));
};
const startReview = () => {
if (!viewRow || busy) return;
setBusy(true);
fetch("/mcn/api/review/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ logId: viewRow.id, type: "ai" }),
})
.then((r) => r.json())
.then((d) => { setToast(d.ok ? (d.message || "已提交 AI 脚本复盘") : (d.error || "提交失败")); })
.catch(() => setToast("提交失败,请重试"))
.finally(() => setBusy(false));
};
const confirmDelete = () => {
if (!delRow) return;
const id = delRow.id;
setDelRow(null);
fetch("/mcn/api/rewrite/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) { setToast(d.error || "删除失败"); return; }
setToast("已删除");
if (viewRow && viewRow.id === id) { setViewRow(null); setViewData(null); }
setItems((prev) => prev.filter((x) => x.id !== id));
})
.catch(() => setToast("删除失败,请重试"));
};
return (0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", flexDirection: "column", gap: 10 },
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.toolbar,
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.title, children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.titleDot }), "最近 AI 脚本"] }), (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnSm, onClick: onOpenAll, children: "查看全部" })]
}), items.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.empty, children: "暂无 AI 脚本" }) : (0, react_jsx_runtime.jsx)("div", {
style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 8 },
children: items.map((row) => {
const sc = scoreOf(row);
const has = Number.isFinite(sc);
const orig = isOrig(row);
return (0, react_jsx_runtime.jsxs)("div", {
style: { display: "flex", flexDirection: "column", gap: 6, padding: "10px 12px", border: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", borderRadius: 8, background: "var(--dsw-alias-bg-base)", minWidth: 0 },
children: [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.link, title: orig ? "打开原创选题脚本(单列)" : "打开 AI 写脚本", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)", fontWeight: 600, lineHeight: "18px", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }, onClick: () => openScript(row), children: row.title }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 12 }, children: [(0, react_jsx_runtime.jsx)("span", { style: { padding: "0 6px", borderRadius: 9, background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: GEN_LABELS[methodOf(row)] || "AI写脚本" }), (0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto", fontWeight: 600, color: has ? scoreColor(sc) : "var(--dsw-alias-label-secondary,#8a8f98)" }, title: has ? "AI 复盘评分" : "暂无复盘评分", children: has ? sc : "-" })] }), (0, react_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)", whiteSpace: "nowrap" }, children: fmtT(row.created_time) + (row.script_len ? " · " + row.script_len + " 字" : "") })]
}, row.id);
})
}), viewRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { paddingTop: "50px" },
onClick: () => setViewRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.rwModal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" }, children: [(0, react_jsx_runtime.jsx)("span", { children: (isOrig(viewRow) ? "原创选题脚本" : "AI写脚本") + " — " + (viewRow.title || "") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.close, title: "关闭", onClick: () => setViewRow(null), children: (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: 14, height: 14, fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, react_jsx_runtime.jsx)("path", { d: "M18 6L6 18M6 6l12 12" }) }) })] }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.rwContent + " " + McnNav_module_css_default.md, style: { maxHeight: "60vh", overflow: "auto" }, children: !viewData ? "加载中…" : (viewData.script ? mdToReact(viewData.script) : "(暂无脚本内容)") }), (0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, paddingTop: 10, borderTop: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))" }, children: [(0, react_jsx_runtime.jsx)("span", { style: { marginRight: "auto", fontSize: 12, color: "var(--dsw-alias-label-secondary,#8a8f98)" }, children: "生成方式:" + (GEN_LABELS[methodOf(viewRow)] || "-") }), isOrig(viewRow) && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, style: { color: "#d9534f", borderColor: "#d9534f" }, onClick: () => setDelRow(viewRow), children: "删除" }), isOrig(viewRow) && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, disabled: busy, title: "发起 AI 脚本复盘(无源模式,独立 10 维评分)", onClick: startReview, children: busy ? "提交中…" : "AI 复盘" })] })]
})]
}), delRow && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
style: { alignItems: "center", paddingTop: 0 },
onClick: () => setDelRow(null),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalTitle, children: "确认删除脚本" }), (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.modalText, children: delRow ? `将删除 AI脚本 #${delRow.id}${isOrig(delRow) ? "(原创选题,无源视频)" : ""},删除后不可恢复。` : "" }), (0, react_jsx_runtime.jsxs)("div", { className: McnNav_module_css_default.modalBtns, children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn, onClick: () => setDelRow(null), children: "取消" }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary, style: { color: "#fff", background: "#d9534f", borderColor: "#d9534f" }, onClick: confirmDelete, children: "确认删除" })] })]
})]
}), toast && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.toast, children: toast })]
});
}
//#endregion
//#region 账号列表页面
//#region V1.0 首页复刻组件(HomeView / V1Icon / RankPreviewV1 / RecentScriptsV1)
const V1_ICON_INNER = {
trending: '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>',
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
pen: '<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/>',
flask: '<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
more: '<circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/><circle cx="5" cy="12" r="1.6"/>',
};
function V1Icon({ icon, size, color }) {
return react.createElement("span", { style: { display: "inline-flex", color: color || "#2f6fed", flexShrink: 0 } },
react.createElement("svg", {
viewBox: "0 0 24 24",
width: size || 20,
height: size || 20,
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
"aria-hidden": "true",
dangerouslySetInnerHTML: { __html: V1_ICON_INNER[icon] || "" },
}));
}
function V1StatBar({ stats }) {
const items = [
["关注账号", stats && stats.accounts],
["视频解析", stats && stats.analyses],
["AI写脚本", stats && stats.rewrites],
["AI脚本诊断", stats && stats.reviews],
["AI脚本评分", stats && stats.writingScore],
];
return react.createElement("div", { className: "v1stats" },
items.map((it) => react.createElement("div", { className: "v1stat", key: it[0] },
react.createElement("span", { className: "v1statV" }, it[1] == null ? "-" : String(it[1])),
react.createElement("span", { className: "v1statL" }, it[0]))));
}
function V1Card({ title, desc, color, icon, onClick }) {
return react.createElement("button", { type: "button", className: "v1card", onClick: onClick, title: desc },
react.createElement("span", { className: "v1cardI", style: { color: color } }, react.createElement(V1Icon, { icon: icon, size: 22 })),
react.createElement("span", { className: "v1cardT" }, title),
react.createElement("span", { className: "v1cardD" }, desc));
}
const V1_HELP_GROUPS = [
["创作写稿", "短视频脚本全流程创作:写脚本、口播/带货脚本、分镜脚本展开。", ["帮我想几个美妆赛道的爆款选题", "帮我写一条带货视频脚本,产品是XX面膜", "把刚才的脚本展开为分镜脚本"]],
["对标拆解", "账号定位与人设设计、对标视频拆解、账号内容套路分析。", ["帮我拆解这个抖音视频", "帮我拆解一下XX账号的内容套路", "分析我的账号定位有什么问题"]],
["账号分析", "达人账号信息获取、视频列表、人设卡生成。", ["帮我查一下俊希的账号设定", "帮我提炼这个账号的账号设定"]],
["诊断评分", "脚本质量诊断:四维评分/复盘报告/合规审查。", ["帮我诊断这个脚本", "给这个脚本打个分", "检查带货脚本有没有违规风险"]],
["榜单数据", "账号周榜/日榜、视频热榜、点赞榜浏览与更新。", ["看看最新的抖音榜单", "更新榜单数据"]]
];
function HomeView({ stats, entries, openEntry, openAiVideos }) {
return react.createElement("div", { className: "v1wrap" },
react.createElement(V1StatBar, { stats: stats }),
react.createElement("div", { className: "v1cards" },
react.createElement(V1Card, { key: "rk", title: "热点数据", desc: "账号日榜/周榜 · 视频热榜 · 点赞榜", color: "#e8590c", icon: "trending", onClick: () => openEntry("ranking") }),
react.createElement(V1Card, { key: "acc", title: "账号列表", desc: "关注的账号 + 视频列表", color: "#2f6fed", icon: "users", onClick: () => openEntry("entry:douyin-accounts") }),
react.createElement(V1Card, { key: "ai", title: "AI写脚本", desc: "AI脚本+分镜提示词", color: "#ec4899", icon: "pen", onClick: () => openAiVideos() }),
react.createElement(V1Card, { key: "rev", title: "脚本诊断", desc: "检查清单+复盘报告", color: "#0ea5e9", icon: "flask", onClick: () => openEntry("script-review") })),
react.createElement(RankPreviewV1, {}),
react.createElement(RecentScriptsV1, { onOpenAll: openAiVideos }));
}
//#endregion
//#region RankPreviewV1(首页紧凑榜单预览,复刻 V1.0:前10行/无分页排序/幽灵刷新)
const V1_BOARD_NAMES = { week: "账号周榜", day: "账号日榜", hot: "视频热榜", likes: "点赞榜" };
const V1_PREVIEW_COLS = {
week: { grid: "", head: "排名 赛道 账号 综合得分 新增粉丝 新增点赞 新增评论 新增分享 功能", fields: ["no", "cat", "name", "score", "num", "num", "num", "num", "act"] },
day: { grid: "", head: "排名 赛道 账号 综合得分 新增粉丝 新增点赞 新增评论 新增分享 功能", fields: ["no", "cat", "name", "score", "num", "num", "num", "num", "act"] },
hot: { grid: " v1rkvid", head: "排名 赛道 作品标题 作者 点赞 评论 分享 收藏", fields: ["no", "cat", "name", "author", "num", "num", "num", "num"] },
likes: { grid: " v1rkvid", head: "排名 赛道 作品标题 作者 新增点赞 新增收藏 新增评论 新增分享", fields: ["no", "cat", "name", "author", "num", "num", "num", "num"] },
};
function RankPreviewV1() {
const [board, setBoard] = react.useState("week");
const [dates, setDates] = react.useState([]);
const [categories, setCategories] = react.useState([]);
const [date, setDate] = react.useState("");
const [category, setCategory] = react.useState("");
const [rows, setRows] = react.useState(null);
const [loading, setLoading] = react.useState(false);
const [msg, setMsg] = react.useState(null);
const [updRunning, setUpdRunning] = react.useState(false);
const metaRef = react.useRef({});
const timersRef = react.useRef([]);
react.useEffect(() => () => { timersRef.current.forEach((x) => clearInterval(x)); timersRef.current = []; }, []);
const loadMeta = (b, cb) => {
fetch("/mcn/api/ranking?board=" + b + "&page=1&pageSize=1")
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
const m = { dates: d.dates || [], categories: d.categories || [], date: d.date || (d.dates && d.dates[0]) || "", category: d.category || "" };
metaRef.current[b] = m;
if (cb) cb(m);
})
.catch((e) => { metaRef.current[b] = { dates: [], categories: [], date: "", category: "" }; if (cb) cb(metaRef.current[b]); setMsg(e.message); });
};
const loadRows = (b, dt, ct, silent) => {
if (!silent) setLoading(true);
fetch("/mcn/api/ranking?board=" + b + "&date=" + encodeURIComponent(dt || "") + "&category=" + encodeURIComponent(ct || "") + "&page=1&pageSize=10")
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "加载失败");
setRows(d.items || []);
})
.catch((e) => { setRows([]); if (!silent) setMsg(e.message); })
.finally(() => { if (!silent) setLoading(false); });
};
const activate = (b) => {
const m = metaRef.current[b];
if (m) {
setBoard(b); setDates(m.dates); setCategories(m.categories); setDate(m.date); setCategory(m.category);
setRows(null); loadRows(b, m.date, m.category);
} else {
setBoard(b); setRows(null); setLoading(true);
loadMeta(b, (mm) => { setDates(mm.dates); setCategories(mm.categories); setDate(mm.date); setCategory(mm.category); loadRows(b, mm.date, mm.category); });
}
};
react.useEffect(() => { activate("week"); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, []);
const shiftDate = (dir) => {
const di = dates.indexOf(date);
const ni = di + dir;
if (ni < 0 || ni >= dates.length) return;
setDate(dates[ni]);
loadRows(board, dates[ni], category);
};
const onCategory = (v) => { setCategory(v); loadRows(board, date, v); };
const importAccount = (r) => {
const name = r.accountName || "";
v1ConfirmBox({
title: "关注账号",
desc: "关注该账号并加入账号列表(触发抖音网页抓取账号信息与视频列表,AI 处理完成后自动入库)?",
target: name,
url: r.profileUrl || "",
okText: "确认关注",
onOk: () => {
setMsg("已提交关注「" + name + "」,AI 正在抓取入库…");
fetch("/mcn/api/external/stage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ account: { name: r.accountName || "", url: r.profileUrl || "", category: r.category || "" } }),
}).then((r2) => r2.json())
.then((dd) => { setMsg(dd.ok ? (dd.message || "已提交") : (dd.error || "提交失败")); })
.catch((ee) => setMsg("提交失败: " + ee.message));
},
});
};
const updateRank = () => {
const doUpdate = (force) => {
return fetch("/mcn/api/ranking/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ force: !!force }) })
.then((r2) => r2.json())
.then((dd) => {
if (!dd.ok) { setMsg(dd.error || "提交失败"); return null; }
setMsg("已提交榜单更新(四榜最新一期),AI 正在抓取落盘,完成后自动刷新…");
setUpdRunning(true);
const started = Date.now();
const timer = setInterval(() => {
fetch("/mcn/api/ranking/update").then((r3) => r3.json()).then((st) => {
if (!st.running || Date.now() - started > 8 * 60 * 1000) {
clearInterval(timer);
setUpdRunning(false);
if (!st.running) { setMsg("榜单更新完成,已刷新数据"); activate(board); } else { setMsg("榜单更新仍在执行(可在 MCN任务会话 查看),稍后可手动刷新"); }
}
}).catch(() => {});
}, 5000);
timersRef.current.push(timer);
return null;
});
};
fetch("/mcn/api/ranking/update")
.then((r) => r.json())
.then((d) => {
if (d.running) { setMsg("榜单更新任务正在执行中,请等待完成后再试"); return null; }
const mins = Math.ceil((d.remainingMs || 0) / 60000);
const force = mins > 0;
v1ConfirmBox({
title: force ? "仍要更新?(将强制触发)" : "更新榜单数据",
desc: force ? "榜单数据 " + mins + " 分钟前刚更新过,重复抓取会浪费红狐 API 配额。确需强制刷新?" : "账号日榜(全品类 1 次)+ 账号周榜(保留 5 赛道)+ 视频热榜/点赞榜(红狐 API 按次计费)。确认更新?",
warn: force ? "强制更新将忽略冷却/防重直接调用红狐 API(按次计费),请确认成本。" : "",
danger: force,
okText: force ? "仍要更新" : "确认更新",
onOk: () => { doUpdate(force); },
});
return null;
})
.catch((e) => setMsg("操作失败: " + e.message));
};
const headParts = V1_PREVIEW_COLS[board].head.split(" ");
const clsFor = (f) => f === "no" ? "v1no" : f === "cat" ? "v1cat" : f === "name" ? "v1name" : f === "author" ? "v1author" : f === "score" ? "v1score" : f === "num" ? "v1num" : f === "act" ? "v1act" : "v1num";
const headEls = headParts.map((h, i) => react.createElement("span", { className: clsFor(i === 3 && (board === "week" || board === "day") ? "score" : (board === "week" || board === "day" ? (i === 0 ? "no" : i === 1 ? "cat" : i === 2 ? "name" : i === 8 ? "act" : "num") : (i === 0 ? "no" : i === 1 ? "cat" : i === 2 ? "name" : i === 3 ? "author" : "num"))), key: "h" + i }, h));
const rowCells = (r, ri) => {
const cells = [];
const accountLike = board === "week" || board === "day";
for (let i = 0; i < headParts.length; i++) {
let content = null;
if (i === 0) content = react.createElement("span", { className: "v1no", key: "c0" }, r.rank == null ? "-" : String(r.rank));
else if (i === 1) content = react.createElement("span", { className: "v1cat", key: "c1" }, r.category || "-");
else if (i === 2 && accountLike) {
content = r.inDb && r.dbId
? react.createElement("span", { className: "v1tlink", style: { color: "#4a90e2", cursor: "pointer" }, title: "查看账号详情", key: "c2", onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-account-detail", { accountId: r.dbId }); } catch (e) {} } }, String(r.accountName || ""))
: (r.profileUrl
? react.createElement("a", { className: "v1tlink", href: r.profileUrl, target: "_blank", rel: "noopener noreferrer", key: "c2" }, String(r.accountName || ""))
: react.createElement("span", { className: "v1name", key: "c2" }, String(r.accountName || "-")));
} else if (i === 2) {
const title = String(r.title || r.aweme_desc || "-");
const url = r.workUrl || r.share_url;
content = url
? react.createElement("a", { className: "v1tlink", href: url, target: "_blank", rel: "noopener noreferrer", title: "打开作品页", key: "c2" }, title)
: react.createElement("span", { className: "v1name", title: title, key: "c2" }, title);
} else if (i === 3 && accountLike) content = react.createElement("span", { className: "v1score", key: "c3" }, r.comprehensiveScore == null ? "-" : String(r.comprehensiveScore));
else if (i === 3) content = react.createElement("span", { className: "v1author", title: String(r.accountName || r.user_nickname || ""), key: "c3" }, String(r.accountName || r.user_nickname || "-"));
else if (i === 8) {
content = r.inDb && r.dbId
? react.createElement("button", { type: "button", className: "v1btn v1btnview", key: "c8", onClick: () => { try { window.__mcnNav && window.__mcnNav.open("douyin-account-detail", { accountId: r.dbId }); } catch (e) {} } }, "查看")
: react.createElement("button", { type: "button", className: "v1btn v1btnimp", key: "c8", title: "关注该账号并加入账号列表", onClick: () => importAccount(r) }, "关注");
} else {
const keyMap = accountLike
? { 4: r.newFans, 5: r.newLikes, 6: r.newComments, 7: r.newShares }
: (board === "hot" ? { 4: r.likeCount, 5: r.commentCount, 6: r.shareCount, 7: r.collectCount } : { 4: r.add_digg_count, 5: r.add_collect_count, 6: r.add_comment_count, 7: r.add_share_count });
content = react.createElement("span", { className: "v1num", key: "c" + i }, keyMap[i] == null || keyMap[i] === "" ? "-" : String(keyMap[i]));
}
cells.push(content);
}
return cells;
};
const di = dates.indexOf(date);
const colDef = V1_PREVIEW_COLS[board];
const isAcct = board === "week" || board === "day";
const emptyRows = isAcct ? null : null;
let body = null;
if (loading) body = react.createElement("div", { style: { padding: "14px", color: "#6b7280", fontSize: 14 } }, "加载中…");
else if (dates.length === 0) body = react.createElement("div", { className: "v1rkempty" }, "「" + (V1_BOARD_NAMES[board] || "") + "」暂无本地数据文件,先运行 mcn-data-insight 抓取技能落盘");
else if (rows === null || rows.length === 0) body = react.createElement("div", { style: { padding: "14px", color: "#6b7280", fontSize: 14 } }, "暂无数据");
else body = react.createElement("div", { className: "v1rklist" },
react.createElement("div", { className: "v1rkrow" + colDef.grid + " v1rkhead" }, headEls),
rows.map((r, ri) => react.createElement("div", { className: "v1rkrow" + colDef.grid, key: "r" + ri }, rowCells(r, ri))));
return react.createElement("div", { className: "v1rank" },
react.createElement("div", { className: "v1rkhead" },
react.createElement("div", { className: "v1rktabs" },
["week", "day", "hot", "likes"].map((b) => react.createElement("button", {
type: "button",
key: b,
className: "v1rktab" + (board === b ? " on" : ""),
onClick: () => activate(b),
}, V1_BOARD_NAMES[b]))),
react.createElement("button", {
type: "button",
className: "v1rkupdate",
title: "更新榜单数据:后台任务重新抓取四榜(账号日榜 全品类 1 次 / 账号周榜 5 赛道 / 视频热榜 / 点赞榜)最新一期并落盘;30 分钟内已更新会拦截防重复",
onClick: updateRank,
}, updRunning ? "更新中…" : react.createElement(V1Icon, { icon: "globe", size: 16 })),
react.createElement("div", { className: "v1rktools" },
categories.length > 0 && react.createElement("select", { className: "v1rksel", value: category, title: "赛道筛选", onChange: (e) => onCategory(e.target.value) },
react.createElement("option", { value: "", key: "" }, "全部赛道"),
categories.map((c) => react.createElement("option", { value: c, key: c }, c))),
react.createElement("button", { type: "button", className: "v1rkbtn", disabled: di <= 0, onClick: () => shiftDate(-1), title: "上一期" }, "◀"),
react.createElement("span", { className: "v1rkdate" }, date ? String(date).slice(5).replace("-", "-") : ""),
react.createElement("button", { type: "button", className: "v1rkbtn", disabled: di < 0 || di >= dates.length - 1, onClick: () => shiftDate(1), title: "下一期" }, "▶"))),
msg && react.createElement("div", { style: { fontSize: 12, color: "#6b7280", margin: "4px 2px 8px" } }, msg),
body);
}
//#endregion
//#region RecentScriptsV1(首页「AI脚本」卡片,复刻 V1.0:标题/5行预览/账号|日期/评分徽章/查看全部)
function v1CleanTitle(text) {
const first = String(text || "").replace(/\r\n/g, "\n").split("\n").map((l) => l.trim()).find((l) => l && !/^#{1,4}\s/.test(l));
if (!first) return null;
let t = first.replace(/^#+\s*/, "").trim();
const m = t.match(/《([^》]+)》/);
if (m && m[1]) t = m[1].trim();
return t ? t.slice(0, 40) : null;
}
function v1PreviewLines(text, n) {
return String(text || "").replace(/\r\n/g, "\n").split("\n").map((l) => l.trim()).filter((l) => l && !/^#{1,4}\s/.test(l)).slice(0, n || 5);
}
function v1FmtDate(t) {
return String(t || "").slice(0, 10);
}
//#region V1 统一脚本内容查看器(复刻 V1.0 openScriptModal:源脚本/AI脚本/右侧诊断|分镜 + 版本条 + 删除)与 V1 确认弹窗
function vvI(pd, size, color) {
return react.createElement("svg", { viewBox: "0 0 24 24", width: size || 15, height: size || 15, fill: "none", stroke: color || "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", style: { flex: "none" } }, react.createElement("path", { d: pd }));
}
function vvFmtT(t) {
const s = String(t || "").replace("T", " ").slice(0, 16);
return s;
}
function vvTitleQ(t) {
return String(t || "").replace(/^《/, "").replace(/》$/, "");
}
function vvMdSafe(t) {
try { if (typeof mdToReact === "function") { const el = mdToReact(t); if (el != null) return el; } } catch (e) { /* md 解析失败降级纯文本 */ }
return react.createElement("pre", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word", margin: 0, fontFamily: "inherit", fontSize: 14, lineHeight: 1.7, color: "#24292f" } }, String(t == null ? "" : t));
}
function vvScoreColor(v) {
return v >= 75 ? "#3a9e6d" : v >= 60 ? "#d99a2b" : "#d9534f";
}
const VV_DIAG_ORDER = ["设定承接", "框架节奏", "钩子设置", "叙事逻辑", "极致事件", "共鸣和共情", "格式规范性", "台词口语化", "内容合规", "教程完整链", "其他"];
function vvParseDims(rv) {
let dimsObj = {};
try { dimsObj = JSON.parse(rv.dims_json || "{}"); } catch (e) { dimsObj = {}; }
if (rv.type === "compare" && dimsObj && !Array.isArray(dimsObj) && typeof dimsObj === "object" && dimsObj.ai) dimsObj = dimsObj.ai;
const names = VV_DIAG_ORDER.filter((k) => dimsObj[k] != null && dimsObj[k] !== "");
return names.map((k) => { const v = Number(dimsObj[k]); return { name: k, val: Number.isFinite(v) ? v : null }; });
}
function vvParseIssues(rv) {
try { return JSON.parse(rv.issues_json || "[]"); } catch (e) { return []; }
}
function vvIconEl(pd, size, color) {
const ns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(ns, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", String(size || 15));
svg.setAttribute("height", String(size || 15));
svg.setAttribute("fill", "none");
svg.setAttribute("stroke", color || "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
svg.style.flex = "none";
const p = document.createElementNS(ns, "path");
p.setAttribute("d", pd);
svg.appendChild(p);
return svg;
}
// V1 风格确认弹窗(DOM 即用;返回关闭函数)// V1 风格确认弹窗(DOM 即用;返回关闭函数)
function v1ConfirmBox(opts) {
const mask = document.createElement("div");
mask.className = "vvMask";
const box = document.createElement("div");
box.className = "vvSmall";
const h3 = document.createElement("h3");
h3.appendChild(vvIconEl(opts.iconPd || "M13 2 3 14h9l-1 8 10-12h-9l1-8", 18, opts.danger ? "#d9534f" : "#2f6fed"));
h3.appendChild(document.createTextNode(opts.title || "确认"));
box.appendChild(h3);
if (opts.desc) { const p = document.createElement("p"); p.className = "vvDim"; p.textContent = opts.desc; box.appendChild(p); }
if (opts.target) { const d = document.createElement("div"); d.className = "vvTarget"; d.textContent = opts.target; box.appendChild(d); }
if (opts.url) { const u = document.createElement("div"); u.className = "vvUrl"; u.textContent = opts.url; box.appendChild(u); }
if (opts.warn) { const w = document.createElement("p"); w.className = "vvWarn"; w.textContent = opts.warn; box.appendChild(w); }
const act = document.createElement("div");
act.className = "vvActions";
const cancel = document.createElement("button");
cancel.type = "button"; cancel.className = "vb"; cancel.textContent = "取消";
const ok = document.createElement("button");
ok.type = "button"; ok.className = "vb " + (opts.danger ? "vbRed" : "vbPrimary"); ok.textContent = opts.okText || "确认执行";
act.appendChild(cancel); act.appendChild(ok); box.appendChild(act);
mask.appendChild(box);
document.body.appendChild(mask);
const close = () => { try { mask.remove(); } catch (e) {} };
mask.addEventListener("click", (e) => { if (e.target === mask) close(); });
cancel.addEventListener("click", close);
ok.addEventListener("click", () => { close(); try { if (opts.onOk) opts.onOk(); } catch (e) {} });
return close;
}
const vvJ = (u) => fetch(u).then((r) => r.json());
const vvPost = (u, b) => fetch(u, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(b || {}) }).then((r) => r.json());
function ScriptViewerV1({ rewriteId, videoId, fallbackTitle, accountName, onClose, onChanged }) {
const [state, setState] = react.useState({ mode: "loading", err: "" });
const [vi, setVi] = react.useState(-1);
const [showSrc, setShowSrc] = react.useState(false);
const [showSide, setShowSide] = react.useState(false);
const [stab, setStab] = react.useState("diag");
const [sbIdx, setSbIdx] = react.useState(-1);
const [side, setSide] = react.useState(null);
const [sideTick, setSideTick] = react.useState(0);
const [busy, setBusy] = react.useState(false);
const [hint, setHint] = react.useState("");
const [ask, setAsk] = react.useState(null);
const stateRef = react.useRef(state); stateRef.current = state;
const viRef = react.useRef(vi); viRef.current = vi;
const stabRef = react.useRef(stab); stabRef.current = stab;
const showSideRef = react.useRef(showSide); showSideRef.current = showSide;
// 初始装配:ref=视频容器全部版本;orig=单条(若实际挂视频下 → 切换 ref 模式)
react.useEffect(() => {
let alive = true;
const vid = Number(videoId) > 0 ? Number(videoId) : 0;
const rid = Number(rewriteId) > 0 ? Number(rewriteId) : 0;
const loadRef = async (v, pickId, acc, ttl) => {
const [vs, vd, st] = await Promise.all([
vvJ("/mcn/api/video/script?videoId=" + v).catch(() => ({ ok: false, rewrites: [], source: null })),
vvJ("/mcn/api/video/detail?videoId=" + v).catch(() => ({ ok: false, video: null })),
vvJ("/mcn/api/storyboard/list?videoId=" + v).catch(() => ({ ok: false, items: [] })),
]);
if (!alive) return;
let rewrites = (vs && vs.rewrites) || [];
if (!rewrites.length) { setState({ mode: "empty", err: "该脚本不存在或已删除" }); return; }
rewrites = rewrites.slice().reverse(); // 旧→新,末位=最新
let idx = pickId ? rewrites.findIndex((x) => String(x.id) === String(pickId)) : -1;
if (idx < 0) idx = rewrites.length - 1;
setState({ mode: "ready", vid: v, source: (vs && vs.source) || null, rewrites, sbAll: (st && st.items) || [], title: ttl || ((vd && vd.video && vd.video.video_title) || ""), acc: acc || ((vd && vd.video && vd.video.account_name) || "") });
setVi(idx);
};
const loadOrig = async () => {
const cs = await vvJ("/mcn/api/custom-script?id=" + rid).catch(() => null);
if (cs && cs.ok && cs.script) {
const s = cs.script;
if (Number(s.video_id) > 0) { await loadRef(Number(s.video_id), rid, s.account_name || "", s.title || ""); return; }
if (!alive) return;
const one = { id: s.id, script_text: s.script_text || "", created_time: s.created_time || "" };
setState({ mode: "ready", vid: 0, source: null, rewrites: [one], sbAll: [], title: s.title || ttl || "", acc: s.account_name || "" });
setVi(0);
return;
}
const all = await vvJ("/mcn/api/rewrite/list?limit=100").catch(() => ({ items: [] }));
const row = ((all.items) || []).find((x) => String(x.id) === String(rid));
if (row && Number(row.video_id) > 0) { await loadRef(Number(row.video_id), rid, "", row.title || ""); return; }
if (!alive) return;
if (row) { const one = { id: row.id, script_text: "", created_time: row.created_time || "" }; setState({ mode: "ready", vid: 0, source: null, rewrites: [one], sbAll: [], title: row.title || ttl || "", acc: "" }); setVi(0); return; }
setState({ mode: "empty", err: "该脚本不存在或已删除" });
};
const ttl = String(fallbackTitle || "");
(vid > 0 ? loadRef(vid, rid, accountName || "", ttl) : (rid > 0 ? loadOrig() : Promise.resolve(setState({ mode: "empty", err: "缺少脚本标识" }))));
return () => { alive = false; };
}, []);
const st = state;
const cur = st.mode === "ready" ? st.rewrites[Math.min(vi, st.rewrites.length - 1)] : null;
// 右侧面板数据加载(diag / sb)
const loadSide = (c) => {
if (!c) return;
setSide(null);
const token = (sideTickRef.current = sideTickRef.current + 1);
const doLoad = async () => {
if (stabRef.current === "diag") {
const rf = await vvJ("/mcn/api/review/full?rewriteId=" + c.id).catch(() => null);
const reviews = (rf && rf.reviews) || {};
const rv = (reviews.ai || reviews.compare) || null;
if (!rv) { if (stateRef.current.mode === "ready" && token === sideTickRef.current) setSide({ kind: "empty", diag: false }); return; }
const dims = vvParseDims(rv);
const issues = vvParseIssues(rv);
let total = Number(rv.total);
let srcTotal = NaN;
if (rv.type === "compare") { try { const t = JSON.parse(rv.total || "{}"); total = Number(t.ai); srcTotal = Number(t.source); } catch (e) {} }
if (token === sideTickRef.current) setSide({ kind: "diag", rv, dims, issues, total, srcTotal, summary: rv.summary || "" });
} else {
const stt = stateRef.current;
const vis = ((stt.sbAll) || []).filter((s) => s.rewrite_id == null || String(s.rewrite_id) === String(c.id));
if (!vis.length) { if (token === sideTickRef.current) setSide({ kind: "sbEmpty", vid: stt.vid }); return; }
if (token === sideTickRef.current) setSide({ kind: "sb", vis });
}
};
doLoad();
};
const curId = cur && cur.id;
react.useEffect(() => { if (cur && showSide) loadSide(cur); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [curId, stab, showSide]);
const sideTickRef = react.useRef(0);
const fireReview = () => {
if (!cur) return;
const hasSource = st.mode === "ready" && st.vid > 0 && !!st.source;
setAsk({ title: hasSource ? "复盘评分" : "AI脚本复盘", desc: hasSource ? "将对该 AI 脚本做复盘评分(AI脚本 / 原视频脚本 / 对比分析三种模式)。" : "将对该 AI 脚本做质量复盘评分(独立分析模式 · 10 维评分制,该脚本无原视频脚本)。", target: "《" + vvTitleQ(st.title || cur.title || "AI 脚本") + "》", okText: "确认执行", onOk: () => {
setBusy(true); setHint("");
vvPost("/mcn/api/review/start", { logId: cur.id, type: "ai" }).then((d) => {
if (!d.ok) throw new Error(d.error || "提交失败");
setHint("已提交复盘任务(" + (d.message || "") + "),完成后点「更新诊断」查看");
}).catch((e) => setHint("提交失败: " + e.message)).finally(() => setBusy(false));
} });
};
const fireGenSb = (regenerate) => {
if (!cur) return;
setAsk({ title: regenerate ? "重新生成分镜" : "生成分镜", desc: "将基于该 AI 脚本展开成 AI 分镜提示词(按分镜技能生成镜头级描述)。", target: "《" + vvTitleQ(st.title || cur.title || "") + "》", okText: "确认生成", onOk: () => {
setBusy(true); setHint("");
vvPost("/mcn/api/storyboard/create", { rewriteId: cur.id }).then((d) => {
if (!d.ok) throw new Error(d.error || "生成失败");
setHint("已提交分镜生成任务,生成完成后本列表自动出现新版本");
let n = 0;
const timer = setInterval(() => {
n++;
vvJ("/mcn/api/storyboard/list?videoId=" + st.vid).then((r2) => {
if (r2.ok) { const items = r2.items || []; if (items.length > (stateRef.current.sbAll || []).length || n >= 8) { clearInterval(timer); if (n < 8) setHint("分镜已生成 ✓"); setState((prev) => (prev.mode === "ready" ? { ...prev, sbAll: items } : prev)); setSide(null); loadSide(cur); } }
}).catch(() => { if (n >= 8) clearInterval(timer); });
}, 5000);
}).catch((e) => setHint("生成失败: " + e.message)).finally(() => setBusy(false));
} });
};
const openDeleteAsk = () => {
if (!cur) return;
setAsk({ title: "删除当前脚本", danger: true, desc: "将删除 AI 脚本" + (st.rewrites.length > 1 ? "第 " + (vi + 1) + "/" + st.rewrites.length + " 版" : "") + "(创建于 " + vvFmtT(cur.created_time) + "):", target: "《" + vvTitleQ(st.title || cur.title || "AI 脚本") + "》", warn: "该脚本的诊断复盘与分镜提示词将一并删除,不可恢复" + (st.rewrites.length <= 1 ? ";其视频若无其他 AI 脚本,视频级复盘也会清理" : "") + "。", okText: "确认删除", onOk: () => {
setBusy(true);
vvPost("/mcn/api/rewrite/delete", { id: cur.id }).then((d) => {
if (!d.ok) throw new Error(d.error || "删除失败");
const rest = st.rewrites.filter((x) => String(x.id) !== String(cur.id));
if (!rest.length) { try { if (onChanged) onChanged(cur.id); } catch (e) {} try { onClose(); } catch (e) {} return; }
setState((prev) => ({ ...prev, rewrites: rest }));
const nv = Math.min(vi, rest.length - 1);
setVi(nv); setSbIdx(-1); setSide(null);
try { if (onChanged) onChanged(cur.id); } catch (e) {}
setHint("已删除该脚本及关联数据");
}).catch((e) => setHint("删除失败: " + e.message)).finally(() => setBusy(false));
} });
};
if (st.mode === "loading") return react.createElement("div", { className: "vvMask", onClick: onClose }, react.createElement("div", { className: "vvSmall", style: { width: 340 }, onClick: (e) => e.stopPropagation() }, react.createElement("div", { className: "vvLoading" }, "加载中…")));
if (st.mode === "empty") return react.createElement("div", { className: "vvMask", onClick: onClose }, react.createElement("div", { className: "vvSmall", style: { width: 380 }, onClick: (e) => e.stopPropagation() }, react.createElement("div", { className: "vvDim" }, st.err || "该脚本不存在或已删除"), react.createElement("div", { className: "vvActions" }, react.createElement("button", { type: "button", className: "vb vbPrimary", onClick: onClose }, "关闭"))));
if (!cur) return null;
const hasSource = st.vid > 0 && !!st.source;
const sbVisible = ((st.sbAll || []).filter((s) => s.rewrite_id == null || String(s.rewrite_id) === String(cur.id)));
const sbCur = sbVisible.length ? sbVisible[Math.min(Math.max(sbIdx, 0), sbVisible.length - 1)] : null;
const xBtn = react.createElement("button", { type: "button", className: "vb", title: "关闭", onClick: onClose, style: { padding: "4px 8px", border: "none", background: "none", color: "#6b7280" } }, vvI("M18 6L6 18M6 6l12 12", 16));
const verCtrl = (label, len, idx, timeStr, onPrev, onNext, extra) => react.createElement(react.Fragment, null,
react.createElement("span", { className: "vvCount" }, label + " " + (idx + 1) + "/" + len, len > 1 && idx >= len - 1 ? react.createElement("span", { className: "vvNewBadge" }, "最新") : null, timeStr ? react.createElement(react.Fragment, null, " · ", react.createElement("span", { style: { color: "#6b7280" } }, vvFmtT(timeStr))) : null),
extra || null,
len > 1 && react.createElement("button", { type: "button", className: "vb vbSm", disabled: idx <= 0, onClick: onPrev, title: "查看较旧版本" }, "← 较旧"),
len > 1 && react.createElement("button", { type: "button", className: "vb vbSm", disabled: idx >= len - 1, onClick: onNext, title: "查看较新版本" }, "较新 →"));
const aiHead = react.createElement("div", { className: "vvSecHead" },
react.createElement("span", { style: { color: "#24292f" } }, "AI脚本"),
hasSource && react.createElement("button", { type: "button", className: "vb vbSm vbView vvSrcToggle" + (showSrc ? " vvSrcOn" : ""), title: showSrc ? "收起源脚本" : "展开源脚本", onClick: () => setShowSrc(!showSrc) }, vvI("M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z M14 2v4a2 2 0 0 0 2 2h4 M10 9H8 M16 13H8 M16 17H8", 14)),
react.createElement("button", { type: "button", className: "vvSideToggle" + (showSide ? "" : " off"), title: showSide ? "收起脚本诊断/分镜提示词,只看 AI 脚本" : "展开脚本诊断/分镜提示词", onClick: () => setShowSide(!showSide) }, vvI(showSide ? "M13 17l5-5-5-5M6 17l5-5-5-5" : "M11 17l-5-5 5-5M18 17l-5-5 5-5", 16)));
const srcSec = hasSource && react.createElement("div", { className: "vvSec" + (showSrc ? "" : " vvHidden") },
react.createElement("div", { className: "vvSecHead" }, "源脚本"),
react.createElement("div", { className: "vvSecBody vvMd" }, vvMdSafe(st.source || "(暂无源脚本数据)")),
react.createElement("div", { className: "vvSecFoot" }));
const sideHead = react.createElement("div", { className: "vvSecHead vvSideHead" },
react.createElement("div", { className: "vvTabs" },
react.createElement("button", { type: "button", className: "vvTab" + (stab === "diag" ? " on" : ""), title: "该版本 AI 脚本的复盘评分与问题清单", onClick: () => { setStab("diag"); setSide(null); } }, "脚本诊断"),
react.createElement("span", { className: "vvSep" }, "|"),
react.createElement("button", { type: "button", className: "vvTab" + (stab === "sb" ? " on" : ""), title: "基于 AI 脚本生成的镜头级分镜提示词", onClick: () => { setStab("sb"); setSide(null); } }, "分镜提示词")));
let sideBody = null, sideFoot = null;
if (!showSide) { /* 隐藏 */ } else if (!side) {
sideBody = react.createElement("div", { className: "vvLoading" }, "加载中…");
} else if (side.kind === "diag") {
const totalFin = Number.isFinite(side.total);
sideBody = react.createElement("div", null,
react.createElement("div", { className: "vvScoreBox" },
react.createElement("div", { className: "vvScoreBoxTitle" }, "AI 总分 " + (totalFin ? side.total : Number(side.rv.total)), side.rv.type === "compare" && Number.isFinite(side.srcTotal) ? "(对比源脚本 " + side.srcTotal + ")" : null, side.rv.verdict ? react.createElement("span", { style: { color: totalFin ? vvScoreColor(side.total) : "#6b7280" } }, " · " + side.rv.verdict) : null),
side.dims.map((d) => react.createElement("div", { className: "vvScoreRow", key: d.name },
react.createElement("span", { className: "vvScoreName" }, d.name),
react.createElement("div", { className: "vvScoreBar" }, react.createElement("div", { className: "vvScoreFill", style: { width: (d.val == null ? 0 : d.val) + "%", background: d.val == null ? "#eee" : vvScoreColor(d.val) } })),
react.createElement("span", { className: "vvScoreVal", style: { color: d.val == null ? "#6b7280" : vvScoreColor(d.val) } }, d.val == null ? "—" : d.val)))),
side.issues.length ? react.createElement("div", { className: "vvIssues" },
react.createElement("div", { className: "vvIssueTitle" }, "问题清单"),
side.issues.map((it, ix) => react.createElement("div", { className: "vvIssueRow", key: "i" + ix },
react.createElement("span", { style: { color: it.severity === "高" ? "#d9534f" : it.severity === "中" ? "#d99a2b" : "#3a9e6d", fontWeight: 600, flex: "none" } }, it.severity || "低"),
react.createElement("span", null, it.issue || ""),
it.suggestion ? react.createElement("span", { style: { color: "#6b7280" } }, "→ " + it.suggestion) : null))) : null,
side.summary ? react.createElement("div", { className: "vvSummary" }, "综合评语:" + side.summary) : null,
side.rv.content ? react.createElement("div", { className: "vvMd", style: { marginTop: 6 } }, vvMdSafe(side.rv.content)) : null);
sideFoot = react.createElement("div", { className: "vvSecFoot" }, react.createElement("button", { type: "button", className: "vb vbSm" + (side.rv ? "" : " vbView"), onClick: fireReview, disabled: busy }, vvI("M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5", 14), side.rv ? "更新诊断" : "开始诊断"));
} else if (side.kind === "empty") {
sideBody = react.createElement("div", { className: "vvEmpty" }, "该版本 AI 脚本尚未诊断");
sideFoot = react.createElement("div", { className: "vvSecFoot" }, react.createElement("button", { type: "button", className: "vb vbSm vbView", onClick: fireReview, disabled: busy }, vvI("M13 2 3 14h9l-1 8 10-12h-9l1-8", 13), "开始诊断"));
} else if (side.kind === "sb") {
const idx = Math.min(Math.max(sbIdx < 0 ? Math.max(0, side.vis.length - 1) : sbIdx, 0), side.vis.length - 1);
const sb = side.vis[idx];
sideBody = sb ? react.createElement("div", { className: "vvMd" }, vvMdSafe(sb.script_text || "")) : null;
sideFoot = react.createElement("div", { className: "vvSecFoot" },
react.createElement("span", { className: "vvCount" }, "分镜 " + (idx + 1) + "/" + side.vis.length, side.vis.length > 1 && idx >= side.vis.length - 1 ? react.createElement("span", { className: "vvNewBadge" }, "最新") : null, sb && sb.created_time ? react.createElement(react.Fragment, null, " · ", vvFmtT(sb.created_time)) : null),
react.createElement("button", { type: "button", className: "vb vbSm", disabled: idx <= 0, onClick: () => setSbIdx(idx - 1), title: "查看较旧版本" }, "← 较旧"),
react.createElement("button", { type: "button", className: "vb vbSm", disabled: idx >= side.vis.length - 1, onClick: () => setSbIdx(idx + 1), title: "查看较新版本" }, "较新 →"),
st.vid > 0 && react.createElement("button", { type: "button", className: "vb vbSm", onClick: () => fireGenSb(true), disabled: busy }, vvI("M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5", 13), "重新生成"));
} else if (side.kind === "sbEmpty") {
sideBody = react.createElement("div", { className: "vvEmpty" }, st.vid > 0 ? "(暂无分镜提示词)" : "该脚本为原创选题(无关联视频),暂不支持分镜提示词");
if (st.vid > 0) sideFoot = react.createElement("div", { className: "vvSecFoot" }, react.createElement("button", { type: "button", className: "vb vbSm vbView", onClick: () => fireGenSb(false), disabled: busy }, vvI("M13 2 3 14h9l-1 8 10-12h-9l1-8", 14), "生成分镜"));
}
const aiBody = react.createElement("div", { className: "vvSecBody vvMd" }, vvMdSafe(cur.script_text || "(暂无内容)"));
const aiFoot = react.createElement("div", { className: "vvSecFoot" },
react.createElement("button", { type: "button", className: "vb vbSm vbDel", title: "删除当前脚本(同时删除其诊断复盘与分镜提示词数据,不可恢复)", onClick: openDeleteAsk, disabled: busy }, vvI("M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14", 13)),
hint ? react.createElement("span", { className: "vvHint", style: { marginLeft: 6 } }, hint) : null,
st.rewrites.length > 1 && verCtrl("版本", st.rewrites.length, vi, cur.created_time, () => { setVi(Math.max(0, vi - 1)); setSbIdx(-1); setSide(null); }, () => { setVi(Math.min(st.rewrites.length - 1, vi + 1)); setSbIdx(-1); setSide(null); }, null));
return react.createElement("div", { className: "vvMask", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } },
react.createElement("div", { className: "vvModal" },
react.createElement("div", { className: "vvTitleRow" },
react.createElement("span", { className: "vvT" },
react.createElement("span", { className: "vvIcon" }, vvI("M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z", 15)),
vvTitleQ(st.title || cur.title || fallbackTitle || "AI 脚本"),
st.acc ? react.createElement("span", { className: "vvMeta" }, " · " + st.acc) : null),
xBtn),
hint ? react.createElement("div", { style: { flex: "none", color: "#b7791f", fontSize: 12, margin: "-6px 0 8px", lineHeight: 1.6 } }, hint) : null,
react.createElement("div", { className: "vvStack" },
srcSec,
react.createElement("div", { className: "vvSec", style: { flex: showSide ? "1.2" : "1" } },
aiHead, aiBody, aiFoot),
react.createElement("div", { className: "vvSec" + (showSide ? "" : " vvHidden") },
sideHead, react.createElement("div", { className: "vvSecBody" }, sideBody), sideFoot))),
ask && react.createElement("div", { className: "vvMask", style: { zIndex: 1100, background: "rgba(0,0,0,.25)" }, onClick: () => setAsk(null) },
react.createElement("div", { className: "vvSmall", onClick: (e) => e.stopPropagation() },
react.createElement("h3", null, vvI("M13 2 3 14h9l-1 8 10-12h-9l1-8", 18, ask.danger ? "#d9534f" : "#2f6fed"), ask.title),
ask.desc ? react.createElement("p", { className: "vvDim" }, ask.desc) : null,
ask.target ? react.createElement("div", { className: "vvTarget" }, ask.target) : null,
ask.warn ? react.createElement("p", { className: "vvWarn" }, "⚠️ " + ask.warn) : null,
react.createElement("div", { className: "vvActions" },
react.createElement("button", { type: "button", className: "vb", onClick: () => setAsk(null) }, "取消"),
react.createElement("button", { type: "button", className: "vb " + (ask.danger ? "vbRed" : "vbPrimary"), onClick: () => { const fn = ask.onOk; setAsk(null); try { fn(); } catch (e) {} } }, ask.okText || "确认执行")))));
}
//#endregion
function RecentScriptsV1({ onOpenAll }) {
const [rows, setRows] = react.useState(null);
const [open, setOpen] = react.useState(null);
react.useEffect(() => {
let alive = true;
const fetchText = (r) => {
const base = { id: r.id, video_id: r.video_id, title: r.title || "", account_name: r.account_name || "", created_time: r.created_time || "", score: r.score };
if (r.video_id > 0) {
const pScript = fetch("/mcn/api/video/script?videoId=" + r.video_id).then((x) => x.json()).then((d) => {
const rw = ((d.rewrites || []).find((w) => w.id === r.id)) || {};
return { script_text: rw.script_text || "" };
}).catch(() => ({ script_text: "" }));
const pDetail = fetch("/mcn/api/video/detail?videoId=" + r.video_id).then((x) => x.json()).then((d) => ({ acc: (d.video && d.video.account_name) || "" })).catch(() => ({ acc: "" }));
return Promise.all([pScript, pDetail]).then(([s, dv]) => ({ ...base, script_text: s.script_text, account_name: base.account_name || dv.acc || "" }));
}
return fetch("/mcn/api/custom-script?id=" + r.id).then((x) => x.json()).then((d) => ({ ...base, script_text: (d.script && d.script.script_text) || d.script_text || "", account_name: base.account_name || (d.script && d.script.account_name) || "" })).catch(() => ({ ...base, script_text: "" })); };
const assemble = (list) => {
const picked = (list || []).filter((r) => r && (r.status === "done" || !r.status) && r.script_len > 0).sort((a, b) => String(b.created_time || "").localeCompare(String(a.created_time || "")) || (b.id - a.id)).slice(0, 8);
if (!picked.length) { if (alive) setRows([]); return; }
const jobs = picked.map((r) => {
let ra = r.review_ai;
if (typeof ra === "string") { try { ra = JSON.parse(ra); } catch (e) { ra = null; } }
const scoreRaw = ra && ra.total;
const sc = scoreRaw != null && scoreRaw !== "" && Number.isFinite(Number(scoreRaw)) ? Number(scoreRaw) : null;
return fetchText({ ...r, review_ai: ra, score: sc });
});
Promise.all(jobs).then((items) => { if (alive) setRows(items.filter((it) => it.script_text)); });
};
const assembleFallback = () => {
fetch("/mcn/api/rewrite/list?limit=40").then((r) => r.json()).then((d) => { if (alive) assemble(d.items || []); }).catch(() => { if (alive) setRows([]); });
};
fetch("/mcn/api/recent-scripts?limit=8")
.then((r) => r.json())
.then((d) => { if (alive) { if (d.ok && d.items && d.items.length) { setRows(d.items); } else { assembleFallback(); } } })
.catch(() => { if (alive) assembleFallback(); });
return () => { alive = false; };
}, []);
const scoreColor = (v) => (v >= 75 ? "#3a9e6d" : v >= 60 ? "#d99a2b" : "#d9534f");
const list = rows === null ? react.createElement("span", { style: { color: "#6b7280" } }, "加载中…")
: rows.length === 0 ? react.createElement("span", { style: { color: "#6b7280" } }, "暂无 AI 写剧本")
: react.createElement("div", { className: "v1rlist" },
rows.map((s) => {
const title = v1CleanTitle(s.title || s.video_title) || "未命名剧本";
const meta = [s.account_name, s.created_time ? v1FmtDate(s.created_time) : ""].filter(Boolean).join(" | ");
const preview = v1PreviewLines(s.script_text, 5);
const sc = Number(s.score);
const hasScore = s.score !== null && s.score !== undefined && s.score !== "" && Number.isFinite(sc);
return react.createElement("button", { type: "button", className: "v1rcard", key: s.id, title: "点击查看完整剧本", onClick: () => setOpen(s) },
react.createElement("span", { className: "v1rcT" }, title),
react.createElement("span", { className: "v1rcP" }, preview.map((l, i) => react.createElement("span", { className: "v1rcL", key: "l" + i }, l))),
react.createElement("span", { className: "v1rcF" },
react.createElement("span", { className: "v1rcM" }, meta),
hasScore && react.createElement("span", { className: "v1rcS", style: { background: scoreColor(sc) + "1a", color: scoreColor(sc) } }, "评分 " + (sc % 1 === 0 ? sc : sc.toFixed(1)))));
}));
return react.createElement("div", { className: "v1recents" },
react.createElement("div", { className: "v1rhead" },
react.createElement("h3", null, "AI脚本"),
react.createElement("button", { type: "button", className: "v1all", onClick: () => { if (onOpenAll) onOpenAll(); } }, "查看全部")),
list,
open && react.createElement(ScriptViewerV1, { key: open.id, rewriteId: open.id, videoId: (open.video_id > 0 ? open.video_id : 0), fallbackTitle: (open.title || open.video_title || ""), accountName: (open.account_name || ""), onClose: () => setOpen(null) }));
}
//#endregion
function McnPage({ onClose, side, onToggleSide }) {
const [toast, setToast] = (0, react.useState)(null);
const [helpOpen, setHelpOpen] = (0, react.useState)(false);
const [moreOpen, setMoreOpen] = (0, react.useState)(false);
const [menuAt, setMenuAt] = (0, react.useState)(null);
const [creativeOpen, setCreativeOpen] = (0, react.useState)(false);
const [creativePrompt, setCreativePrompt] = (0, react.useState)("");
const [creativeLoading, setCreativeLoading] = (0, react.useState)(false);
// 当前创作目标达人(null = 通用创作)
const [creativeAccount, setCreativeAccount] = (0, react.useState)(null);
// 手动色调:dark / light / null(跟随系统),localStorage 持久化(下次打开保持)
const [theme, setTheme] = (0, react.useState)(() => {
try { const v = localStorage.getItem("mcnNav.theme"); return v === "dark" || v === "light" ? v : null; } catch (e) { return null; }
});
const isDarkNow = theme === "dark" || (theme !== "light" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches);
(0, react.useEffect)(() => {
document.documentElement.style.colorScheme = isDarkNow ? "dark" : "light";
document.body.toggleAttribute("data-ds-dark-theme", isDarkNow);
}, [isDarkNow]);
const toggleTheme = () => {
const next = isDarkNow ? "light" : "dark";
setTheme(next);
try { localStorage.setItem("mcnNav.theme", next); } catch (e) {}
};
// 工作台视图:home = 入口网格;entry:<id> = 外部插件注册的功能页;ai-videos = AI写作提升视频列表
const [view, setView] = (0, react.useState)("home");
// 程序化导航来源栈(__mcnNav.back 逐层返回用;ref 避免 useEffect 闭包捕获旧值)
const viewRef = (0, react.useRef)(view);
viewRef.current = view;
// 导航栈:每项 { view, params } —— open/卡片点击入栈,back 弹栈恢复,
// 支持多层跳转(如 首页→热门账号→账号详情→视频详情 后逐层返回,参数一并恢复)
const navStackRef = (0, react.useRef)([]);
// 首页卡片/入口进入:入栈当前页(供 back 逐层返回),再切到目标视图(view 为完整视图名,如 "entry:douyin-accounts" 或 "ai-videos")
const openEntry = (view) => {
navStackRef.current.push({ view: viewRef.current, params: entryParamsRef.current });
setEntryParams(null);
setView(view);
};
// 数据预览统计(后端 60s 缓存,前端仅挂载时拉取一次)
const [stats, setStats] = (0, react.useState)(null);
(0, react.useEffect)(() => {
let alive = true;
fetch("/mcn/api/stats")
.then((r) => r.json())
.then((d) => { if (alive && d.ok) setStats(d); })
.catch(() => {});
return () => { alive = false; };
}, []);
// AI写作提升:全部视频列表(已解析优先 → 点赞降序)
const [aiVideos, setAiVideos] = (0, react.useState)([]);
const [aiTotal, setAiTotal] = (0, react.useState)(0);
const [aiPage, setAiPage] = (0, react.useState)(1);
const [aiSearch, setAiSearch] = (0, react.useState)("");
const [aiParsed, setAiParsed] = (0, react.useState)("");
const [aiLoading, setAiLoading] = (0, react.useState)(false);
// 账号预筛与时间窗口:来源=账号列表/详情 __mcnNav.openView('ai-videos',{accountId,accountName}) 或本页工具栏;作用于 rewrite 与 custom 两个 tab(null/"")=全部
const [aiScope, setAiScope] = (0, react.useState)(null);
const [aiDays, setAiDays] = (0, react.useState)("");
const appliedScopeRef = (0, react.useRef)("");
const loadAiVideos = (p = 1, s = aiSearch, pr = aiParsed, silent = false, acc, days) => {
if (!silent) setAiLoading(true);
// acc/days 显式传入(含 null/"")用于「清除筛选」等场景;省略时取当前 state
const scopeId = acc === undefined ? (aiScope ? String(aiScope.id) : "") : (acc ? String(acc.id) : "");
const daysQ = days === undefined ? (aiDays || "") : (days || "");
fetch("/mcn/api/videos?page=" + p + "&pageSize=20&search=" + encodeURIComponent(s) + "&parsed=" + encodeURIComponent(pr) + (scopeId ? "&account=" + encodeURIComponent(scopeId) : "") + (daysQ ? "&days=" + encodeURIComponent(daysQ) : ""))
.then((r) => r.json())
.then((d) => { if (d.ok) { setAiVideos(d.items || []); setAiTotal(d.total || 0); setAiPage(d.page || 1); } })
.catch(() => {})
.finally(() => { if (!silent) setAiLoading(false); });
};
// 清除账号预筛与时间窗口(AiVideosPanel 顶部「清除筛选」)
const clearAiScope = () => {
appliedScopeRef.current = "";
setAiScope(null);
setAiDays("");
loadAiVideos(1, aiSearch, aiParsed, false, null, "");
};
// 打开「AI写脚本」视图(首页卡片 / 最近AI脚本「查看全部」)
const openAiVideosView = () => {
appliedScopeRef.current = "";
setAiScope(null);
setAiDays("");
loadAiVideos(1);
openEntry("ai-videos");
};
// 改写状态轮询:列表视图下每 10s 静默刷新(网页/聊天窗口触发的改写任务都能实时反映「执行中」与完成状态)
(0, react.useEffect)(() => {
if (view !== "ai-videos") return;
const t = setInterval(() => loadAiVideos(aiPage, aiSearch, aiParsed, true), 10000);
return () => clearInterval(t);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [view, aiPage, aiSearch, aiParsed, aiScope, aiDays]);
// 视频解析:提交 MCP 解析任务(后端并发上限 8)
const analyzeVideo = (row) => {
fetch("/mcn/api/video/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ videoId: row.id }),
})
.then((r) => r.json())
.then((d) => {
if (d.ok) { setToast(d.message || "已提交视频解析"); loadAiVideos(aiPage); }
else { setToast(d.error || "解析提交失败"); }
})
.catch(() => setToast("解析提交失败"));
};
// 视频改写:调用 short-video-script 技能基于账号设定+选题改写 S9 故事脚本(后端校验并发 8/信息齐备)
const rewriteVideo = (row) => {
fetch("/mcn/api/video/rewrite", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ videoId: row.id }),
})
.then((r) => r.json())
.then((d) => {
if (d.ok) {
setToast(d.message || "已提交改写");
// 立即本地标记该行为执行中(避免刷新竞态),随后轮询接管
setAiVideos((prev) => prev.map((v) => (v.id === row.id ? { ...v, rewrite_running: true } : v)));
loadAiVideos(aiPage);
}
else { setToast(d.error || "改写提交失败"); }
})
.catch(() => setToast("改写提交失败"));
};
// 外部插件注册的功能入口(window.__mcnEntries),即插即用
const entries = window.__mcnEntries || [];
// 程序化导航参数(如账号详情页的 accountId),由 __mcnNav.open(entryId, params) 携带
const [entryParams, setEntryParams] = (0, react.useState)(null);
// entryParams 同步 ref(nav.open 在 useEffect 闭包内,读 state 会拿到旧值,需经 ref)
const entryParamsRef = (0, react.useRef)(null);
entryParamsRef.current = entryParams;
// 账号预筛应用:外部 openView('ai-videos',{accountId,accountName}) 进入时按账号拉取 rewrite 列表并重置时间窗口(每个账号上下文仅一次;清除筛选后重置标记可再次应用)
(0, react.useEffect)(() => {
if (view !== "ai-videos") return;
const p = entryParams;
const accId = p && p.accountId != null ? String(p.accountId) : "";
if (accId && appliedScopeRef.current !== accId) {
appliedScopeRef.current = accId;
const scope = { id: accId, name: p.accountName || "" };
setAiScope(scope);
setAiDays("");
loadAiVideos(1, aiSearch, aiParsed, false, scope, "");
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [view, entryParams]);
const activeEntry = view.startsWith("entry:") ? entries.find((e) => e.id === view.slice(6)) : undefined;
const scheduleEntry = (entries || []).find((e) => e.id === "mcn-schedule");
const socialEntry = (entries || []).find((e) => e.id === "social-workbench");
// 程序化导航 API:供其他插件(如账号列表行点击)跳转到指定功能页并携带参数
(0, react.useEffect)(() => {
const nav = {
open: (entryId, params) => {
if (!window.__mcnEntries?.some((e) => e.id === entryId)) return false;
// 入栈当前页 + 其参数(供 back 原样返回,支持多层跳转)
navStackRef.current.push({ view: viewRef.current, params: entryParamsRef.current });
setEntryParams(params ?? null);
setView("entry:" + entryId);
return true;
},
// 逐层返回来源页(从详情/子页面返回时回到进入前的页面与参数,如账号详情→视频详情→返回)
back: () => {
const from = navStackRef.current.pop();
setEntryParams(from ? from.params : null);
setView(from ? from.view : "home");
},
goHome: () => { navStackRef.current = []; setEntryParams(null); setView("home"); },
// 切到任意内部视图(如 AI会话/AI写脚本),可携带 params(如 {accountId,accountName} 账号预筛)
openView: (v, params) => {
navStackRef.current.push({ view: viewRef.current, params: entryParamsRef.current });
setEntryParams(params ?? null);
setView(v);
},
};
window.__mcnNav = nav;
return () => { if (window.__mcnNav === nav) delete window.__mcnNav; };
}, []);
// 打开创作弹窗:可选传入达人,自动带入其信息生成创作需求
const openCreative = (account) => {
setCreativeAccount(account);
// 输入框只留"需求补充"(可空);达人信息通过 state 携带,提交时拼接
setCreativePrompt("");
setCreativeOpen(true);
};
const submitCreative = () => {
const extra = creativePrompt.trim();
// 组装最终 prompt:有达人则先带达人信息,再拼用户补充需求
let prompt = "";
if (creativeAccount) {
const info = [
`账号名:${creativeAccount.account_name}`,
creativeAccount.track ? `赛道:${creativeAccount.track}` : "",
creativeAccount.content ? `账号定位:${creativeAccount.content}` : "",
].filter(Boolean).join(";");
prompt = `请为达人「${creativeAccount.account_name}」创作一条短视频脚本。\n达人信息:${info}\n请先进行账号设定分析,再生成完整脚本。`;
}
if (extra) {
prompt = prompt ? `${prompt}\n补充需求:${extra}` : extra;
}
if (!prompt) { setToast("请输入创作需求"); return; }
setCreativeLoading(true);
fetch("/mcn/api/creative", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
})
.then((r) => r.json())
.then((d) => {
if (!d.ok) throw new Error(d.error || "触发失败");
setToast(d.message || "已开始创作,请切换到对应会话查看");
setCreativeOpen(false);
setCreativePrompt("");
setCreativeAccount(null);
})
.catch((e) => setToast("创作触发失败: " + e.message))
.finally(() => setCreativeLoading(false));
};
(0, react.useEffect)(() => {
if (!toast) return;
const timer = setTimeout(() => setToast(null), 3000);
return () => clearTimeout(timer);
}, [toast]);
return (0, react_jsx_runtime.jsxs)("main", {
className: McnNav_module_css_default.page,
"aria-label": "MCN 工作台",
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.body,
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.header,
children: [(0, react_jsx_runtime.jsxs)("nav", {
className: McnNav_module_css_default.crumb,
"aria-label": "面包屑",
children: view === "home"
? [(0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.crumbCurrent, children: "MCN 工作台" })]
: [(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.crumbLink,
onClick: () => { setEntryParams(null); setView("home"); },
children: "MCN 工作台"
}), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.crumbSep, children: "/" }), (0, react_jsx_runtime.jsx)("span", { className: McnNav_module_css_default.crumbCurrent, children: activeEntry ? activeEntry.title : (view === "ranking" ? "热点数据" : view === "ai-videos" ? "AI写脚本" : view === "script-review" ? "脚本诊断" : "功能页") })]
}), (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.themeBtn, "aria-label": isDarkNow ? "切换到浅色" : "切换到深色", title: isDarkNow ? "切换到浅色" : "切换到深色", onClick: toggleTheme, children: isDarkNow ? (0, react_jsx_runtime.jsx)(IconSun, {}) : (0, react_jsx_runtime.jsx)(IconMoon, {}) }),
(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.sideBtn, "aria-label": side === "right" ? "面板在右侧,点击移到左侧" : "面板在左侧,点击移到右侧", title: side === "right" ? "面板在右侧,点击移到左侧" : "面板在左侧,点击移到右侧", onClick: onToggleSide, children: "⇄" }),
(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.themeBtn, title: "功能帮助", "aria-label": "功能帮助", onClick: () => setHelpOpen(true), children: (0, react_jsx_runtime.jsx)(V1Icon, { icon: "help", size: 15 }) }),
scheduleEntry && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.themeBtn, title: "计划任务:定时提醒/周期任务", "aria-label": "计划任务", onClick: () => openEntry("entry:mcn-schedule"), children: (0, react_jsx_runtime.jsx)(V1Icon, { icon: "clock", size: 15 }) }),
socialEntry && (0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.themeBtn, title: "社媒工作台:Easel 内容工作台", "aria-label": "社媒工作台", onClick: () => openEntry("entry:social-workbench"), children: (0, react_jsx_runtime.jsx)(V1Icon, { icon: "globe", size: 15 }) }),
(0, react_jsx_runtime.jsx)("button", { type: "button", className: McnNav_module_css_default.themeBtn, title: "更多功能", "aria-label": "更多功能", onClick: (e) => { const r = e.currentTarget.getBoundingClientRect(); setMenuAt({ x: Math.max(8, Math.round(r.right) - 172), y: Math.round(r.bottom) + 6 }); setMoreOpen((v) => !v); }, children: (0, react_jsx_runtime.jsx)(V1Icon, { icon: "more", size: 15 }) })
]
}), view === "home" && (0, react_jsx_runtime.jsx)(HomeView, { stats: stats, entries: entries, openEntry: openEntry, openAiVideos: openAiVideosView }), view === "ranking" && (0, react_jsx_runtime.jsx)(RankingSection, {}), view === "ai-videos" && (0, react_jsx_runtime.jsx)(AiVideosPanel, {
videos: aiVideos,
total: aiTotal,
page: aiPage,
loading: aiLoading,
search: aiSearch,
parsed: aiParsed,
onSearch: (s) => { setAiSearch(s); loadAiVideos(1, s, aiParsed); },
onParsedChange: (pr) => { setAiParsed(pr); loadAiVideos(1, aiSearch, pr); },
days: aiDays,
account: aiScope,
onDaysChange: (d) => { setAiDays(d); loadAiVideos(1, aiSearch, aiParsed, false, aiScope, d); },
onClearAccount: clearAiScope,
onPage: (p) => loadAiVideos(p),
onBack: () => { window.__mcnNav?.back(); },
onCreate: (row) => rewriteVideo(row),
onAnalyze: (row) => analyzeVideo(row),
onRewritesChanged: () => loadAiVideos(aiPage),
}), view === "script-review" && (0, react_jsx_runtime.jsx)(ScriptReviewPanel, {
onBack: () => { window.__mcnNav?.back(); },
}), view === "ai-sessions" && (0, react_jsx_runtime.jsx)(AiSessionsView, {
onBack: () => { window.__mcnNav?.back(); },
}), view.startsWith("entry:") && activeEntry && (0, react_jsx_runtime.jsx)(activeEntry.component, {
params: entryParams
})]
}),
helpOpen && react.createElement("div", { style: { position: "fixed", inset: 0, background: "rgba(0,0,0,.35)", zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }, onClick: () => setHelpOpen(false) }, react.createElement("div", { style: { background: "#fff", borderRadius: 12, maxWidth: 640, width: "100%", maxHeight: "82vh", overflow: "auto", padding: "18px 22px", boxShadow: "0 8px 30px rgba(0,0,0,.18)" }, onClick: (e) => e.stopPropagation() }, react.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 10 } }, react.createElement("span", { style: { fontSize: 18, fontWeight: 700, color: "#24292f", flex: 1 } }, "功能帮助"), react.createElement("button", { type: "button", style: { border: "none", background: "none", fontSize: 18, cursor: "pointer", color: "#6b7280" }, title: "关闭", onClick: () => setHelpOpen(false) }, "✕")), react.createElement("div", { style: { color: "#6b7280", fontSize: 13, marginBottom: 12 } }, "直接对 AI 说需求即可,无需记指令(以下为常见问法)。"), V1_HELP_GROUPS.map((g) => react.createElement("div", { key: g[0] }, react.createElement("div", { className: "v1helpTitle" }, g[0]), react.createElement("div", { className: "v1helpDesc" }, g[1]), react.createElement("div", { className: "v1helpQa" }, g[2].map((q) => react.createElement("div", { key: q }, "• ", react.createElement("b", null, q)))))))),
moreOpen && menuAt && react.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 2999 }, onClick: () => setMoreOpen(false) }),
moreOpen && menuAt && react.createElement("div", { style: { position: "fixed", left: menuAt.x, top: menuAt.y, zIndex: 3000, minWidth: 158, background: "#fff", border: "1px solid #e3e6ea", borderRadius: 10, boxShadow: "0 6px 24px rgba(0,0,0,.14)", padding: "6px", display: "flex", flexDirection: "column" } }, react.createElement("button", { type: "button", style: { border: "none", background: "none", textAlign: "left", padding: "8px 12px", borderRadius: 8, fontSize: 14, color: "#24292f", cursor: "pointer", fontFamily: "inherit" }, onClick: () => { setMoreOpen(false); openEntry("entry:mcn-schedule"); } }, "自动任务"), react.createElement("button", { type: "button", style: { border: "none", background: "none", textAlign: "left", padding: "8px 12px", borderRadius: 8, fontSize: 14, color: "#d9534f", cursor: "pointer", fontFamily: "inherit" }, onClick: () => { setMoreOpen(false); onClose(); } }, "关闭面板")), creativeOpen && (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.mask,
onClick: () => setCreativeOpen(false),
children: [(0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modal,
onClick: (e) => e.stopPropagation(),
children: [(0, react_jsx_runtime.jsx)("div", {
className: McnNav_module_css_default.modalTitle,
children: creativeAccount ? `为「${creativeAccount.account_name}」创作短视频脚本` : "短视频脚本创作"
}), creativeAccount && (0, react_jsx_runtime.jsx)("div", {
className: McnNav_module_css_default.modalAccount,
children: [(0, react_jsx_runtime.jsx)("span", {
className: McnNav_module_css_default.modalAccountName,
children: creativeAccount.account_name
}), creativeAccount.track && (0, react_jsx_runtime.jsx)("span", {
className: McnNav_module_css_default.modalAccountTrack,
children: creativeAccount.track
})]
}), (0, react_jsx_runtime.jsx)("div", {
className: McnNav_module_css_default.modalText,
children: creativeAccount
? "已带入该达人信息,确认创作时自动携带。可在下方补充创作需求(可选):"
: "将调用短视频脚本创作技能(会话:dsh_short-video-script)为你创作脚本。请输入创作需求:"
}), (0, react_jsx_runtime.jsx)("textarea", {
className: McnNav_module_css_default.textarea,
rows: 4,
placeholder: creativeAccount ? "可选:补充创作需求(如时长、风格、广告植入等)" : "例如:为美妆账号写一个 30 秒种草脚本,产品是 XX 面膜",
value: creativePrompt,
onChange: (e) => setCreativePrompt(e.target.value)
}), (0, react_jsx_runtime.jsxs)("div", {
className: McnNav_module_css_default.modalBtns,
children: [(0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.btn,
onClick: () => setCreativeOpen(false),
disabled: creativeLoading,
children: "取消"
}), (0, react_jsx_runtime.jsx)("button", {
type: "button",
className: McnNav_module_css_default.btn + " " + McnNav_module_css_default.btnPrimary,
onClick: submitCreative,
disabled: creativeLoading,
children: creativeLoading ? "触发中…" : "开始创作"
})]
})]
})]
}), toast && (0, react_jsx_runtime.jsx)("div", { className: McnNav_module_css_default.toast, children: toast })]
});
}
//#endregion
//#region plugin entry
const inject = ["slots", "sessions"];
function apply(ctx) {
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
name: "sidebar.footer.action",
id: "mcn-nav",
order: 60,
inject: () => ({
openSession: (sessionId) => {
ctx.sessions.open(sessionId);
}
})
}, McnNavTrigger));
// 独立侧边栏入口:AI会话(MCN工作台 下方单独一行)
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
name: "sidebar.footer.action",
id: "mcn-sessions",
order: 61,
inject: () => ({})
}, AiSessionsTrigger));
}
//#endregion
exports.apply = apply;
exports.inject = inject;
return module.exports;
}
});