Files
WorkBuddy df56c2c137 初始化提交:contentm_agent 工作区全量快照
内容分四块:
1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。
2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。
3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。
4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。

.gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
2026-10-08 08:13:02 +08:00

1488 lines
97 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DBG hash=#/p/p1/ok route=p1 lock=false viewKids=6 sheets=1 rules=117 appDisplay=grid bodyBg=rgb(250, 249, 245) | --t-paper=[#faf9f5] --paper=[#faf9f5] --fs-18=[18px] r0=[:root { --t-paper: #faf9f5; --t-surface: #ffffff; --t-surface-soft: #f7f6f1; --t-ink: #1a1a18; --t-ink-mute: #57564f; --t-ink-faint: #8a887f; --t-line: #e7e5dc; --t-line-soft: #efeee7; --t-accent: #9E4CFF; --t-accent-tex]</title>
<style>
/* ============================================================
三层令牌:primitive(--t- 与 --raw- 前缀)→ semantic(无语义前缀)→ component(--c- 前缀)
定调来源:design-system-tiaoyue 亮色基线(9 页 clipper 实测)
⛔ 本文件不设 .eyebrow / .lead / .hero(工具型页面)
============================================================ */
:root{
/* ---- primitive ---- */
--t-paper:#faf9f5; --t-surface:#ffffff; --t-surface-soft:#f7f6f1;
--t-ink:#1a1a18; --t-ink-mute:#57564f; --t-ink-faint:#8a887f;
--t-line:#e7e5dc; --t-line-soft:#efeee7;
--t-accent:#9E4CFF; --t-accent-text:#963dff; --t-on-accent:#FFFFFF;
--t-ok:#3d7a4f; --t-err:#b4453a; --t-warn:#8a5a00;
--raw-radius-card:24px; --raw-radius-panel:16px; --raw-radius-row:14px;
--raw-radius-input:9px; --raw-radius-btn:26px;
/* ---- semantic ---- */
--paper:var(--t-paper); --surface:var(--t-surface); --surface-soft:var(--t-surface-soft);
--ink:var(--t-ink); --ink-mute:var(--t-ink-mute); --ink-faint:var(--t-ink-faint);
--line:var(--t-line); --line-soft:var(--t-line-soft);
--accent:var(--t-accent); --accent-text:var(--t-accent-text); --on-accent:var(--t-on-accent);
--ok:var(--t-ok); --err:var(--t-err); --warn:var(--t-warn);
--fs-12:12px; --fs-14:14px; --fs-16:16px; --fs-18:18px; --fs-20:20px; --fs-24:24px; --fs-30:30px;
--sp-4:4px; --sp-8:8px; --sp-16:16px; --sp-24:24px; --sp-32:32px; --sp-64:64px;
--mg-2:2px; --mg-4:4px; --mg-6:6px; --mg-8:8px; --mg-12:12px;
--font: "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "Noto Sans SC", sans-serif;
--sh-L1: 0 1px 1px rgba(26,26,24,.03), 0 2px 6px -2px rgba(26,26,24,.06);
--sh-L2: 0 1px 2px rgba(26,26,24,.04), 0 8px 20px -12px rgba(26,26,24,.14), 0 18px 40px -18px rgba(26,26,24,.10);
--sh-L3: 0 2px 4px rgba(26,26,24,.05), 0 14px 32px -12px rgba(26,26,24,.18), 0 32px 64px -24px rgba(26,26,24,.16);
/* ---- component ---- */
--c-btn-h:40px; --c-btn-h-sm:32px; --c-btn-px:20px; --c-btn-py:12px;
--c-card-pad:24px; --c-card-pad-dense:16px;
--c-input-h:36px; --c-tag-h:22px;
--nav-w:240px;
--tfast: .15s cubic-bezier(0.32,0.72,0,1);
}
@media (max-width:1024px){ :root{ --nav-w:200px; } }
*,*::before,*::after{ box-sizing:border-box; }
html,body{ margin:0; padding:0; }
body{
background:var(--paper); color:var(--ink);
font-family:var(--font); font-size:var(--fs-14); line-height:1.55;
-webkit-font-smoothing:antialiased;
}
h1,h2,h3{ margin:0; font-weight:600; }
h1{ font-size:var(--fs-24); line-height:1.3; }
h2{ font-size:var(--fs-18); line-height:1.35; }
h3{ font-size:var(--fs-16); line-height:1.4; }
p{ margin:0; }
ul,ol{ margin:0; padding:0; list-style:none; }
button{ font-family:inherit; }
a{ color:var(--accent-text); text-decoration:none; }
a:hover{ text-decoration:underline; }
.num{ font-variant-numeric:tabular-nums; }
.meta{ font-size:var(--fs-12); color:var(--ink-mute); font-variant-numeric:tabular-nums; }
.nowrap{ white-space:nowrap; }
/* 焦点环:⛔ 不许被 outline:none 抹掉 */
:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
/* ============ 外壳:主区 + 侧区 ============ */
.app{ display:grid; grid-template-columns:var(--nav-w) minmax(0,1fr); min-height:100vh; }
.sidebar{
background:var(--paper); border-right:1px solid var(--line-soft);
padding:var(--sp-24) var(--mg-12) var(--sp-24) var(--mg-12);
display:flex; flex-direction:column; gap:var(--sp-24); min-width:0;
}
.brand{ padding:0 var(--mg-8); }
.brand-mark{ font-size:var(--fs-16); font-weight:600; letter-spacing:.01em; }
.nav-group{ display:flex; flex-direction:column; gap:var(--mg-4); }
.nav-group-title{
display:flex; align-items:center; width:100%; height:32px; padding:0 var(--mg-8);
border:0; border-radius:var(--raw-radius-row); background:transparent; text-align:left;
font-family:inherit; font-size:var(--fs-12); color:var(--ink-mute); font-weight:600; cursor:pointer;
transition:background-color var(--tfast);
}
.nav-group-title:hover{ background:var(--surface-soft); }
.nav-group-items{ display:flex; flex-direction:column; gap:var(--mg-2); padding-left:var(--mg-8); }
.nav-item{
display:flex; align-items:center; justify-content:space-between; gap:var(--mg-8);
width:100%; height:36px; padding:0 var(--mg-12); border:0; border-left:3px solid transparent;
background:transparent; color:var(--ink); font-size:var(--fs-14); text-align:left;
border-radius:var(--raw-radius-row); cursor:pointer; min-width:0;
transition:background-color var(--tfast), border-color var(--tfast);
}
.nav-item:hover{ background:var(--surface-soft); }
.nav-item.is-active{ background:var(--surface-soft); border-left-color:var(--accent); font-weight:600; }
.nav-item .nav-count{ font-size:var(--fs-12); color:var(--ink-mute); font-variant-numeric:tabular-nums; }
.main{ display:flex; flex-direction:column; min-width:0; }
.topbar{
display:flex; align-items:center; gap:var(--sp-16); flex-wrap:wrap;
padding:var(--sp-16) var(--sp-32); border-bottom:1px solid var(--line-soft);
background:var(--paper); position:sticky; top:0; z-index:5;
}
.topbar .spacer{ flex:1 1 auto; }
.topbar-label{ font-size:var(--fs-12); color:var(--ink-mute); }
.view{ padding:var(--sp-24) var(--sp-32) var(--sp-64) var(--sp-32); min-width:0; }
/* ============ 控件 ============ */
.btn{
display:inline-flex; align-items:center; justify-content:center; gap:var(--mg-6);
height:var(--c-btn-h); padding:0 var(--c-btn-px); border-radius:var(--raw-radius-btn);
font-size:var(--fs-14); font-weight:500; cursor:pointer; white-space:nowrap;
border:1px solid var(--line); background:var(--surface); color:var(--ink);
transition:background-color var(--tfast), border-color var(--tfast), box-shadow var(--tfast);
}
.btn:hover{ border-color:var(--ink-faint); }
.btn-primary{
background:var(--accent); border-color:var(--accent); color:var(--on-accent);
font-size:var(--fs-16); font-weight:600; /* 4.26:1 需走大字线 3:1,故 ≥16px + 600 */
}
.btn-primary:hover{ border-color:var(--accent); box-shadow:var(--sh-L1); }
.btn-ghost{
height:var(--c-btn-h-sm); padding:0 var(--mg-12); border:0; background:transparent;
color:var(--accent-text); font-size:var(--fs-14);
}
.btn-ghost:hover{ background:var(--surface-soft); }
.btn[disabled],.btn-primary[disabled]{ opacity:.45; cursor:not-allowed; }
.btn[disabled]:hover{ border-color:var(--line); box-shadow:none; }
.btn-sm{ height:var(--c-btn-h-sm); padding:0 var(--mg-12); font-size:var(--fs-14); }
.input,.select,.textarea{
width:100%; min-width:0; border:1px solid var(--line); border-radius:var(--raw-radius-input);
background:var(--surface); color:var(--ink); font-family:inherit;
}
.input,.select{ height:var(--c-input-h); padding:0 var(--mg-12); font-size:var(--fs-14); }
.textarea{ padding:var(--mg-8) var(--mg-12); font-size:var(--fs-14); line-height:1.6; resize:vertical; }
.input[disabled],.textarea[disabled],.select[disabled]{ background:var(--surface-soft); color:var(--ink-faint); cursor:not-allowed; }
.field-label{ display:block; font-size:var(--fs-12); color:var(--ink-mute); font-weight:600; margin-bottom:var(--mg-4); }
.switch{ height:var(--c-input-h); padding:0 var(--mg-8); font-size:var(--fs-14); }
.tag{
display:inline-flex; align-items:center; height:var(--c-tag-h); padding:0 var(--mg-8);
border-radius:999px; background:var(--surface-soft); font-size:var(--fs-12); font-weight:600;
color:var(--ink-mute); white-space:nowrap;
}
.tag-ok{ color:var(--ok); } .tag-err{ color:var(--err); } .tag-warn{ color:var(--warn); }
.tag-assume{ color:var(--warn); }
/* ============ 结构件 ============ */
.block{ margin-top:var(--sp-32); min-width:0; }
.block:first-child{ margin-top:0; }
.block-head{
display:flex; align-items:center; gap:var(--sp-16); flex-wrap:wrap;
padding-bottom:var(--mg-8); border-bottom:1px solid var(--line); margin-bottom:var(--sp-16);
}
.block-head .grow{ flex:1 1 auto; }
.panel{ background:var(--surface); border-radius:var(--raw-radius-panel); box-shadow:var(--sh-L1); padding:var(--c-card-pad-dense); min-width:0; }
.card{ background:var(--surface); border-radius:var(--raw-radius-card); box-shadow:var(--sh-L2); padding:var(--c-card-pad); min-width:0; }
.soft{ background:var(--surface-soft); border-radius:var(--raw-radius-panel); padding:var(--sp-16); min-width:0; }
.grid-main-side{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:var(--sp-24); align-items:start; }
.grid-cards{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:var(--sp-16); }
.row{ display:flex; align-items:center; gap:var(--mg-8); flex-wrap:wrap; min-width:0; }
.stack{ display:flex; flex-direction:column; gap:var(--sp-16); min-width:0; }
.hairline{ border-top:1px solid var(--line-soft); }
/* 行式列表:行间 border-top,⛔ 不给行套框、⛔ 不给行整行底色 */
.list{ display:flex; flex-direction:column; min-width:0; }
.list-row{
display:grid; grid-template-columns:minmax(0,2.2fr) minmax(0,1.6fr) auto;
gap:var(--sp-16); align-items:center; width:100%; text-align:left;
padding:var(--mg-12) var(--mg-8); border:0; border-top:1px solid var(--line-soft);
background:transparent; color:var(--ink); cursor:pointer; font-size:var(--fs-14); min-width:0;
transition:background-color var(--tfast);
}
.list-row:first-child{ border-top:0; }
.list-row:hover{ background:var(--surface-soft); }
.list-row.is-active{ box-shadow:inset 3px 0 0 var(--accent); }
.list-row .primary-cell{ display:flex; flex-direction:column; gap:var(--mg-2); min-width:0; }
.list-row .primary-cell strong{ font-weight:600; }
.list-row .secondary-cell{ display:flex; flex-direction:column; gap:var(--mg-2); min-width:0; font-size:var(--fs-12); color:var(--ink-mute); }
.list-row .action-cell{ display:flex; gap:var(--mg-8); align-items:center; justify-content:flex-end; }
.list-plain{ display:flex; flex-direction:column; }
.list-plain > li{ padding:var(--mg-12) var(--mg-8); border-top:1px solid var(--line-soft); font-size:var(--fs-14); }
.list-plain > li:first-child{ border-top:0; }
table.ds-table{ width:100%; border-collapse:collapse; font-size:var(--fs-14); min-width:0; }
.table-wrap{ overflow-x:auto; min-width:0; }
table.ds-table th{
text-align:left; font-size:var(--fs-12); color:var(--ink-mute); font-weight:600;
padding:var(--mg-8) var(--mg-8); border-bottom:1px solid var(--line);
}
table.ds-table td{ padding:var(--mg-8); border-top:1px solid var(--line-soft); vertical-align:top; }
table.ds-table .num-col{ text-align:right; font-variant-numeric:tabular-nums; }
/* 环节进度 */
.steps{ display:flex; gap:var(--mg-4); flex-wrap:wrap; }
.step{
display:flex; align-items:center; gap:var(--mg-6); height:var(--c-btn-h-sm);
padding:0 var(--mg-12); border:1px solid var(--line); border-radius:999px;
background:var(--surface); font-size:var(--fs-12); cursor:pointer; color:var(--ink-mute);
transition:background-color var(--tfast), border-color var(--tfast), color var(--tfast);
}
.step.done{ color:var(--ink); }
.step.current{ background:var(--ink); border-color:var(--ink); color:var(--surface); font-weight:600; }
.step[disabled]{ opacity:.45; cursor:not-allowed; }
/* 演示开关条 */
.demo{ display:flex; align-items:center; gap:var(--mg-8); }
.statebar{ display:flex; flex-wrap:wrap; gap:var(--mg-4); }
/* 状态面板 */
.state-panel{ background:var(--surface); border-radius:var(--raw-radius-card); box-shadow:var(--sh-L2); padding:var(--sp-64) var(--sp-32); text-align:center; }
.state-panel h2{ margin-bottom:var(--mg-8); }
.state-panel p{ color:var(--ink-mute); max-width:56ch; margin:0 auto var(--sp-16); }
.skel{ height:14px; border-radius:var(--mg-8); background:var(--surface-soft); }
.skel-w70{ width:70%; } .skel-w45{ width:45%; } .skel-w90{ width:90%; }
.skel-stack{ display:flex; flex-direction:column; gap:var(--mg-12); }
/* 遮罩 / 浮层:可见关闭出口唯一(右上 ×) */
.overlay{
position:fixed; inset:0; background:rgba(26,26,24,.34);
display:flex; align-items:flex-start; justify-content:center; padding:var(--sp-64) var(--sp-16);
z-index:50;
}
.layer{
width:min(560px, calc(100vw - 32px)); background:var(--surface);
border-radius:var(--raw-radius-card); box-shadow:var(--sh-L3); padding:var(--sp-24);
max-height:calc(100vh - 128px); overflow:auto; text-align:left;
}
.layer-head{ display:flex; align-items:center; gap:var(--sp-16); margin-bottom:var(--sp-16); }
.layer-head h2{ flex:1 1 auto; }
.layer-close{
width:var(--c-btn-h-sm); height:var(--c-btn-h-sm); flex:0 0 auto; border:1px solid var(--line);
border-radius:999px; background:var(--surface); color:var(--ink); font-size:var(--fs-16);
line-height:1; cursor:pointer;
}
.layer-close:hover{ background:var(--surface-soft); }
.layer-actions{ display:flex; gap:var(--mg-8); justify-content:flex-end; margin-top:var(--sp-24); }
/* 提示条 */
.toast{
position:fixed; left:50%; bottom:var(--sp-32); transform:translateX(-50%);
background:var(--ink); color:var(--surface); font-size:var(--fs-14);
padding:var(--mg-8) var(--sp-16); border-radius:999px; box-shadow:var(--sh-L2); z-index:80;
}
.notice{
display:flex; gap:var(--mg-8); align-items:flex-start; padding:var(--mg-12) var(--sp-16);
border-radius:var(--raw-radius-panel); background:var(--surface-soft); font-size:var(--fs-14);
margin-bottom:var(--sp-16);
}
.notice strong{ font-weight:600; }
.notice-warn strong{ color:var(--warn); }
.notice-err strong{ color:var(--err); }
/* selftest 输出 */
.st-out{ background:#14140f; color:#f2f1ea; border-radius:var(--raw-radius-panel); padding:var(--sp-16); font-family:ui-monospace,Consolas,monospace; font-size:var(--fs-12); white-space:pre-wrap; overflow-x:auto; }
.st-pass{ color:#8fd6a5; } .st-fail{ color:#ff9a8f; }
@media (max-width:1024px){
.grid-cards{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:640px){
.app{ grid-template-columns:minmax(0,1fr); }
.sidebar{
flex-direction:row; align-items:center; gap:var(--mg-4); overflow-x:auto;
border-right:0; border-bottom:1px solid var(--line-soft);
padding:var(--mg-8) var(--mg-8); position:sticky; top:0; z-index:6; background:var(--paper);
}
.brand{ display:none; }
.nav-group{ flex-direction:row; align-items:center; gap:var(--mg-4); }
.nav-group-title{ width:auto; padding:0 var(--mg-8); border-bottom:3px solid transparent; border-radius:999px; }
.nav-group-items{ flex-direction:row; gap:var(--mg-4); padding-left:0; }
.nav-item{ width:auto; border-left:0; border-bottom:3px solid transparent; border-radius:999px; }
.nav-item.is-active{ border-left:0; border-bottom-color:var(--accent); }
.view{ padding:var(--sp-16) var(--sp-16) var(--sp-64); }
.topbar{ padding:var(--mg-12) var(--sp-16); }
.grid-main-side{ grid-template-columns:minmax(0,1fr); }
.grid-cards{ grid-template-columns:minmax(0,1fr); }
.list-row{ grid-template-columns:minmax(0,1fr); }
.list-row .action-cell{ justify-content:flex-start; }
}
@media (prefers-reduced-motion: reduce){
*{ transition:none !important; }
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand"><span class="brand-mark">整合营销工作台</span></div>
<nav id="nav">
<div class="nav-group">
<button class="nav-group-title" data-act="G-01" data-page="p2">找选题</button>
<div class="nav-group-items">
<button class="nav-item " data-act="G-05" data-page="p2">
<span>选题雷达</span><span class="nav-count">5</span>
</button>
<button class="nav-item " data-act="G-05" data-page="p3">
<span>资产库</span><span class="nav-count">6</span>
</button>
</div>
</div>
<div class="nav-group">
<button class="nav-group-title" data-act="G-02" data-page="p4">走创作流水线</button>
<div class="nav-group-items">
<button class="nav-item " data-act="G-05" data-page="p4">
<span>创作台</span>
</button>
<button class="nav-item " data-act="G-05" data-page="p5">
<span>发布中心</span><span class="nav-count">3</span>
</button>
</div>
</div>
<div class="nav-group">
<button class="nav-group-title" data-act="G-03" data-page="p1">看矩阵复盘</button>
<div class="nav-group-items">
<button class="nav-item is-active" data-act="G-05" data-page="p1">
<span>矩阵总览</span><span class="nav-count">5</span>
</button>
<button class="nav-item " data-act="G-05" data-page="p7">
<span>复盘台</span><span class="nav-count">2</span>
</button>
<button class="nav-item " data-act="G-05" data-page="p8">
<span>记录与对账</span><span class="nav-count">5</span>
</button>
</div>
</div>
<div class="nav-group">
<button class="nav-group-title" data-act="G-04" data-page="p6">账号画像(跨组)</button>
<div class="nav-group-items">
<button class="nav-item " data-act="G-05" data-page="p6">
<span>账号画像</span>
</button>
</div>
</div></nav>
</aside>
<div class="main">
<header class="topbar">
<div class="demo">
<span class="topbar-label">账号</span>
<select class="select switch" id="account-pick" data-act="G-07" aria-label="切换账号"><option value="a1" selected="">深夜食堂笔记</option><option value="a2">阿元的下班饭</option><option value="a3">阳台种菜日记</option><option value="a4">校门口那家店</option><option value="a5">咖啡因实验室</option></select>
</div>
<div class="demo">
<span class="topbar-label">看哪个状态</span>
<select class="select switch" id="state-pick" data-act="G-09" aria-label="切换演示状态"><option value="ok" selected="">正常</option><option value="loading">加载中</option><option value="empty">空</option><option value="error">出错</option><option value="disabled">条件未满足</option><option value="noperm">无权限</option></select>
</div>
<div class="spacer"></div>
<button class="btn btn-sm" data-act="G-10">全局检索</button>
<button class="btn btn-sm" data-act="G-06">随手记</button>
<button class="btn btn-sm" data-act="G-08">假设 2 项</button>
</header>
<main class="view" id="view">
<div class="block-head"><h2 class="grow">P1 矩阵总览</h2><span class="tag tag-assume">假设</span></div>
<section class="block">
<div class="block-head"><h2 class="grow">今日待处理</h2><span class="meta">5 项 · 按截止时间排</span></div>
<div class="panel"><ul class="list">
<li><button class="list-row" data-act="P1-01" data-to="p5">
<span class="primary-cell"><strong>周五专题 · 3 个版本等确认</strong><span class="meta">阿元的下班饭 · 截止 今天 12:00</span></span>
<span class="secondary-cell"><span>待审</span></span>
<span class="action-cell"><span class="tag ">待审</span></span>
</button></li>
<li><button class="list-row" data-act="P1-01" data-to="p4">
<span class="primary-cell"><strong>换盆实录 · 停在文案态</strong><span class="meta">阳台种菜日记 · 截止 今天</span></span>
<span class="secondary-cell"><span>在写</span></span>
<span class="action-cell"><span class="tag ">在写</span></span>
</button></li>
<li><button class="list-row" data-act="P1-01" data-to="p7">
<span class="primary-cell"><strong>咖啡因实验室 · 昨天那期数据回了</strong><span class="meta">咖啡因实验室 · 截止 今天</span></span>
<span class="secondary-cell"><span>待复盘</span></span>
<span class="action-cell"><span class="tag ">待复盘</span></span>
</button></li>
<li><button class="list-row" data-act="P1-01" data-to="p8">
<span class="primary-cell"><strong>校门口那家店 · 一条商单记录待核</strong><span class="meta">校门口那家店 · 截止 10-09</span></span>
<span class="secondary-cell"><span>待核销</span></span>
<span class="action-cell"><span class="tag ">待核销</span></span>
</button></li>
<li><button class="list-row" data-act="P1-01" data-to="p2">
<span class="primary-cell"><strong>半夜解馋系列 · 5 条候选待筛</strong><span class="meta">深夜食堂笔记 · 截止 本周</span></span>
<span class="secondary-cell"><span>选题</span></span>
<span class="action-cell"><span class="tag ">选题</span></span>
</button></li></ul></div>
</section>
<section class="block">
<div class="block-head"><h2 class="grow">账号矩阵列表</h2><span class="meta">5 个账号 · 按今日待办排</span></div>
<div class="panel"><ul class="list">
<li><button class="list-row" data-act="P1-02" data-acct="a1">
<span class="primary-cell"><strong>深夜食堂笔记</strong><span class="meta">抖音 · 42 条内容</span></span>
<span class="secondary-cell"><span>今日该发:待发:3 分钟快手菜</span><span>已发 1 条 10:20 · 播放 12.4 万 · 完播 38%</span></span>
<span class="action-cell"><span class="tag tag-warn">数据异动</span>
<span data-act="P1-03" data-acct="a1" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li>
<li><button class="list-row" data-act="P1-02" data-acct="a2">
<span class="primary-cell"><strong>阿元的下班饭</strong><span class="meta">小红书 · 31 条内容</span></span>
<span class="secondary-cell"><span>今日该发:待审 1 条:周五专题</span><span>已发 1 条 09:05 · 点赞 4 820 · 收藏率 9.1%</span></span>
<span class="action-cell">
<span data-act="P1-03" data-acct="a2" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li>
<li><button class="list-row" data-act="P1-02" data-acct="a3">
<span class="primary-cell"><strong>阳台种菜日记</strong><span class="meta">抖音 · 18 条内容</span></span>
<span class="secondary-cell"><span>今日该发:在写:换盆实录</span><span>未发 · 上条播放 2.1 万</span></span>
<span class="action-cell">
<span data-act="P1-03" data-acct="a3" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li>
<li><button class="list-row" data-act="P1-02" data-acct="a4">
<span class="primary-cell"><strong>校门口那家店</strong><span class="meta">视频号 · 9 条内容</span></span>
<span class="secondary-cell"><span>今日该发:待发:探店补拍</span><span>已发 1 条 11:40 · 转发 610 · 评论 88</span></span>
<span class="action-cell"><span class="tag tag-warn">数据异动</span>
<span data-act="P1-03" data-acct="a4" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li>
<li><button class="list-row" data-act="P1-02" data-acct="a5">
<span class="primary-cell"><strong>咖啡因实验室</strong><span class="meta">B 站 · 26 条内容</span></span>
<span class="secondary-cell"><span>今日该发:待复盘 1 条</span><span>已发 1 条 08:30 · 三连率 6.3%</span></span>
<span class="action-cell">
<span data-act="P1-03" data-acct="a5" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li></ul></div>
</section>
<section class="block">
<div class="block-head"><h2 class="grow">异常值提示</h2><span class="meta">2 条异动</span></div>
<div class="panel">
<ul class="list">
<li><button class="list-row" data-act="P1-04" data-acct="a1">
<span class="primary-cell"><strong>深夜食堂笔记</strong><span class="meta">播放 12.4 万 · 完播 38%</span></span>
<span class="secondary-cell"><span>比它平时高出一大截</span></span>
<span class="action-cell"><span class="tag ">去选题雷达</span></span>
</button></li>
<li><button class="list-row" data-act="P1-04" data-acct="a4">
<span class="primary-cell"><strong>校门口那家店</strong><span class="meta">转发 610 · 评论 88</span></span>
<span class="secondary-cell"><span>比它平时高出一大截</span></span>
<span class="action-cell"><span class="tag ">去选题雷达</span></span>
</button></li></ul>
</div>
</section>
<section class="block">
<div class="block-head"><h2 class="grow">本月表现最好的一条</h2></div>
<div class="panel"><div class="row">
<div style="flex:1 1 240px;min-width:0">
<h3>阿元的下班饭 · 周五专题</h3>
<p class="meta">10-02 发布 · 点赞 9 800 · 收藏率 12.4%</p>
</div>
<button class="btn btn-sm" data-act="P1-05">看这条复盘</button>
</div></div>
</section>
<section class="block">
<div class="block-head"><h2 class="grow">全局检索与随手记</h2></div>
<div class="panel"><div class="row">
<button class="btn btn-sm" data-act="G-10">全局检索</button>
<button class="btn btn-sm" data-act="G-06">随手记一条</button>
<span class="meta">随手记在任意页面一次点击内都能碰到。</span>
</div></div>
</section></main>
</div>
</div>
<div id="layer-root"></div>
<script>
/* ============================================================
MCN 短视频整合营销工作台 · 可点通原型
状态由 hash 路由的第二段驱动: #/p/p4/empty
页面骨架照 prd/2b-界面布局.md 的八页(冻结),⛔ 未增删板块
============================================================ */
/* ---------- 数据(示意内容按真实产品的样子写,不写"示例/未接入"字样) ---------- */
const ACCOUNTS = [
{id:'a1',name:'深夜食堂笔记',platform:'抖音',posts:42,today:'待发:3 分钟快手菜',posted:'已发 1 条 10:20',perf:'播放 12.4 万 · 完播 38%',anom:true},
{id:'a2',name:'阿元的下班饭',platform:'小红书',posts:31,today:'待审 1 条:周五专题',posted:'已发 1 条 09:05',perf:'点赞 4 820 · 收藏率 9.1%',anom:false},
{id:'a3',name:'阳台种菜日记',platform:'抖音',posts:18,today:'在写:换盆实录',posted:'未发',perf:'上条播放 2.1 万',anom:false},
{id:'a4',name:'校门口那家店',platform:'视频号',posts:9,today:'待发:探店补拍',posted:'已发 1 条 11:40',perf:'转发 610 · 评论 88',anom:true},
{id:'a5',name:'咖啡因实验室',platform:'B 站',posts:26,today:'待复盘 1 条',posted:'已发 1 条 08:30',perf:'三连率 6.3%',anom:false}
];
const TODOS = [
{id:'t1',kind:'待审',text:'周五专题 · 3 个版本等确认',to:'p5',acct:'阿元的下班饭',due:'今天 12:00'},
{id:'t2',kind:'在写',text:'换盆实录 · 停在文案态',to:'p4',acct:'阳台种菜日记',due:'今天'},
{id:'t3',kind:'待复盘',text:'咖啡因实验室 · 昨天那期数据回了',to:'p7',acct:'咖啡因实验室',due:'今天'},
{id:'t4',kind:'待核销',text:'校门口那家店 · 一条商单记录待核',to:'p8',acct:'校门口那家店',due:'10-09'},
{id:'t5',kind:'选题',text:'半夜解馋系列 · 5 条候选待筛',to:'p2',acct:'深夜食堂笔记',due:'本周'}
];
const BENCH = [
{id:'b1',title:'一个人吃饭也要摆盘',acct:'对标号 · 小食光',mult:'4.8×',when:'3 小时前',tag:'爆款',zone:'低粉爆款'},
{id:'b2',title:'月薪五千怎么吃好一周',acct:'对标号 · 省钱食堂',mult:'3.1×',when:'昨天',tag:'爆款',zone:'低粉爆款'},
{id:'b3',title:'冰箱清库存的三种做法',acct:'对标号 · 冰箱博士',mult:'1.2×',when:'昨天',tag:'常态',zone:'常态'},
{id:'b4',title:'夜宵摊老板不会说的三件事',acct:'对标号 · 夜市老王',mult:'6.4×',when:'前天',tag:'异常',zone:'异常值'},
{id:'b5',title:'十分钟出一餐的偷懒法',acct:'对标号 · 懒人厨房',mult:'0.9×',when:'3 天前',tag:'常态',zone:'常态'}
];
const CANDIDATES = [
{id:'c1',text:'半夜饿了除了泡面还能怎么办',hit:'这条评论被顶了 2.1 千次,是这个号的老问题',state:'idle'},
{id:'c2',text:'一个人住怎么买食材不浪费',hit:'连续 4 条内容下面都在问',state:'idle'},
{id:'c3',text:'有没有不用洗锅的做法',hit:'出现 62 次',state:'idle'}
];
const TOPICS = [
{id:'k1',text:'一个人的晚饭怎么摆得不寒酸',group:'todo',src:'对标号 · 小食光',state:'待做'},
{id:'k2',text:'五千块月薪的一周菜谱',group:'todo',src:'评论区洞察',state:'待做'},
{id:'k3',text:'冰箱清库存系列',group:'writing',src:'灵感速记',state:'在写'},
{id:'k4',text:'夜宵摊的三个坑',group:'todo',src:'对标号 · 夜市老王',state:'待做'},
{id:'k5',text:'十分钟一餐的偷懒法',group:'done',src:'对标号 · 懒人厨房',state:'已用'},
{id:'k6',text:'换盆实录:土到底怎么配',group:'writing',src:'灵感速记',state:'在写'}
];
const ASSETS = [
{id:'as1',zone:'angle',text:'用"一个人"这个处境切,不用"简单"',src:'对标号 · 小食光 · 2026-10-06 那条',state:'已入库',time:'10-06',draft:false},
{id:'as2',zone:'angle',text:'把价格焦虑变成对照,不给结论',src:'对标号 · 省钱食堂 · 2026-10-05',state:'已入库',time:'10-05',draft:false},
{id:'as3',zone:'structure',text:'先给成品,再倒推步骤',src:'对标号 · 冰箱博士 · 2026-10-05',state:'待入库',time:'10-05',draft:false},
{id:'as4',zone:'structure',text:'开头三秒抛反常识,再解释',src:'',state:'待入库',time:'10-07',draft:true},
{id:'as5',zone:'opening',text:'"这句话我听了三年,直到自己试了一次"',src:'对标号 · 夜市老王 · 2026-10-04',state:'已入库',time:'10-04',draft:false},
{id:'as6',zone:'title',text:'数字 + 处境 + 反预期(3 种写法)',src:'对标号 · 懒人厨房 · 2026-10-03',state:'已入库',time:'10-03',draft:false}
];
const STEPS = ['选题态','思路态','文案态','标题态','封面态','检查态','成稿态'];
const VERSIONS = [
{id:'v1',name:'初稿',time:'10-07 21:10',cur:false},
{id:'v2',name:'加了价格对照',time:'10-08 09:02',cur:false},
{id:'v3',name:'缩到 45 秒',time:'10-08 10:15',cur:true}
];
const RECORDS = [
{id:'rec1',text:'深夜食堂笔记 · 3 分钟快手菜',by:'陈队长',time:'10-08 10:20',ver:'v3',state:'已发布',acct:'深夜食堂笔记',settle:'待结算',amount:'¥ 3 200'},
{id:'rec2',text:'阿元的下班饭 · 周五专题',by:'李编导',time:'10-08 09:05',ver:'v2',state:'已发布',acct:'阿元的下班饭',settle:'已结算',amount:'—'},
{id:'rec3',text:'校门口那家店 · 探店补拍',by:'陈队长',time:'10-08 11:40',ver:'v1',state:'待审',acct:'校门口那家店',settle:'未登记',amount:'—'},
{id:'rec4',text:'咖啡因实验室 · 手冲水温那期',by:'李编导',time:'10-07 08:30',ver:'v4',state:'已发布',acct:'咖啡因实验室',settle:'已结算',amount:'¥ 1 800'},
{id:'rec5',text:'阳台种菜日记 · 换盆实录',by:'李编导',time:'10-06 19:12',ver:'v1',state:'已归档',acct:'阳台种菜日记',settle:'未登记',amount:'—'}
];
const REVIEWS = [
{id:'r1',text:'咖啡因实验室 · 手冲水温那期',acct:'咖啡因实验室',day:'10-07',data:'到了',play:'8.6 万',fin:'41%',tri:'6.3%',note:'结论:水温段讲太长,掉在 38 秒处'},
{id:'r2',text:'深夜食堂笔记 · 3 分钟快手菜',acct:'深夜食堂笔记',day:'10-08',data:'到了',play:'12.4 万',fin:'38%',tri:'—',note:''},
{id:'r3',text:'校门口那家店 · 探店补拍',acct:'校门口那家店',day:'10-06',data:'未回',play:'—',fin:'—',tri:'—',note:''}
];
const PROFILES = {
a1:{定位:'城市独居青年的快手饭',风格:'暖色实拍 · 无口播 · 字幕为主',受众:'22–30 岁,一二线独居',平台:'抖音为主,同步小红书',偏好:'真实厨房、不做摆拍;不接减肥类',记忆:['10-06 观众对"一人食"这个词有共鸣,标题里保留','10-07 45 秒以下完播明显更好']},
a2:{定位:'下班后的一顿正经饭',风格:'慢剪 · 有人声 · 长镜头',受众:'25–35 岁,做饭新手',平台:'小红书',偏好:'不夸大功效、不用"必吃"',记忆:['10-05 收藏率靠步骤图带起来']},
a3:{定位:'阳台可复制的小规模种菜',风格:'记录向 · 手持 · 过程为主',受众:'30–45 岁',平台:'抖音',偏好:'不承诺产量',记忆:[]},
a4:{定位:'小城小店的故事',风格:'人物访谈 · 现场收音',受众:'本地 + 怀旧向',平台:'视频号',偏好:'不摆拍老板、不虚构故事',记忆:['10-04 转发集中在"开店多少年"这句']},
a5:{定位:'把咖啡讲得不玄',风格:'实验对比 · 数据可视化',受众:'咖啡爱好者',平台:'B 站',偏好:'不吹器具、不做器材带货',记忆:['10-07 水温段太长,38 秒处开始掉']}
};
const CONTENT = {key:'k3',title:'冰箱清库存系列 · 第 3 条',step:2,stepOut:['选题:冰箱剩菜怎么配','思路:先给成品,再倒推三步','','','','','']};
/* ---------- 状态 ---------- */
const S = {
route:'p1', pageState:'ok', account:'a1',
layer:null, noteDraft:'', searchQ:'',
topics:JSON.parse(JSON.stringify(TOPICS)), topicGroup:'todo',
benchFilter:'all', commentOpen:false, commentState:'idle',
candidates:JSON.parse(JSON.stringify(CANDIDATES)),
assets:JSON.parse(JSON.stringify(ASSETS)), assetZone:'angle', assetSel:'as1',
refs:[],
step:CONTENT.step, stepOut:CONTENT.stepOut.slice(), stepBusy:false,
versionOpen:false,
targetOpen:false, targets:{accounts:['a1','a2'],platforms:['抖音']},
versions:JSON.parse(JSON.stringify(VERSIONS)), verSel:'v1', verDraft:null, verDirty:false,
checkState:'idle', confirmOpen:false, publishDone:false,
profileDirty:false, memoryOpen:false,
reviewSel:'r1', attrDraft:'', historyOpen:false, wroteBack:false,
recordSel:'rec1', traceOpen:false, billingOpen:false, checkedOut:false,
toast:null
};
const PAGES = {
p1:'矩阵总览', p2:'选题雷达', p3:'资产库', p4:'创作台',
p5:'发布中心', p6:'账号画像', p7:'复盘台', p8:'记录与对账'
};
const NAV = [
{group:'找选题', act:'G-01', items:[['p2','选题雷达'],['p3','资产库']]},
{group:'走创作流水线', act:'G-02', items:[['p4','创作台'],['p5','发布中心']]},
{group:'看矩阵复盘', act:'G-03', items:[['p1','矩阵总览'],['p7','复盘台'],['p8','记录与对账']]},
{group:'账号画像(跨组)', act:'G-04', items:[['p6','账号画像']]}
];
const STATES = [['ok','正常'],['loading','加载中'],['empty','空'],['error','出错'],['disabled','条件未满足'],['noperm','无权限']];
/* ---------- 每页各状态的说明(逐页不同,⛔ 不用一句通用话糊过去) ---------- */
const STATE_COPY = {
p1:{loading:['正在取矩阵数据','5 个账号的今日数据正在汇总。'],empty:['还没接入账号','矩阵总览要先有账号。去账号画像建第一个账号。','p6'],error:['数据源不可用','最近一次成功取值:10-08 09:12。数据源没回话,这里不显示空白冒充正常。'],disabled:['今天没有要处理的事','待处理列表为空,主操作暂时没有对象。'],noperm:['利润相关字段需要管理员权限','你当前是编导角色,账号矩阵里的利润列对你不显示。']},
p2:{loading:['正在拉对标更新','3 个在监控的账号正在取近三天内容。'],empty:['监控名单是空的','先录一个对标号,才能看到它的更新和异常值。'],error:['拉取失败','平台授权可能已过期。可以先手工录入对标号,不必卡在这里。'],disabled:['还没选账号','选题要落到某个账号上才能转入创作。'],noperm:['—','']},
p3:{loading:['条目加载中','正在读这个分区的拆解结果。'],empty:['这个分区还没有条目','去选题雷达发一条拆解,结果会落到这里。','p2'],error:['读取失败','本地知识库目录没读出来。重试一次,或检查目录是否还在。'],disabled:['这条还不能入库','它没有出处,按门禁先留在草稿里。补上出处就能入库。'],noperm:['—','']},
p4:{loading:['正在跑这一步','文案生成中,已跑 24 秒。'],empty:['这一步还没有产出','先在上面写点方向,再点开始。'],error:['这一步失败了','已产出的内容保留在这里,可以直接重试这一步。'],disabled:['还差封面','文案有了、封面还没有。进检查态前封面必须有。'],noperm:['—','']},
p5:{loading:['检查中','硬闸与软闸正在过这一批版本。'],empty:['还没有可发布的版本','成稿之后才会有版本。回创作台把内容推到成稿。','p4'],error:['检查本身没跑成','按不过处理:宁拦不放。改完可以复检。'],disabled:['有硬拦项没处理','敏感信息这一处不改完,确认发布点不动。'],noperm:['这个账号你没有发布权限','校门口那家店归陈队长。你可以看版本,不能替它发。']},
p6:{loading:['画像加载中','正在读这个账号的口径与记忆。'],empty:['还没有画像','建一份画像,这个账号才有自己的口径。'],error:['保存失败','你改的内容还在,没有被清掉。重试一次。'],disabled:['这个账号你只能看','它不是归你负责的账号,字段锁住了。'],noperm:['没有改画像的权限','你可以看,改动需要内容负责人来做。']},
p7:{loading:['正在读这条的数据','已发布内容的回读最多 90 秒。'],empty:['还没有能复盘的内容','先有发布记录,才有复盘。','p8'],error:['数据读不到','可以手工录入这条的表现,归因照做。'],disabled:['归因还没写','没有结论就不能回写,回写会改画像记忆。'],noperm:['—','']},
p8:{loading:['记录加载中','正在读发布留痕。'],empty:['还没有发布记录','确认发布之后,这里会留一条。'],error:['读取失败','本地留痕库没读出来。重试一次。'],disabled:['还没选中记录','先点一条记录,才有东西可核销。'],noperm:['利润列对你隐藏','你不是管理员,收支金额列不显示。其余列照常可看。']}
};
/* ---------- 反馈提示 ---------- */
let toastTimer=null;
function toast(text){ S.toast=text; paint(); clearTimeout(toastTimer); toastTimer=setTimeout(()=>{ S.toast=null; paint(); },1500); }
/* ---------- 路由 ---------- */
function parseHash(){
const raw=(location.hash||'').replace(/^#\/?/,'');
const parts=raw.split('/').filter(Boolean); // ['p','p4','empty']
if(parts[0]==='selftest'){ S.route='selftest'; S.pageState='ok'; return; }
let page=parts[1]||'p1'; let st=parts[2]||'ok';
if(!PAGES[page]) page='p1';
if(!STATES.some(s=>s[0]===st)) st='ok';
S.route=page; S.pageState=st;
}
function go(page,st){ location.hash='#/p/'+page+'/'+(st||'ok'); }
/* ---------- 元素 ---------- */
const $=s=>document.querySelector(s);
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function el(html){ const d=document.createElement('div'); d.innerHTML=html.trim(); return d.firstElementChild; }
const ACTIONS={}; // data-act → 处理函数(交互清单的落地处)
/* ---------- 导航 ---------- */
function renderNav(){
$('#nav').innerHTML = NAV.map(g=>`
<div class="nav-group">
<button class="nav-group-title" data-act="${g.act}" data-page="${g.items[0][0]}">${esc(g.group)}</button>
<div class="nav-group-items">
${g.items.map(([id,label])=>`
<button class="nav-item ${S.route===id?'is-active':''}" data-act="G-05" data-page="${id}">
<span>${esc(label)}</span>${navCount(id)}
</button>`).join('')}
</div>
</div>`).join('');
}
function navCount(id){
const n={p1:TODOS.length,p2:BENCH.length,p3:S.assets.length,p5:S.versions.length,p7:REVIEWS.filter(r=>r.data==='到了').length,p8:RECORDS.length}[id];
return n?'<span class="nav-count">'+n+'</span>':'';
}
/* ---------- 通用结构件 ---------- */
function head(title,right){
return `<div class="block-head"><h2 class="grow">${esc(title)}</h2>${right||''}</div>`;
}
function tag(text,cls){ return `<span class="tag ${cls||''}">${esc(text)}</span>`; }
const ASSUME_TAG='<span class="tag tag-assume">假设</span>';
/* ============================ 八页 ============================ */
function pageP1(){
const todos=TODOS.map(t=>`
<li><button class="list-row" data-act="P1-01" data-to="${t.to}">
<span class="primary-cell"><strong>${esc(t.text)}</strong><span class="meta">${esc(t.acct)} · 截止 ${esc(t.due)}</span></span>
<span class="secondary-cell"><span>${esc(t.kind)}</span></span>
<span class="action-cell">${tag(t.kind,'')}</span>
</button></li>`).join('');
const rows=ACCOUNTS.map(a=>`
<li><button class="list-row" data-act="P1-02" data-acct="${a.id}">
<span class="primary-cell"><strong>${esc(a.name)}</strong><span class="meta">${esc(a.platform)} · ${a.posts} 条内容</span></span>
<span class="secondary-cell"><span>今日该发:${esc(a.today)}</span><span>${esc(a.posted)} · ${esc(a.perf)}</span></span>
<span class="action-cell">${a.anom?tag('数据异动','tag-warn'):''}
<span data-act="P1-03" data-acct="${a.id}" class="btn btn-ghost" role="button" tabindex="0">看复盘</span></span>
</button></li>`).join('');
const anom=ACCOUNTS.filter(a=>a.anom);
return `
${head('P1 矩阵总览', ASMAY_NOTE_P1)}
<section class="block">
${head('今日待处理', `<span class="meta">${TODOS.length} 项 · 按截止时间排</span>`)}
<div class="panel"><ul class="list">${todos}</ul></div>
</section>
<section class="block">
${head('账号矩阵列表', `<span class="meta">${ACCOUNTS.length} 个账号 · 按今日待办排</span>`)}
<div class="panel"><ul class="list">${rows}</ul></div>
</section>
<section class="block">
${head('异常值提示', `<span class="meta">${anom.length} 条异动</span>`)}
<div class="panel">
${anom.length?`<ul class="list">${anom.map(a=>`
<li><button class="list-row" data-act="P1-04" data-acct="${a.id}">
<span class="primary-cell"><strong>${esc(a.name)}</strong><span class="meta">${esc(a.perf)}</span></span>
<span class="secondary-cell"><span>比它平时高出一大截</span></span>
<span class="action-cell">${tag('去选题雷达','')}</span>
</button></li>`).join('')}</ul>`:`<p class="meta">目前没有异动。</p>`}
</div>
</section>
<section class="block">
${head('本月表现最好的一条', '')}
<div class="panel"><div class="row">
<div style="flex:1 1 240px;min-width:0">
<h3>阿元的下班饭 · 周五专题</h3>
<p class="meta">10-02 发布 · 点赞 9 800 · 收藏率 12.4%</p>
</div>
<button class="btn btn-sm" data-act="P1-05">看这条复盘</button>
</div></div>
</section>
<section class="block">
${head('全局检索与随手记', '')}
<div class="panel"><div class="row">
<button class="btn btn-sm" data-act="G-10">全局检索</button>
<button class="btn btn-sm" data-act="G-06">随手记一条</button>
<span class="meta">随手记在任意页面一次点击内都能碰到。</span>
</div></div>
</section>`;
}
const ASMAY_NOTE_P1 = ASSUME_TAG;
function pageP2(){
const zone=S.benchFilter;
const list=BENCH.filter(b=>zone==='all'||b.zone===zone);
const low=BENCH.filter(b=>b.zone==='低粉爆款').length, anom=BENCH.filter(b=>b.zone==='异常值').length;
const topicList=S.topics.filter(t=>t.group===S.topicGroup);
const groupLabel={todo:'待做',writing:'在写',done:'已用'};
return `
${head('P2 选题雷达','')}
<section class="block">
${head('对标监控概览', `<span class="meta">在监控 3 个账号</span>`)}
<div class="grid-cards">
<div class="card"><p class="meta">在监控账号</p><p class="num" style="font-size:var(--fs-30)">3</p></div>
<button class="card" data-act="P2-01" ${low?'':'disabled'} style="text-align:left;border:0;cursor:pointer">
<p class="meta">低粉爆款</p><p class="num" style="font-size:var(--fs-30)">${low}</p></button>
<button class="card" data-act="P2-02" ${anom?'':'disabled'} style="text-align:left;border:0;cursor:pointer">
<p class="meta">异常值</p><p class="num" style="font-size:var(--fs-30)">${anom}</p></button>
<div class="card"><p class="meta">近三天更新</p><p class="num" style="font-size:var(--fs-30)">${BENCH.length}</p></div>
</div>
${zone!=='all'?`<p class="meta" style="margin-top:var(--mg-8)">列表已按「${esc(zone)}」筛过 · 点概览里的数字可再筛</p>`:''}
</section>
<section class="block">
${head('对标内容列表', `<span class="meta">${list.length} 条 · 倍数=相对该号平时水平</span>`)}
<div class="panel"><ul class="list">${list.map(b=>`
<li><div class="list-row">
<span class="primary-cell"><strong>${esc(b.title)}</strong><span class="meta">${esc(b.acct)} · ${esc(b.when)}</span></span>
<span class="secondary-cell"><span class="num">${esc(b.mult)} ${esc(b.tag)}</span></span>
<span class="action-cell"><button class="btn btn-sm" data-act="P2-03" data-bench="${b.id}">发起拆解</button></span>
</div></li>`).join('')}</ul></div>
</section>
<section class="block">
${head('评论区洞察', `<button class="btn btn-ghost" data-act="P2-04">${S.commentOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.commentOpen?`
<div class="row" style="margin-bottom:var(--sp-16)">
<button class="btn btn-sm" data-act="P2-05" ${S.commentState==='fetching'?'disabled':''}>抓取这条的评论</button>
<span class="meta">按点赞最多取前 20 条</span>
</div>
${S.commentState==='fetching'?`<div class="skel-stack"><div class="skel skel-w90"></div><div class="skel skel-w70"></div><div class="skel skel-w45"></div></div>`:''}
${S.commentState==='done'?`<ul class="list">${S.candidates.map(c=>`
<li><div class="list-row">
<span class="primary-cell"><strong>${esc(c.text)}</strong><span class="meta">${esc(c.hit)}</span></span>
<span class="secondary-cell"><span>${c.state==='idle'?'待处理':(c.state==='adopted'?'已采纳':'已忽略')}</span></span>
<span class="action-cell">
<button class="btn btn-sm" data-act="P2-06" data-c="${c.id}" ${c.state!=='idle'?'disabled':''}>采纳</button>
<button class="btn btn-sm" data-act="P2-07" data-c="${c.id}" ${c.state!=='idle'?'disabled':''}>忽略</button>
</span>
</div></li>`).join('')}</ul>`:''}
${S.commentState==='idle'?`<p class="meta">展开后点抓取,从这条内容的评论区里出候选选题。抓不到可以手工粘评论。</p>`:''}
`:`<p class="meta">这里不占主屏:从对标列表拿选题是主路,评论区是另一路输入。</p>`}
</div>
</section>
<section class="block">
${head('选题库', `<span class="meta">${topicList.length} 条</span>`)}
<div class="panel">
<div class="row" style="margin-bottom:var(--sp-16)">
${['todo','writing','done'].map(g=>`<button class="btn btn-sm" data-act="P2-08" data-group="${g}" ${S.topicGroup===g?'style="border-color:var(--ink);font-weight:600"':''}>${groupLabel[g]}</button>`).join('')}
</div>
<ul class="list">${topicList.map(t=>`
<li><div class="list-row">
<span class="primary-cell"><strong>${esc(t.text)}</strong><span class="meta">来源:${esc(t.src)}</span></span>
<span class="secondary-cell"><span>${esc(t.state)}</span></span>
<span class="action-cell">
<button class="btn btn-sm" data-act="P2-09" data-k="${t.id}" ${t.group==='done'?'disabled':''}>转入创作</button>
<button class="btn btn-sm" data-act="P2-10" data-k="${t.id}">拆解</button>
</span>
</div></li>`).join('')}</ul>
</div>
</section>
<section class="block">
${head('灵感速记', '')}
<div class="panel"><div class="row">
<button class="btn btn-sm" data-act="G-06">随手记一条</button>
<span class="meta">随手记在任意页面一次点击内都能碰到。</span>
</div></div>
</section>`;
}
function pageP3(){
const zones=[['angle','角度'],['structure','结构'],['opening','开场白'],['title','标题']];
const list=S.assets.filter(a=>a.zone===S.assetZone);
const sel=S.assets.find(a=>a.id===S.assetSel)||list[0];
const pending=S.assets.filter(a=>a.state==='待入库');
return `
${head('P3 资产库','')}
<section class="block">
${head('分区切换', `<span class="meta">一次看一个分区</span>`)}
<div class="panel"><div class="row">
${zones.map(([z,l])=>`<button class="btn btn-sm" data-act="P3-01" data-zone="${z}" ${S.assetZone===z?'style="border-color:var(--ink);font-weight:600"':''}>${l} <span class="num">${S.assets.filter(a=>a.zone===z).length}</span></button>`).join('')}
</div></div>
</section>
<div class="block grid-main-side">
<section>
${head('条目列表', `<span class="meta">${list.length} 条</span>`)}
<div class="panel"><ul class="list">${list.map(a=>`
<li><button class="list-row ${sel&&a.id===sel.id?'is-active':''}" data-act="P3-02" data-a="${a.id}">
<span class="primary-cell"><strong>${esc(a.text)}</strong><span class="meta">${a.src?('出处:'+esc(a.src)):'没有出处'}</span></span>
<span class="secondary-cell"><span>${esc(a.state)} · ${esc(a.time)}</span></span>
<span class="action-cell">${a.draft?tag('草稿','tag-warn'):''}</span>
</button></li>`).join('')}</ul></div>
</section>
<section>
${head('单条详情', sel?`<span class="meta">${esc(sel.time)} 采集</span>`:'')}
<div class="panel">
${sel?`
<p class="field-label">原文</p><p>${esc(sel.text)}</p>
<p class="field-label" style="margin-top:var(--sp-16)">出处</p><p>${sel.src?esc(sel.src):'没有出处 —— 按门禁只能留在草稿,补上出处才能入库。'}</p>
<div class="row" style="margin-top:var(--sp-16)">
${sel.draft?tag('草稿','tag-warn'):tag('已入库','tag-ok')}
${sel.state==='待入库'?`<button class="btn btn-sm" data-act="P3-03" data-a="${sel.id}" ${sel.src?'':'disabled'}>入库</button>`:''}
</div>
`:`<p class="meta">这个分区还没有条目。</p>`}
</div>
</section>
</div>
<section class="block">
${head('引用入口', `<span class="meta">把这条拿去创作台用</span>`)}
<div class="panel"><div class="row">
<button class="btn btn-sm" data-act="P3-04" data-a="${sel?sel.id:''}" ${sel?'':'disabled'}>引用到创作台</button>
<span class="meta">还有 ${pending.filter(p=>p.src).length} 条待入库,可入库的先入。</span>
</div></div>
</section>`;
}
function pageP4(){
const out=S.stepOut[S.step]||'';
return `
${head('P4 创作台','')}
<section class="block">
${head('当前内容', `<span class="meta">来自选题库</span>`)}
<div class="panel"><div class="row">
<div style="flex:1 1 260px;min-width:0">
<h3>${esc(CONTENT.title)}</h3>
<p class="meta">账号:${esc(ACCOUNTS[0].name)} · 当前版本 ${esc((S.versions.find(v=>v.cur)||S.versions[0]).id)}</p>
</div>
<button class="btn btn-sm" data-act="P4-07">版本列表 <span class="num">${S.versions.length}</span></button>
</div></div>
</section>
<section class="block">
${head('环节进度', `<span class="meta">第 ${S.step+1}/7 步 · 可回退</span>`)}
<div class="panel"><div class="steps">
${STEPS.map((s,i)=>`<button class="step ${i<S.step?'done':''} ${i===S.step?'current':''}" data-act="P4-01" data-i="${i}" ${i>S.step?'disabled':''}>${i+1} ${esc(s)}</button>`).join('')}
</div></div>
</section>
<section class="block">
${head('当前环节工作区', `<span class="meta">${esc(STEPS[S.step])}</span>`)}
<div class="panel">
<label class="field-label" for="p4-in">这一步要产出什么</label>
<textarea class="textarea" id="p4-in" data-act="P4-02" rows="5" placeholder="写清方向,点开始这一步">${esc(out)}</textarea>
<div class="row" style="margin-top:var(--sp-16);justify-content:space-between">
<button class="btn" data-act="P4-03" ${out?'':'disabled'}>${S.stepBusy?'生成中…':'开始这一步'}</button>
<button class="btn btn-primary" data-act="P4-06">推进到「${esc(STEPS[Math.min(S.step+1,6)])}」</button>
</div>
${S.stepBusy?'<div class="skel-stack" style="margin-top:var(--sp-16)"><div class="skel skel-w90"></div><div class="skel skel-w70"></div></div>':''}
</div>
</section>
<section class="block">
${head('资产引用区', `<span class="meta">${S.refs.length} 条已引用</span>`)}
<div class="panel">
${S.refs.length?`<ul class="list">${S.refs.map(r=>`
<li><div class="list-row">
<span class="primary-cell"><strong>${esc(r.text)}</strong><span class="meta">来自资产库 · ${esc(r.zone)}</span></span>
<span class="secondary-cell"></span>
<span class="action-cell"><button class="btn btn-sm" data-act="P4-05" data-a="${r.id}">移除引用</button></span>
</div></li>`).join('')}</ul>`:`<p class="meta">还没有引用资产。这里可以空着写,但引过一条的角度和结构,稿子会稳一些。</p>`}
<div class="row" style="margin-top:var(--sp-16)">
<button class="btn btn-sm" data-act="P4-04">从资产库拿一条</button>
</div>
</div>
</section>
<section class="block">
${head('版本', `<button class="btn btn-ghost" data-act="P4-07">${S.versionOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.versionOpen?`<ul class="list">${S.versions.map(v=>`
<li><div class="list-row">
<span class="primary-cell"><strong>${esc(v.id)}</strong><span class="meta">${esc(v.name)}</span></span>
<span class="secondary-cell"><span>${esc(v.time)}</span></span>
<span class="action-cell">${v.cur?tag('当前版','tag-ok'):`<button class="btn btn-sm" data-act="P4-08" data-v="${v.id}">切为当前版</button>`}</span>
</div></li>`).join('')}</ul>`:`<p class="meta">共 ${S.versions.length} 个版本。展开可看每个版本改了什么、切回旧版。</p>`}
</div>
</section>`;
}
function pageP5(){
const targetAccts=ACCOUNTS.filter(a=>S.targets.accounts.includes(a.id));
const vers=targetAccts.flatMap(a=>S.targets.platforms.map(p=>({key:a.id+'|'+p,acct:a.name,platform:p})));
const hard=S.checkState==='hard', soft=S.checkState==='soft';
return `
${head('P5 发布中心','')}
<section class="block">
${head('目标选择', `<button class="btn btn-ghost" data-act="P5-01">${S.targetOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.targetOpen?`
<p class="field-label">发给哪些账号</p>
<div class="row" style="margin-bottom:var(--sp-16)">${ACCOUNTS.map(a=>`<button class="btn btn-sm" data-act="P5-02" data-acct="${a.id}" ${S.targets.accounts.includes(a.id)?'style="border-color:var(--ink);font-weight:600"':''}>${esc(a.name)}</button>`).join('')}</div>
<p class="field-label">发给哪些平台</p>
<div class="row">${['抖音','小红书','视频号','B 站'].map(p=>`<button class="btn btn-sm" data-act="P5-03" data-p="${p}" ${S.targets.platforms.includes(p)?'style="border-color:var(--ink);font-weight:600"':''}>${esc(p)}</button>`).join('')}</div>
`:`<p class="meta">这次发 ${targetAccts.length} 个账号 × ${S.targets.platforms.length} 个平台 = ${vers.length} 个版本。从创作台带版本进来时目标已定,这里是补选。</p>`}
</div>
</section>
<div class="block grid-main-side">
<section>
${head('各版本预览与逐版编辑', `<span class="meta">${vers.length} 版</span>`)}
<div class="panel">
<div class="row" style="margin-bottom:var(--sp-16)">${vers.map(v=>`<button class="btn btn-sm" data-act="P5-04" data-v="${v.key}" ${S.verSel===v.key?'style="border-color:var(--ink);font-weight:600"':''}>${esc(v.acct)} / ${esc(v.platform)}</button>`).join('')}</div>
${vers.length?(()=>{ const cur=vers.find(v=>v.key===S.verSel)||vers[0]; return `
<p class="field-label" for="p5-edit">${esc(cur.acct)} · ${esc(cur.platform)}</p>
<textarea class="textarea" id="p5-edit" data-act="P5-05" rows="4">${esc(S.verDraft!==null?S.verDraft:('把这条内容按「'+cur.platform+'」的口径改一版:'+CONTENT.title))}</textarea>
<div class="row" style="margin-top:var(--sp-16)"><button class="btn btn-sm" data-act="P5-06" ${S.verDirty?'':'disabled'}>保存本版</button>${S.verDirty?tag('已改','tag-warn'):''}</div>
`})():'<p class="meta">还没有选目标账号。</p>'}
</div>
</section>
<section>
${head('检查结果', `<span class="meta">硬的拦住 · 软的只提醒</span>`)}
<div class="panel">
${S.checkState==='idle'?`<p class="meta">这一批还没过闸。</p>`:''}
${S.checkState==='checking'?`<div class="skel-stack"><div class="skel skel-w90"></div><div class="skel skel-w70"></div></div>`:''}
${hard?`<div class="notice notice-err"><strong>硬拦 1 处</strong><span>版本里出现了账号密钥(第 2 版第 3 行)。这一处不改完,不能发。</span></div>`:''}
${soft?`<div class="notice notice-warn"><strong>软劝 1 处</strong><span>这条的说话方式和这个账号平时的口径差得有点远。只是提醒,不拦你发。</span></div>`:''}
${S.checkState==='pass'?`<div class="notice"><strong>没有硬拦项</strong><span>两处软劝已看过。可以发。</span></div>`:''}
<div class="row" style="margin-top:var(--sp-16)"><button class="btn btn-sm" data-act="P5-07" ${S.checkState==='checking'?'disabled':''}>复检</button></div>
</div>
</section>
</div>
<section class="block">
${head('确认发布', `<span class="meta">这一步由人来点</span>`)}
<div class="panel"><div class="row" style="justify-content:space-between">
<span class="meta">${S.publishDone?'这一批已经登记进发布记录。':'发出后会在记录与对账里留一条,谁在什么时候确认的都查得到。'}</span>
<button class="btn btn-primary" data-act="P5-08" ${(hard||!vers.length||S.publishDone)?'disabled':''}>确认发布</button>
</div></div>
</section>
<section class="block">
${head('发布记录', '')}
<div class="panel"><div class="row">
<button class="btn btn-sm" data-act="P5-11">去记录与对账</button>
<span class="meta">目前 ${RECORDS.length} 条记录。</span>
</div></div>
</section>`;
}
function pageP6(){
const acc=ACCOUNTS.find(a=>a.id===S.account)||ACCOUNTS[0];
const p=PROFILES[acc.id]||{定位:'',风格:'',受众:'',平台:'',偏好:'',记忆:[]};
const fields=[['定位','定位'],['风格','风格'],['受众','受众'],['平台','平台'],['偏好','偏好与红线']];
return `
${head('P6 账号画像','')}
<div class="block grid-main-side">
<section>
${head('账号列表', `<span class="meta">${ACCOUNTS.length} 个</span>`)}
<div class="panel"><ul class="list">${ACCOUNTS.map(a=>`
<li><button class="list-row ${a.id===acc.id?'is-active':''}" data-act="P6-01" data-acct="${a.id}">
<span class="primary-cell"><strong>${esc(a.name)}</strong><span class="meta">${esc(a.platform)}</span></span>
<span class="secondary-cell"><span>${a.posts} 条内容</span></span>
<span class="action-cell"></span>
</button></li>`).join('')}</ul></div>
</section>
<section>
${head('画像六维', `<span class="meta">${esc(acc.name)}</span>`)}
<div class="panel">
${fields.map(([k,label])=>`
<div style="margin-bottom:var(--sp-16)">
<label class="field-label" for="p6-${k}">${esc(label)}</label>
<input class="input" id="p6-${k}" data-act="P6-02" data-k="${k}" value="${esc(p[k]||'')}">
</div>`).join('')}
<div>
<p class="field-label">长期记忆</p>
<p class="meta">${(p.记忆||[]).length} 条 · 只追加,不覆盖历史结论</p>
</div>
<div class="row" style="margin-top:var(--sp-16);justify-content:space-between">
<span class="meta">${S.profileDirty?'有改动没保存':'没有改动'}</span>
<button class="btn btn-primary" data-act="P6-03" ${S.profileDirty?'':'disabled'}>保存画像</button>
</div>
</div>
</section>
</div>
<section class="block">
${head('记忆条目', `<button class="btn btn-ghost" data-act="P6-04">${S.memoryOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.memoryOpen?((p.记忆&&p.记忆.length)?`<ul class="list-plain">${p.记忆.map(m=>`<li>${esc(m)}</li>`).join('')}</ul>`:`<p class="meta">这个账号还没有记忆条目。复盘回写会往这里加。</p>`):`<p class="meta">记忆是复盘回写的产物,不是编辑画像的输入。</p>`}
</div>
</section>
<section class="block">
${head('关联内容', '')}
<div class="panel">
${(S.topics.filter(t=>t.src.includes(acc.name)||true).slice(0,3).length)?`<ul class="list">${S.topics.slice(0,3).map(t=>`
<li><button class="list-row" data-act="P6-05" data-k="${t.id}">
<span class="primary-cell"><strong>${esc(t.text)}</strong><span class="meta">来源:${esc(t.src)}</span></span>
<span class="secondary-cell"><span>${esc(t.state)}</span></span>
<span class="action-cell">${tag('去创作台','')}</span>
</button></li>`).join('')}</ul>`:`<p class="meta">这个账号还没有做过内容。</p>`}
</div>
</section>`;
}
function pageP7(){
const r=REVIEWS.find(x=>x.id===S.reviewSel)||REVIEWS[0];
const acc=ACCOUNTS.find(a=>a.name===r.acct)||ACCOUNTS[0];
return `
${head('P7 复盘台','')}
<div class="block grid-main-side">
<section>
${head('待复盘列表', `<span class="meta">${REVIEWS.length} 条</span>`)}
<div class="panel"><ul class="list">${REVIEWS.map(x=>`
<li><button class="list-row ${x.id===r.id?'is-active':''}" data-act="P7-01" data-r="${x.id}">
<span class="primary-cell"><strong>${esc(x.text)}</strong><span class="meta">${esc(x.acct)} · ${esc(x.day)} 发布</span></span>
<span class="secondary-cell"><span>${x.data==='到了'?'数据已回':'数据还没回来'}</span></span>
<span class="action-cell">${x.data==='到了'?tag('可复盘','tag-ok'):tag('等数据','tag-warn')}</span>
</button></li>`).join('')}</ul></div>
</section>
<section>
${head('这一条的表现数据', `<span class="meta">${esc(r.day)} 回读</span>`)}
<div class="panel">
${r.data==='到了'?`
<div class="grid-cards" style="grid-template-columns:repeat(3,minmax(0,1fr))">
<div><p class="meta">播放</p><p class="num" style="font-size:var(--fs-20)">${esc(r.play)}</p></div>
<div><p class="meta">完播</p><p class="num" style="font-size:var(--fs-20)">${esc(r.fin)}</p></div>
<div><p class="meta">三连率</p><p class="num" style="font-size:var(--fs-20)">${esc(r.tri)}</p></div>
</div>
`:`<div class="notice notice-warn"><strong>数据还没回来</strong><span>可以先手工录这条的表现,归因照做,不用等。</span></div>
<button class="btn btn-sm" data-act="P7-02">手工录入表现</button>`}
</div>
</section>
</div>
<section class="block">
${head('归因结论', `<span class="meta">这一栏由人来定,机器只给数据</span>`)}
<div class="panel">
<label class="field-label" for="p7-attr">这条为什么是这个结果</label>
<textarea class="textarea" id="p7-attr" data-act="P7-03" rows="4" placeholder="写下你的判断。写了才能回写。">${esc(S.attrDraft||r.note)}</textarea>
</div>
</section>
<section class="block">
${head('回写目标', `<span class="meta">这次回写到哪个账号</span>`)}
<div class="panel"><div class="row" style="justify-content:space-between">
<div style="flex:1 1 240px;min-width:0">
<h3>${esc(acc.name)}</h3>
<p class="meta">${S.wroteBack?'已回写一条记忆到这份画像。':'回写会往这份画像的记忆里加一条,不改历史结论。'}</p>
</div>
<button class="btn btn-primary" data-act="P7-04" ${(S.attrDraft||r.note)?'':'disabled'}>确认归因并回写</button>
</div></div>
</section>
<section class="block">
${head('历史复盘', `<button class="btn btn-ghost" data-act="P7-05">${S.historyOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.historyOpen?`<ul class="list-plain">
<li>10-06 · 深夜食堂笔记:45 秒以下完播明显更好</li>
<li>10-05 · 阿元的下班饭:步骤图带起收藏率</li>
<li>10-04 · 校门口那家店:转发集中在"开店多少年"这句</li>
</ul>`:`<p class="meta">当前这次复盘不受历史影响,但写的时候可以参照。</p>`}
</div>
</section>`;
}
function pageP8(){
const r=RECORDS.find(x=>x.id===S.recordSel)||RECORDS[0];
return `
${head('P8 记录与对账','')}
<div class="block grid-main-side">
<section>
${head('发布记录列表', `<span class="meta">${RECORDS.length} 条 · 只追加不改写</span>`)}
<div class="panel"><ul class="list">${RECORDS.map(x=>`
<li><button class="list-row ${x.id===r.id?'is-active':''}" data-act="P8-01" data-rec="${x.id}">
<span class="primary-cell"><strong>${esc(x.text)}</strong><span class="meta">${esc(x.by)} · ${esc(x.time)} · ${esc(x.ver)}</span></span>
<span class="secondary-cell"><span>${esc(x.state)}</span></span>
<span class="action-cell">${x.state==='已发布'?tag('可核销',''):tag(x.state,'tag-warn')}</span>
</button></li>`).join('')}</ul></div>
</section>
<section>
${head('单条溯源', `<button class="btn btn-ghost" data-act="P8-02">${S.traceOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.traceOpen?`
<ul class="list-plain">
<li>内容:${esc(r.text)}</li>
<li>版本:${esc(r.ver)}(当时发布用的就是这一版,之后改稿不影响它)</li>
<li>确认人:${esc(r.by)} · ${esc(r.time)}</li>
<li>检查留痕:硬拦 0 · 软劝 1(10-08 09:40)</li>
</ul>`:`<p class="meta">核销看列表就够;要深究这条发的是哪一版,点开这里。</p>`}
</div>
</section>
</div>
<section class="block">
${head('对账核对区', `<span class="meta">内容 · 发布记录 · 收支 三样对得上</span>`)}
<div class="panel">
<div class="table-wrap"><table class="ds-table">
<thead><tr><th>项</th><th>值</th><th>对得上</th></tr></thead>
<tbody>
<tr><td>内容</td><td>${esc(r.text)}</td><td>是</td></tr>
<tr><td>发布记录</td><td>${esc(r.by)} · ${esc(r.time)} · ${esc(r.ver)}</td><td>${r.state==='已发布'?'是':'还没有确认记录'}</td></tr>
<tr><td>收支</td><td class="num">${esc(r.amount)}</td><td>${r.amount==='—'?'没有商单':'与记录一致'}</td></tr>
</tbody>
</table></div>
<div class="row" style="margin-top:var(--sp-16);justify-content:space-between">
<span class="meta">${S.checkedOut?'这条已经核销。':'核销代表这三样你核过了。'}</span>
<button class="btn btn-primary" data-act="P8-03" ${(S.checkedOut||r.state!=='已发布')?'disabled':''}>核销这条记录</button>
</div>
</div>
</section>
<section class="block">
${head('商单与收支', `${ASSUME_TAG}<button class="btn btn-ghost" data-act="P8-04">${S.billingOpen?'收起':'展开'}</button>`)}
<div class="panel">
${S.billingOpen?`
<div class="notice notice-warn"><strong>本块是假设</strong><span>商单对账进不进第一版、收支口径是不是内容级,都还没有结论。这里按「进第一版、口径按内容级」画。</span></div>
<ul class="list-plain">
<li>${esc(r.text)} · 商单金额 <span class="num">${esc(r.amount)}</span> · ${esc(r.settle)}</li>
<li>深夜食堂笔记 · 10-08 · <span class="num">¥ 3 200</span> · 待结算</li>
<li>咖啡因实验室 · 10-07 · <span class="num">¥ 1 800</span> · 已结算</li>
</ul>`:`<p class="meta">一块 P2 的收支记录,可整块没有。</p>`}
</div>
</section>`;
}
/* ---------- 页面骨架渲染 ---------- */
const RENDER={p1:pageP1,p2:pageP2,p3:pageP3,p4:pageP4,p5:pageP5,p6:pageP6,p7:pageP7,p8:pageP8};
/* ---------- 页面级状态面板(逐页不同的话术) ---------- */
function statePanel(page){
const st=S.pageState; const c=(STATE_COPY[page]||{})[st];
if(!c||!c[1]) return '';
const [title,desc,to]=c;
const icons={loading:'',empty:'',error:'',disabled:'',noperm:''};
let body='';
if(st==='loading') body=`<div class="skel-stack" style="max-width:520px;margin:0 auto var(--sp-24)"><div class="skel skel-w90"></div><div class="skel skel-w70"></div><div class="skel skel-w45"></div></div>`;
if(st==='empty'&&to) body=`<button class="btn btn-primary" data-act="STATE-GO" data-to="${to}">去${esc(PAGES[to])}</button>`;
if(st==='error') body=`<button class="btn btn-sm" data-act="STATE-RETRY">重试一次</button>`;
if(st==='disabled') body=`<span class="tag tag-warn">条件未满足</span>`;
if(st==='noperm') body=`<span class="tag tag-warn">角色限制</span>`;
return `<section class="block"><div class="state-panel"><h2>${esc(title)}</h2><p>${esc(desc)}</p>${icons[st]}${body}</div></section>`;
}
/* ---------- 浮层 ---------- */
function layerHTML(){
const L=S.layer; if(!L) return '';
const close=`<button class="layer-close" data-act="X-close" aria-label="关闭">×</button>`;
if(L==='quicknote') return `<div class="layer">
<div class="layer-head"><h2>随手记一条</h2>${close}</div>
<label class="field-label" for="note">想到了什么</label>
<input class="input" id="note" data-act="R-01" value="${esc(S.noteDraft)}" placeholder="一句话就够,先进待做">
<div class="layer-actions">
<button class="btn" data-act="R-03">取消</button>
<button class="btn btn-primary" data-act="R-02" ${S.noteDraft?'':'disabled'}>保存进待做</button>
</div></div>`;
if(L==='search'){
const q=S.searchQ.trim();
const hits=q?[
{t:'选题 · '+ (S.topics.find(k=>k.text.includes(q))?.text||q),to:'p2'},
{t:'资产 · '+(S.assets.find(a=>a.text.includes(q))?.text||q),to:'p3'},
{t:'记录 · '+(RECORDS.find(r=>r.text.includes(q))?.text||q),to:'p8'}
]:[];
return `<div class="layer">
<div class="layer-head"><h2>全局检索</h2>${close}</div>
<label class="field-label" for="q">找选题、资产、记录</label>
<input class="input" id="q" data-act="S-01" value="${esc(S.searchQ)}" placeholder="打两个字就开始筛">
<div style="margin-top:var(--sp-16)">
${q?(hits.length?`<ul class="list">${hits.map(h=>`<li><button class="list-row" data-act="S-02" data-to="${h.to}"><span class="primary-cell"><strong>${esc(h.t)}</strong></span><span class="secondary-cell"><span>去 ${esc(PAGES[h.to])}</span></span><span class="action-cell"></span></button></li>`).join('')}</ul>`:`<p class="meta">没有对上的条目。</p>`):`<p class="meta">输入关键字,结果按选题、资产、记录三类给。</p>`}
</div></div>`;
}
if(L==='assume') return `<div class="layer">
<div class="layer-head"><h2>两处还没结论的假设</h2>${close}</div>
<ul class="list-plain">
<li><strong>假设 1 · P1 的形态</strong><br>矩阵级视图是否真需要,还没有结论。这一版按「需要」画:P1 是矩阵总览,5 个板块都在。若结论是「不需要」,P1 降为单账号今日,只留今日待处理与本月最好,矩阵列表下移到账号切换。<br><span class="meta">复核人:主会话转用户 · 时机:②段收口时过 · 落点:2b §四 与 §二 P1</span></li>
<li><strong>假设 2 · P8 的第 4 板块</strong><br>商单对账进不进第一版、收支口径是不是内容级,还没有结论。这一版按「进第一版、口径按内容级」画,整块可缺。<br><span class="meta">复核人:主会话转用户 · 时机:同上 · 落点:2b §四 与 §二 P8</span></li>
</ul></div>`;
if(L==='versions') return `<div class="layer">
<div class="layer-head"><h2>版本列表</h2>${close}</div>
<ul class="list">${S.versions.map(v=>`<li><div class="list-row">
<span class="primary-cell"><strong>${esc(v.id)}</strong><span class="meta">${esc(v.name)}</span></span>
<span class="secondary-cell"><span>${esc(v.time)}</span></span>
<span class="action-cell">${v.cur?tag('当前版','tag-ok'):`<button class="btn btn-sm" data-act="P4-08" data-v="${v.id}">切为当前版</button>`}</span>
</div></li>`).join('')}</ul></div>`;
if(L==='assetpick') return `<div class="layer">
<div class="layer-head"><h2>从资产库拿一条</h2>${close}</div>
<ul class="list">${S.assets.filter(a=>a.state==='已入库').map(a=>`<li><div class="list-row">
<span class="primary-cell"><strong>${esc(a.text)}</strong><span class="meta">${esc(a.zone)} · 出处:${esc(a.src)}</span></span>
<span class="secondary-cell"></span>
<span class="action-cell"><button class="btn btn-sm" data-act="PICK-ASSET" data-a="${a.id}">引用</button></span>
</div></li>`).join('')}</ul></div>`;
if(L==='confirm') return `<div class="layer">
<div class="layer-head"><h2>确认发出这一批</h2>${close}</div>
<p>${S.targets.accounts.length} 个账号 × ${S.targets.platforms.length} 个平台。发出后会写进发布记录,谁在什么时候确认的都查得到。</p>
<div class="layer-actions">
<button class="btn" data-act="P5-10">取消</button>
<button class="btn btn-primary" data-act="P5-09">确认发出</button>
</div></div>`;
if(L==='checkout') return `<div class="layer">
<div class="layer-head"><h2>核销这条记录</h2>${close}</div>
<p>核销代表内容、发布记录、收支三样你都核过了。之后要改,得二次确认。</p>
<div class="layer-actions">
<button class="btn" data-act="X-close">取消</button>
<button class="btn btn-primary" data-act="CHECKOUT-OK">确认核销</button>
</div></div>`;
return '';
}
/* ---------- 绘制 ---------- */
const ST={ lock:false }; // 自检期间锁住异步重绘,防自检结果被覆盖
function paint(){
if(ST.lock) return;
renderNav();
const ap=$('#account-pick'); if(ap){ ap.innerHTML=ACCOUNTS.map(a=>`<option value="${a.id}" ${a.id===S.account?'selected':''}>${esc(a.name)}</option>`).join(''); }
const sp=$('#state-pick'); if(sp){ sp.innerHTML=STATES.map(([v,l])=>`<option value="${v}" ${v===S.pageState?'selected':''}>${esc(l)}</option>`).join(''); }
const v=$('#view');
if(S.route==='selftest'){ v.innerHTML=renderSelftest(); return; }
const pageHTML = (S.pageState==='ok') ? RENDER[S.route]() : statePanel(S.route)+`<section class="block"><div class="panel"><div class="row"><span class="meta">这里是 ${esc(PAGES[S.route])} 的 ${esc(S.pageState)} 态。</span><button class="btn btn-sm" data-act="STATE-NORMAL">回到正常态</button></div></div></section>`;
v.innerHTML = pageHTML;
$('#layer-root').innerHTML = layerHTML();
if(S.toast){ $('#layer-root').insertAdjacentHTML('beforeend', `<div class="toast" role="status">${esc(S.toast)}</div>`); }
}
/* ============================ 交互清单(唯一真相源) ============================ */
/* data-act 值即 DESIGN.md §2.4 的 ID。下面每个键都必须有实现,否则自检报红。 */
const IMPL = {
/* 外壳 */
'G-01':()=>go('p2'), 'G-02':()=>go('p4'), 'G-03':()=>go('p1'), 'G-04':()=>go('p6'),
'G-06':()=>{ S.layer='quicknote'; paint(); setTimeout(()=>{const n=$('#note'); if(n) n.focus();},0); },
'G-07':null, /* select,由 change 事件处理 */
'G-08':()=>{ S.layer='assume'; paint(); },
'G-09':null, /* select */
'G-10':()=>{ S.layer='search'; paint(); setTimeout(()=>{const q=$('#q'); if(q) q.focus();},0); },
'X-close':()=>{ S.layer=null; paint(); },
'R-01':null,'S-01':null, /* 输入框 */
'R-02':()=>{ if(!S.noteDraft) return; S.topics.unshift({id:'n'+Date.now(),text:S.noteDraft,group:'todo',src:'灵感速记',state:'待做'});
S.layer=null; S.noteDraft=''; paint(); toast('已进选题库待做'); },
'R-03':()=>{ S.layer=null; S.noteDraft=''; paint(); },
'S-02':e=>{ const to=e.target.closest('[data-to]').dataset.to; S.layer=null; S.searchQ=''; go(to); },
'STATE-NORMAL':()=>{ S.pageState='ok'; if(S.route==='selftest'){ location.hash='#/p/p1/ok'; return; } go(S.route,'ok'); },
'STATE-RETRY':()=>{ toast('已重试一次'); },
'STATE-GO':e=>{ go(e.target.closest('[data-to]').dataset.to); },
/* P1 */
'P1-01':e=>{ go(e.target.closest('[data-to]').dataset.to); },
'P1-02':e=>{ S.account=e.target.closest('[data-acct]').dataset.acct; S.layer=null; go('p6'); },
'P1-03':e=>{ const a=e.target.closest('[data-acct]').dataset.acct; S.account=a; go('p7'); },
'P1-04':()=>{ S.benchFilter='异常值'; go('p2'); },
'P1-05':()=>{ S.reviewSel='r2'; go('p7'); },
/* P2 */
'P2-01':()=>{ S.benchFilter='低粉爆款'; paint(); },
'P2-02':()=>{ S.benchFilter='异常值'; paint(); },
'P2-03':e=>{ S.assetZone='structure'; S.assetSel='as3'; go('p3'); },
'P2-04':()=>{ S.commentOpen=!S.commentOpen; paint(); },
'P2-05':()=>{ S.commentState='fetching'; paint(); setTimeout(()=>{ S.commentState='done'; paint(); },420); },
'P2-06':e=>{ const c=e.target.closest('[data-c]').dataset.c; const it=S.candidates.find(x=>x.id===c); it.state='adopted';
S.topics.unshift({id:'c'+c,text:it.text,group:'todo',src:'评论区洞察',state:'待做'}); paint(); toast('已进选题库待做'); },
'P2-07':e=>{ const c=e.target.closest('[data-c]').dataset.c; S.candidates.find(x=>x.id===c).state='ignored'; paint(); },
'P2-08':e=>{ S.topicGroup=e.target.closest('[data-group]').dataset.group; paint(); },
'P2-09':e=>{ const k=e.target.closest('[data-k]').dataset.k; const t=S.topics.find(x=>x.id===k); t.group='writing'; t.state='在写'; go('p4'); },
'P2-10':()=>{ S.assetZone='angle'; go('p3'); },
/* P3 */
'P3-01':e=>{ S.assetZone=e.target.closest('[data-zone]').dataset.zone; const l=S.assets.filter(a=>a.zone===S.assetZone); if(l[0]) S.assetSel=l[0].id; paint(); },
'P3-02':e=>{ S.assetSel=e.target.closest('[data-a]').dataset.a; paint(); },
'P3-03':e=>{ const a=e.target.closest('[data-a]').dataset.a; const it=S.assets.find(x=>x.id===a); it.state='已入库'; it.draft=false; paint(); toast('已入库'); },
'P3-04':e=>{ const a=e.target.closest('[data-a]').dataset.a; const it=S.assets.find(x=>x.id===a); if(!S.refs.some(r=>r.id===a)) S.refs.push({id:a,text:it.text,zone:it.zone}); go('p4'); },
/* P4 */
'P4-01':e=>{ S.step=+e.target.closest('[data-i]').dataset.i; paint(); },
'P4-02':null,
'P4-03':()=>{ S.stepBusy=true; paint(); setTimeout(()=>{ S.stepBusy=false; S.stepOut[S.step]=(S.stepOut[S.step]||'')||'机器出的一版,写在上面,你可以改'; paint(); },420); },
'P4-04':()=>{ S.layer='assetpick'; paint(); },
'P4-05':e=>{ const a=e.target.closest('[data-a]').dataset.a; S.refs=S.refs.filter(r=>r.id!==a); paint(); },
'P4-06':()=>{ if(S.step<6){ S.step++; if(!S.stepOut[S.step]) S.stepOut[S.step]=''; paint(); toast('已推进到「'+STEPS[S.step]+'」'); } },
'P4-07':()=>{ if(S.layer==='versions'){ S.layer=null; paint(); } else { S.layer='versions'; paint(); } },
'P4-08':e=>{ const v=e.target.closest('[data-v]').dataset.v; S.versions.forEach(x=>x.cur=(x.id===v)); paint(); toast('当前版切成 '+v); },
'PICK-ASSET':e=>{ const a=e.target.closest('[data-a]').dataset.a; const it=S.assets.find(x=>x.id===a); if(!S.refs.some(r=>r.id===a)) S.refs.push({id:a,text:it.text,zone:it.zone}); S.layer=null; paint(); toast('已引用 '+a); },
/* P5 */
'P5-01':()=>{ S.targetOpen=!S.targetOpen; paint(); },
'P5-02':e=>{ const a=e.target.closest('[data-acct]').dataset.acct; const i=S.targets.accounts.indexOf(a);
if(i>=0) S.targets.accounts.splice(i,1); else S.targets.accounts.push(a);
if(!S.targets.accounts.length){ const o=ACCOUNTS.find(x=>x.id!==a); S.targets.accounts.push(o.id); }
paint(); },
'P5-03':e=>{ const p=e.target.closest('[data-p]').dataset.p; const i=S.targets.platforms.indexOf(p);
if(i>=0) S.targets.platforms.splice(i,1); else S.targets.platforms.push(p);
if(!S.targets.platforms.length) S.targets.platforms.push(p==='抖音'?'小红书':'抖音');
paint(); },
'P5-04':e=>{ S.verSel=e.target.closest('[data-v]').dataset.v; S.verDraft=null; S.verDirty=false; paint(); },
'P5-05':null,
'P5-06':()=>{ S.verDirty=false; S.checkState='idle'; paint(); toast('本版已保存,检查结果要重跑'); },
'P5-07':()=>{ S.checkState='checking'; paint(); setTimeout(()=>{ S.checkState='pass'; paint(); },420); },
'P5-08':()=>{ S.layer='confirm'; paint(); },
'P5-09':()=>{ S.layer=null; S.publishDone=true; paint(); toast('已登记一条发布记录'); },
'P5-10':()=>{ S.layer=null; paint(); },
'P5-11':()=>go('p8'),
/* P6 */
'P6-01':e=>{ S.account=e.target.closest('[data-acct]').dataset.acct; S.profileDirty=false; paint(); },
'P6-02':null,
'P6-03':()=>{ S.profileDirty=false; paint(); toast('画像已保存'); },
'P6-04':()=>{ S.memoryOpen=!S.memoryOpen; paint(); },
'P6-05':()=>go('p4'),
/* P7 */
'P7-01':e=>{ S.reviewSel=e.target.closest('[data-r]').dataset.r; S.wroteBack=false; paint(); },
'P7-02':()=>{ toast('已切到手工录入'); },
'P7-03':null,
'P7-04':()=>{ S.wroteBack=true; const r=REVIEWS.find(x=>x.id===S.reviewSel); const acc=ACCOUNTS.find(a=>a.name===r.acct);
if(acc&&PROFILES[acc.id]) PROFILES[acc.id].记忆.push('10-08 '+r.text+':'+(S.attrDraft||r.note)); paint(); toast('已回写到 '+r.acct); },
'P7-05':()=>{ S.historyOpen=!S.historyOpen; paint(); },
/* P8 */
'P8-01':e=>{ S.recordSel=e.target.closest('[data-rec]').dataset.rec; S.checkedOut=false; paint(); },
'P8-02':()=>{ S.traceOpen=!S.traceOpen; paint(); },
'P8-03':()=>{ S.layer='checkout'; paint(); },
'P8-04':()=>{ S.billingOpen=!S.billingOpen; paint(); },
'CHECKOUT-OK':()=>{ S.layer=null; S.checkedOut=true; paint(); toast('这条已核销'); }
};
ACTIONS.G05PAGE=e=>{ const p=e.target.closest('[data-page]'); if(p) go(p.dataset.page); };
/* ============================ 事件绑定 ============================ */
document.addEventListener('click', ev=>{
const t=ev.target.closest('[data-act]'); if(!t) return;
const act=t.dataset.act;
/* 从自检页离开:任何导航动作都先把自检锁解开 */
if(act && (act.startsWith('G-0')||act.startsWith('STATE-'))) ST.lock=false;
if(act==='G-05'){ ACTIONS.G05PAGE({target:t}); return; }
if(t.hasAttribute('disabled')) return;
const fn=IMPL[act];
if(typeof fn==='function') fn({target:t});
});
document.addEventListener('input', ev=>{
const t=ev.target.closest('[data-act]'); if(!t) return;
const act=t.dataset.act;
if(act==='R-01'){ S.noteDraft=t.value; const b=document.querySelector('[data-act="R-02"]'); if(b) b.disabled=!t.value.trim(); return; }
if(act==='S-01'){ S.searchQ=t.value; const q=t.value,pos=t.selectionStart; paint(); const n=$('#q'); if(n){ n.value=q; n.focus(); n.setSelectionRange(pos,pos); } return; }
if(act==='P4-02'){ S.stepOut[S.step]=t.value; const b=document.querySelector('[data-act="P4-03"]'); if(b) b.disabled=!t.value.trim(); return; }
if(act==='P5-05'){ S.verDraft=t.value; S.verDirty=true; const b=document.querySelector('[data-act="P5-06"]'); if(b) b.disabled=false; return; }
if(act==='P6-02'){ S.profileDirty=true; const b=document.querySelector('[data-act="P6-03"]'); if(b) b.disabled=false; return; }
if(act==='P7-03'){ S.attrDraft=t.value; const b=document.querySelector('[data-act="P7-04"]'); if(b) b.disabled=!t.value.trim(); return; }
});
document.addEventListener('change', ev=>{
const t=ev.target.closest('[data-act]'); if(!t) return;
if(t.dataset.act==='G-07'){ S.account=t.value; paint(); toast('已切到 '+t.value); }
if(t.dataset.act==='G-09'){ S.pageState=t.value; paint(); }
});
document.addEventListener('keydown', ev=>{ if(ev.key==='Escape'&&S.layer&&!ST.lock){ S.layer=null; paint(); } });
window.addEventListener('hashchange', ()=>{ if(ST.lock) return; parseHash(); paint(); });
/* ============================ 自检(#/selftest) ============================ */
let CONSOLE_ERRORS=[];
window.addEventListener('error', e=>{ const m='error: '+e.message; CONSOLE_ERRORS.push(m); showErr(m); });
window.addEventListener('unhandledrejection', e=>{ const m='rejection: '+e.reason; CONSOLE_ERRORS.push(m); showErr(m); });
function showErr(m){
let b=document.getElementById('errbar');
if(!b){ b=document.createElement('div'); b.id='errbar'; b.className='notice notice-err';
b.style.cssText='position:fixed;left:16px;right:16px;bottom:16px;z-index:99;margin:0';
document.body.appendChild(b); }
b.innerHTML='<strong>页面报错</strong><span></span>';
b.lastElementChild.textContent=m;
}
/* 场景=「某个页面 / 某个浮层打开着」。自检在真实场景里跑,不在空壳上跑。 */
const SCENES=[
{name:'P1 矩阵总览', set:()=>{ S.route='p1'; S.pageState='ok'; S.layer=null; }},
{name:'P2 选题雷达', set:()=>{ S.route='p2'; S.pageState='ok'; S.layer=null; S.commentOpen=true; S.commentState='done'; }},
{name:'P3 资产库', set:()=>{ S.route='p3'; S.pageState='ok'; S.layer=null; S.assetSel='as3'; }},
{name:'P4 创作台', set:()=>{ S.route='p4'; S.pageState='ok'; S.layer=null; S.versionOpen=true; }},
{name:'P5 发布中心', set:()=>{ S.route='p5'; S.pageState='ok'; S.layer=null; S.targetOpen=true; S.checkState='idle'; }},
{name:'P5 有硬拦', set:()=>{ S.route='p5'; S.pageState='ok'; S.layer=null; S.targetOpen=true; S.checkState='hard'; }},
{name:'P6 账号画像', set:()=>{ S.route='p6'; S.pageState='ok'; S.layer=null; S.memoryOpen=true; }},
{name:'P7 复盘台', set:()=>{ S.route='p7'; S.pageState='ok'; S.layer=null; S.reviewSel='r1'; S.attrDraft=''; }},
{name:'P8 记录对账', set:()=>{ S.route='p8'; S.pageState='ok'; S.layer=null; S.recordSel='rec1'; }},
{name:'外壳(非 P1)', set:()=>{ S.route='p8'; S.pageState='ok'; S.layer=null; }},
{name:'随手记浮层', set:()=>{ S.route='p1'; S.pageState='ok'; S.layer='quicknote'; S.noteDraft=''; }},
{name:'检索浮层空', set:()=>{ S.route='p1'; S.pageState='ok'; S.layer='search'; S.searchQ=''; }},
{name:'检索有结果', set:()=>{ S.route='p1'; S.pageState='ok'; S.layer='search'; S.searchQ='选题'; }},
{name:'假设浮层', set:()=>{ S.route='p1'; S.pageState='ok'; S.layer='assume'; }},
{name:'版本浮层', set:()=>{ S.route='p4'; S.pageState='ok'; S.layer='versions'; }},
{name:'资产选择浮层', set:()=>{ S.route='p4'; S.pageState='ok'; S.layer='assetpick'; }},
{name:'发布二次确认', set:()=>{ S.route='p5'; S.pageState='ok'; S.layer='confirm'; S.checkState='pass'; }},
{name:'核销二次确认', set:()=>{ S.route='p8'; S.pageState='ok'; S.layer='checkout'; }}
];
function applyScene(sc){
sc.set();
/* ⚠️ file:// 下 history.replaceState 可能被拒(origin 为 opaque),拒了就只改内存态 */
try{ if(PAGES[S.route]) history.replaceState(null,'','#/p/'+S.route+'/'+S.pageState); }catch(e){}
paint();
}
const NO_CLICK=['G-05','G-07','G-09']; // 二级页项 / 两个下拉:另有专门核法
const NOOP_OK=['STATE-NORMAL','STATE-RETRY'];
function _renderSelftest(){
const L=[]; const pass=[]; const fail=[];
const ok=(t)=>{pass.push(t); L.push('<span class="st-pass">PASS</span> '+t);};
const bad=(t)=>{fail.push(t); L.push('<span class="st-fail">FAIL</span> '+t);};
const saved={route:S.route,pageState:S.pageState,hash:location.hash};
ST.lock=false;
const sig=()=>JSON.stringify([location.hash,S.layer,S.route,S.pageState,S.step,S.commentOpen,S.commentState,
S.versionOpen,S.targetOpen,S.assetSel,S.assetZone,S.recordSel,S.reviewSel,S.historyOpen,S.traceOpen,
S.billingOpen,S.memoryOpen,S.topicGroup,S.benchFilter,S.publishDone,S.wroteBack,S.checkedOut,
S.profileDirty,S.verDirty,S.verSel,S.attrDraft,S.noteDraft,S.searchQ,S.refs.length,
S.topics.length,S.versions.map(v=>v.cur),S.toast]);
/* 1) 八页都能渲染出内容 */
Object.keys(PAGES).forEach(p=>{
S.route=p; S.pageState='ok'; S.layer=null; paint();
const n=$('#view').children.length;
if(n>0) ok('路由 #/p/'+p+' 渲染出 '+n+' 个顶层节点'); else bad('路由 #/p/'+p+' 是空的');
});
/* 2) 六态在每一页都能渲染 */
let stFail=0;
Object.keys(PAGES).forEach(p=>{
STATES.forEach(([st])=>{
S.route=p; S.pageState=st; S.layer=null; paint();
if(!($('#view').children.length>0)) { stFail++; bad('P'+p.slice(1)+' 的 '+st+' 态没渲染出内容'); }
});
});
if(stFail===0) ok('六态 × 八页 = '+ (Object.keys(PAGES).length*STATES.length) +' 个组合,全部渲染出内容');
/* 3) 交互清单 ↔ 页面:双向差集 */
const declared=new Set(Object.keys(IMPL));
const seen=new Set(); const extra=[]; let domCount=0;
const navPages=new Set();
SCENES.forEach(sc=>{
applyScene(sc);
document.querySelectorAll('[data-act]').forEach(n=>{
const a=n.dataset.act;
if(a==='G-05'){ navPages.add(n.dataset.page); return; }
domCount++; seen.add(a);
if(!declared.has(a)) extra.push(a+'@'+sc.name);
});
});
navPages.delete(undefined);
if(navPages.size===Object.keys(PAGES).length) ok('侧栏二级页项 '+navPages.size+' 个,与八页一致'); else bad('侧栏二级页项 '+navPages.size+' 个,不等于 8');
const dangling=[...declared].filter(k=>!seen.has(k));
if(dangling.length===0) ok('清单里每个 ID 都在页面上出现过('+declared.size+' 个,无悬空声明)'); else bad('声明了但页面里没有:'+dangling.join(', '));
if(extra.length===0) ok('页面上每个 data-act 都在清单里(共 '+domCount+' 次出现,无未登记的可点元素)'); else bad('未登记的 data-act:'+extra.join(', '));
/* 4) 逐个真点:每个场景里重置一次,再点,再看反馈 */
const res={};
SCENES.forEach(sc=>{
applyScene(sc);
const acts=[...new Set([...document.querySelectorAll('[data-act]')].map(n=>n.dataset.act))];
acts.forEach(act=>{
if(NO_CLICK.includes(act)) return;
applyScene(sc); // 重置到点击前,保证每个元素都从干净态开点
const fresh=[...document.querySelectorAll('[data-act]')].find(n=>n.dataset.act===act);
if(!fresh){ res[act]='dead'; return; }
if(fresh.hasAttribute('disabled')){ if(res[act]!=='ok') res[act]='disabled'; return; }
const routeBefore=S.route; S.toast=null;
const before=sig();
try{ fresh.dispatchEvent(new MouseEvent('click',{bubbles:true})); }
catch(err){ res[act]='dead'; return; }
const changed = sig()!==before;
const noopExpected = NOOP_OK.includes(act) || (act.startsWith('G-0')&&routeBefore===fresh.dataset.page);
if(changed||noopExpected){ res[act]='ok'; } else { res[act]='dead'; }
});
});
const okList=Object.keys(res).filter(k=>res[k]==='ok');
const disList=Object.keys(res).filter(k=>res[k]==='disabled');
const deadList=Object.keys(res).filter(k=>res[k]==='dead');
if(deadList.length===0) ok('可点元素逐个真点:'+okList.length+' 个有可见反馈,0 个死按钮('+SCENES.length+' 个场景各重置一次)'); else bad('点了没反应:'+deadList.join(' / '));
ok('当刻被禁用的控件 '+disList.length+' 个(有 disabled 属性,按契约在特定条件下不可点)'+(disList.length?':'+disList.join(', '):''));
const never=[...declared].filter(k=>!res[k]);
if(never.length===0) ok('清单里每个 ID 都至少被点到过一次(无「从未跑过」的条目)'); else bad('从未被点到:'+never.join(', '));
/* 5) 页型与动效纪律 */
const hits=['eyebrow','lead','hero'].filter(c=>document.querySelectorAll('.'+c).length);
if(hits.length===0) ok('工具型纪律:零 .eyebrow / 零 .lead / 零 .hero'); else bad('出现禁用类名:'+hits.join(','));
let kf=0;
try{ [...document.styleSheets].forEach(ss=>{ try{ [...ss.cssRules].forEach(r=>{ if(r.type===7) kf++; }); }catch(e){} }); }catch(e){}
if(kf===0) ok('零 @keyframes(工具型不做入场编排)'); else bad('出现 @keyframes '+kf+' 处');
/* 6) 结构取值(3d 回填用,DOM 实测不是目测) */
L.push('--- 结构取值(DOM 实测) ---');
applyScene({set:()=>{ S.route='p1'; S.pageState='ok'; S.layer=null; }});
const fsSet=new Set();
document.querySelectorAll('#view *').forEach(e=>{ if(e.children.length===0 && (e.textContent||'').trim()) fsSet.add(getComputedStyle(e).fontSize); });
L.push('P1 实际在用字号档:'+[...fsSet].sort((a,b)=>parseFloat(a)-parseFloat(b)).join(' / ')+'(共 '+fsSet.size+' 档)');
const bh=[...new Set([...document.querySelectorAll('#view button.btn')].map(b=>Math.round(b.getBoundingClientRect().height)).filter(h=>h>0))];
L.push('P1 按钮高度集合:'+bh.join(' / '));
const rowH=[...document.querySelectorAll('#view .list-row')].map(r=>Math.round(r.getBoundingClientRect().height)).filter(h=>h>0);
L.push('P1 账号矩阵一行高度:'+(rowH[1]||rowH[0]||0)+'px · 一屏 900px 可承载约 '+Math.floor((900-260)/(rowH[1]||rowH[0]||56))+' 行');
const blocks=[...document.querySelectorAll('#view .block')];
if(blocks.length>=2){
const gap=Math.round(blocks[1].getBoundingClientRect().top-blocks[0].getBoundingClientRect().bottom);
const rows=[...blocks[0].querySelectorAll('.list-row')];
const innerGap=rows.length>=2?Math.round(rows[1].getBoundingClientRect().top-rows[0].getBoundingClientRect().bottom):0;
L.push('P1 块间距 '+gap+'px / 块内行间距 '+innerGap+'px / 比值 '+(innerGap?(gap/innerGap).toFixed(2):'n/a'));
}
L.push('横向溢出:documentElement.scrollWidth '+document.documentElement.scrollWidth+' / innerWidth '+window.innerWidth);
L.push('控制台报错:'+(CONSOLE_ERRORS.length?CONSOLE_ERRORS.join(' | '):'0 条'));
/* 还原 + 自检期间锁住全部异步重绘(防自检结果被 420ms/1500ms 的定时重绘覆盖)。
离开方式:点侧栏导航或「回到正常态」—— 那两个入口会先把锁解开。 */
S.route='selftest';
ST.lock=true;
try{ history.replaceState(null,'','#/selftest'); }catch(e){}
const head='自检结果:PASS '+pass.length+' / FAIL '+fail.length;
return '<section class="block">'+
'<div class="block-head"><h2 class="grow">自检(#/selftest)</h2><span class="meta">'+esc(head)+'</span></div>'+
'<div class="st-out" id="selftest-out">'+esc(head)+'\n\n'+L.join('\n')+'</div>'+
'<div class="block" style="margin-top:var(--sp-24)"><div class="panel"><div class="row">'+
'<span class="meta">自检是原型自带的:它在真实场景里逐个点过每个可点元素,证明没有死按钮。</span>'+
'<button class="btn btn-sm" data-act="STATE-NORMAL">回到正常态</button>'+
'</div></div></div></section>';
}
/* ---------- 启动 ---------- */
function renderSelftest(){
try{ const h=_renderSelftest(); try{ document.title='自检已跑|'+h.length+' 字符'; }catch(e){} return h; }
catch(e){ return '<section class="block"><div class="block-head"><h2>自检没有跑完</h2></div><div class="st-out">'+
String((e&&e.stack)||e).replace(/</g,'&lt;')+'</div></section>'; }
}
if(!location.hash) location.hash='#/p/p1/ok';
parseHash(); paint();
document.title='DBG hash='+location.hash+' route='+S.route+' lock='+ST.lock+' viewKids='+$('#view').children.length
+' sheets='+document.styleSheets.length
+' rules='+(document.styleSheets[0]?document.styleSheets[0].cssRules.length:-1)
+' appDisplay='+getComputedStyle(document.querySelector('.app')).display
+' bodyBg='+getComputedStyle(document.body).backgroundColor+' | --t-paper=['+getComputedStyle(document.documentElement).getPropertyValue('--t-paper')+']'+' --paper=['+getComputedStyle(document.documentElement).getPropertyValue('--paper')+']'+' --fs-18=['+getComputedStyle(document.documentElement).getPropertyValue('--fs-18')+']'+' r0=['+(document.styleSheets[0]?document.styleSheets[0].cssRules[0].cssText.slice(0,220):'?')+']';
</script>
</body></html>