1487 lines
97 KiB
HTML
1487 lines
97 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|||
|
|
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,'<')+'</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>
|