Browse Source

初始设计

yuan 1 month ago
commit
e2cb242564
27 changed files with 3718 additions and 0 deletions
  1. BIN
      .DS_Store
  2. 1 0
      .gitignore
  3. 12 0
      CMS-UI完整设计.html
  4. 29 0
      src/README.md
  5. 641 0
      src/app.js
  6. 114 0
      src/book.css
  7. 257 0
      src/book.js
  8. 28 0
      src/build.py
  9. 443 0
      src/data.js
  10. 295 0
      src/data2.js
  11. BIN
      src/fonts/Inter-Medium.woff2
  12. BIN
      src/fonts/Inter-Regular.woff2
  13. BIN
      src/fonts/Inter-SemiBold.woff2
  14. 139 0
      src/frame.css
  15. 256 0
      src/kit.css
  16. 1 0
      src/node_modules
  17. 154 0
      src/overlay.css
  18. 28 0
      src/pack.py
  19. 279 0
      src/pages.css
  20. 355 0
      src/pages.js
  21. 42 0
      src/shot.mjs
  22. 56 0
      src/template.html
  23. 329 0
      src/test.mjs
  24. 14 0
      src/theme.css
  25. 120 0
      src/tokens.js
  26. 34 0
      src/使用说明.txt
  27. 91 0
      src/归档/tokens-深色候选AC.js

BIN
.DS_Store


+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+send

File diff suppressed because it is too large
+ 12 - 0
CMS-UI完整设计.html


+ 29 - 0
src/README.md

@@ -0,0 +1,29 @@
+# 源码说明
+
+`../CMS-UI完整设计.html` 由这里的文件生成,单文件、离线可用。基于 2026-10-08 第一批分项选型的评审结果(最终:基础视觉 F、框架 A、长表单弹窗 J、抽屉 E;短表单弹窗按发起人给的布局;登录页按 10-07 追加的分项选型登录页 C「右侧浮卡·全幅底图」,左侧有三端入口)。
+
+| 文件 | 内容 |
+| --- | --- |
+| `tokens.js` | 两套皮肤的设计令牌:浅色 F 晴空渐变(默认)+ 深色 B 石墨灰(2026-10-08 从三版候选里选定;未选的 A、C 在 `归档/tokens-深色候选AC.js`,不参与构建)。运行时每套生成为 `.cv[data-v=皮肤]` 上的 CSS 变量;`window.TOKENS` 是当前皮肤,`TOKENS.use(key)` 原地切换;`palette`/`strip` 给规范摘要的色板用。换肤只改颜色与质感,尺寸、圆角、字体不变 |
+| `data.js` | 三端菜单、字典、列表、订单详情、补录、新建投资者、TRS 下单(沿用分项选型,从代码提取) |
+| `data2.js` | 本稿补充:票据详情、清算、风险监控、首页、个人中心、登录、反馈类文案;`pages` 是设计稿目录(来源、设计要点、待确认) |
+| `kit.css` | 画布内组件样式(按钮、输入、标签、表格、卡片、提示、弹窗外观),全部读令牌 |
+| `frame.css` | 框架 A 的布局与收起态(取自分项选型 frames.css 的通用部分 + A) |
+| `overlay.css` | 弹窗骨架、表单项、弹窗 J 的卡片分区、抽屉骨架、描述表、数字卡片、抽屉 E |
+| `pages.css` | 新增页面:首页、页内页签、清算、TRS 下单、票据详情、确认框、个人中心、404/401、登录、反馈类 |
+| `theme.css` | 令牌之外的结构差异(渐变勾选框、渐变页签线等,读 `--grad`,两套共用)+ 深色专属的几条(`[data-dark]`、`[data-v="db"]`),最后加载 |
+| `fonts/` | Inter 400/500/600(构建时内嵌,F 的数字字体) |
+| `app.js` | 画布核心:图标、令牌注入、框架 A 外壳、列表页、弹窗 J(补录、新建投资者)、抽屉 E(订单详情)、画布挂载与交互;对外暴露 `window.UI` |
+| `pages.js` | 新增页面与弹层的渲染,登记到 `UI.pageRenderers` / `overlayRenderers` / `canvasRenderers` |
+| `book.js` / `book.css` / `template.html` | 设计稿外壳:目录、舞台、说明面板、规范摘要、全屏、路由 |
+
+命令(在 `src/` 下执行):
+
+- `python3 build.py`:生成 HTML。
+- `node test.mjs`:在生成的 HTML 里实际点击每一类交互并断言,失败退出码为 1。
+- `node shot.mjs [前缀]`:按原尺寸截图到 `../shots/`(可用 `SHOT_DIR`、`SHOT_DPR` 改)。
+- `python3 pack.py`:构建 → 测试 → 打包到 `../发给同事/`,测试不通过不出包。
+
+页面地址:`#p/<页面>[/<端>][/<变体>][/t=<皮肤>]`,例如 `#p/login/trs/reset`、`#p/err/issue/401`、`#p/home/issue/t=db`;皮肤 `f`/`db`,不带时沿用当前皮肤。单独模式 `#solo/<页面>[/<端>][/<变体>][/t=<皮肤>]` 只渲染一张原尺寸画布。截图:`SHOT_THEME=db node shot.mjs` 按指定皮肤截,文件名带皮肤前缀。
+
+`node_modules` 是指向本机已有 playwright 的软链接,只供测试和截图使用。

+ 641 - 0
src/app.js

@@ -0,0 +1,641 @@
+/* 三端 CMS · 完整 UI 设计稿 · 画布核心
+ * 画布(.cv)是 1440×900 的真实尺寸页面,用 CSS zoom 缩放显示,画面里的点击都是真实交互。
+ * 本文件:图标、令牌注入、框架 A 外壳、列表页、弹窗(新建投资者用 J 卡片分区;补录等短表单用 2026-10-08 给的布局)、抽屉 E(订单详情)、画布挂载与交互。
+ * 新增页面(登录、首页、页签页、清算、个人中心、404/401、TRS 下单、票据详情、确认框、反馈类)在 pages.js;设计稿外壳在 book.js。
+ */
+window.UI = (function () {
+  "use strict";
+  const D = window.SPEC_DATA;
+  const D2 = window.SPEC_DATA2;
+  const T = window.TOKENS;
+
+  const $ = (s, r = document) => r.querySelector(s);
+  const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
+  const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
+
+  /* ---------------- 图标 ---------------- */
+  const ICONS = {
+    home: '<path d="M3.5 10.5 12 3.5l8.5 7"/><path d="M5.5 9v11h13V9"/><path d="M10 20v-5.5h4V20"/>',
+    chart: '<path d="M4 4v16h16"/><path d="M8 16v-4"/><path d="M12 16V8"/><path d="M16 16v-6"/>',
+    shield: '<path d="M12 3 5 6v5.5c0 4.2 2.9 7.8 7 9.5 4.1-1.7 7-5.3 7-9.5V6Z"/><path d="m9 12 2 2 4-4"/>',
+    ticket: '<path d="M4 7.5A1.5 1.5 0 0 1 5.5 6h13A1.5 1.5 0 0 1 20 7.5V10a2 2 0 0 0 0 4v2.5a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 16.5V14a2 2 0 0 0 0-4Z"/><path d="M14.5 6v12" stroke-dasharray="2 2.2"/>',
+    coin: '<circle cx="12" cy="12" r="8.5"/><path d="m9 7.5 3 3.5 3-3.5M8.5 12h7M8.5 15h7M12 11v6"/>',
+    check: '<circle cx="12" cy="12" r="8.5"/><path d="m8.5 12 2.5 2.5 4.5-5"/>',
+    gear: '<circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="7.6" stroke-dasharray="3.1 2.87"/>',
+    cpu: '<rect x="6" y="6" width="12" height="12" rx="2"/><rect x="9.5" y="9.5" width="5" height="5" rx=".5"/><path d="M9.5 3v3M14.5 3v3M9.5 18v3M14.5 18v3M3 9.5h3M3 14.5h3M18 9.5h3M18 14.5h3"/>',
+    building: '<path d="M4.5 20V5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v15"/><path d="M14.5 9.5h4a1 1 0 0 1 1 1V20"/><path d="M3 20h18M8 8h3M8 11.5h3M8 15h3"/>',
+    clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>',
+    mail: '<rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="m4 7 8 6 8-6"/>',
+    user: '<circle cx="12" cy="8.5" r="3.8"/><path d="M4.5 20c.6-3.8 3.7-6.2 7.5-6.2s6.9 2.4 7.5 6.2"/>',
+    card: '<rect x="3.5" y="5.5" width="17" height="13" rx="2"/><path d="M3.5 10h17M7 15h4"/>',
+    scale: '<path d="M12 4.5v15M6 19.5h12M5 8h14"/><path d="m5 8-2.5 5.5h5ZM19 8l-2.5 5.5h5Z"/>',
+    sliders: '<path d="M4 7h9M17 7h3M4 12h3M11 12h9M4 17h11M19 17h1"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="17" cy="17" r="2"/>',
+    file: '<path d="M14 3.5H7a1.5 1.5 0 0 0-1.5 1.5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8Z"/><path d="M14 3.5V8h4.5M9 13h6M9 16.5h6"/>',
+    search: '<circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/>',
+    refresh: '<path d="M19.5 11A7.5 7.5 0 0 0 6 6.8L4.5 8.5"/><path d="M4.5 4.5v4h4"/><path d="M4.5 13A7.5 7.5 0 0 0 18 17.2l1.5-1.7"/><path d="M19.5 19.5v-4h-4"/>',
+    download: '<path d="M12 4v11"/><path d="m7.5 10.5 4.5 4.5 4.5-4.5"/><path d="M5 19.5h14"/>',
+    bell: '<path d="M6.5 16v-5a5.5 5.5 0 1 1 11 0v5l1.5 2H5Z"/><path d="M10 20.5a2 2 0 0 0 4 0"/>',
+    fold: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M9.5 4.5v15"/><path d="m16 10-2 2 2 2"/>',
+    unfold: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M9.5 4.5v15"/><path d="m14 10 2 2-2 2"/>',
+    expand: '<path d="M4.5 9V4.5H9M19.5 9V4.5H15M4.5 15v4.5H9M19.5 15v4.5H15"/>',
+    down: '<path d="m6.5 9.5 5.5 5.5 5.5-5.5"/>',
+    right: '<path d="m9.5 6.5 5.5 5.5-5.5 5.5"/>',
+    left: '<path d="m14.5 6.5-5.5 5.5 5.5 5.5"/>',
+    x: '<path d="M7 7l10 10M17 7 7 17"/>',
+    calendar: '<rect x="3.5" y="5" width="17" height="15" rx="2"/><path d="M3.5 10h17M8 3v4M16 3v4"/>',
+    logout: '<path d="M14.5 4.5h3a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-3"/><path d="M10 16.5 5.5 12 10 7.5M5.5 12h10"/>',
+    info: '<circle cx="12" cy="12" r="8.5"/><path d="M12 11v5.5M12 7.8h.01"/>',
+    warn: '<path d="M12 4 2.8 19.5h18.4Z"/><path d="M12 10v4.5M12 17h.01"/>',
+    error: '<circle cx="12" cy="12" r="8.5"/><path d="m9 9 6 6M15 9l-6 6"/>',
+    success: '<circle cx="12" cy="12" r="8.5"/><path d="m8.5 12 2.5 2.5 4.5-5"/>',
+    plus: '<path d="M12 5v14M5 12h14"/>',
+    edit: '<path d="M4.5 19.5h4l10-10-4-4-10 10Z"/><path d="m13 7 4 4"/>',
+    upload: '<path d="M12 20V9"/><path d="m7.5 13.5 4.5-4.5 4.5 4.5"/><path d="M5 4.5h14"/>',
+    layers: '<path d="m12 4 8.5 4.5L12 13 3.5 8.5Z"/><path d="m3.5 13 8.5 4.5 8.5-4.5"/>',
+    more: '<circle cx="5.5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="18.5" cy="12" r="1.2"/>',
+    lock: '<rect x="5.5" y="10.5" width="13" height="10" rx="2"/><path d="M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3"/>',
+    eye: '<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="3"/>',
+    eyeoff: '<path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><path d="m4 4 16 16"/>',
+    globe: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c3 3 3 14 0 17M12 3.5c-3 3-3 14 0 17"/>',
+    moon: '<path d="M19.5 14.5A8 8 0 0 1 9.5 4.5a8 8 0 1 0 10 10Z"/>',
+    sun: '<circle cx="12" cy="12" r="4"/><path d="M12 3v2.5M12 18.5V21M3 12h2.5M18.5 12H21M5.6 5.6l1.8 1.8M16.6 16.6l1.8 1.8M5.6 18.4l1.8-1.8M16.6 7.4l1.8-1.8"/>',
+    key: '<circle cx="8" cy="14" r="3.5"/><path d="m10.5 11.5 8-8M15 6l2.5 2.5M18 3.5 20.5 6"/>',
+    phone: '<rect x="7" y="3.5" width="10" height="17" rx="2"/><path d="M11 17.5h2"/>',
+    arrow: '<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>',
+    copy: '<rect x="8.5" y="8.5" width="11" height="11" rx="1.5"/><path d="M15.5 8.5v-2a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h2"/>',
+    stop: '<circle cx="12" cy="12" r="8.5"/><rect x="9" y="9" width="6" height="6" rx="1"/>',
+    play: '<circle cx="12" cy="12" r="8.5"/><path d="m10 8.5 5 3.5-5 3.5Z"/>',
+    camera: '<path d="M4.5 8.5h3l1.5-2h6l1.5 2h3v10h-15Z"/><circle cx="12" cy="13" r="3"/>',
+    male: '<circle cx="10" cy="14" r="5"/><path d="m13.5 10.5 6-6M14 4.5h5.5V10"/>',
+  };
+  const ic = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[n] || ""}</svg>`;
+  const CHECK = '<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6.2 5 8.5 9.5 3.5"/></svg>';
+  const EMPTY = '<svg viewBox="0 0 64 48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" style="color:var(--t4)"><path d="M8 28 16 12h32l8 16v12a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2Z"/><path d="M8 28h14l3 5h14l3-5h14"/></svg>';
+
+  /* ---------------- 设计令牌 → CSS 变量 ---------------- */
+  // 每套皮肤各生成一段 .cv[data-v=皮肤]{--令牌:值};画布按当前皮肤的 key 打 data-v,换肤只改属性和重绘
+  function injectTokens() {
+    const st = document.createElement("style");
+    const block = (sel, tokens) => `${sel}{${Object.entries(tokens).map(([n, val]) => `--${n}:${val}`).join(";")}}`;
+    st.textContent = T.order.map((k) => block(k === "f" ? `.cv,.cv[data-v="${k}"]` : `.cv[data-v="${k}"]`, T.all[k].tokens)).join("\n");
+    document.head.appendChild(st);
+  }
+  // 切换皮肤:改 window.TOKENS 的当前取值(持有 T 引用的模块都能看到),返回是否真的换了;重绘由调用方负责
+  const setTheme = (key) => T.use(key);
+  // 深色皮肤之间互切时记住最近一版,登录页的月亮按钮和用户菜单「界面设置」在浅色和这一版之间来回
+  const THEME_PICK = { dark: "db" };
+  function toggleTheme() {
+    if (T.dark) THEME_PICK.dark = T.key;
+    return setTheme(T.dark ? "f" : THEME_PICK.dark);
+  }
+  const dict = Object.assign({}, D.dict, D2.dict);
+  const tag = (dk, v) => `<span class="tg tg-${(dict[dk] || {})[v] || "info"}">${esc(v)}</span>`;
+
+  /* ---------------- 三端 ---------------- */
+  const listActions = [["查找", "primary", "search"], ["重置", "", "refresh"], ["数据导出", "", "download"]];
+  const ENDS = {
+    issue: { short: "发行", sys: D.sysName.issue, logo: "发", home: "共赢票据管理/订单管理", extra: "共赢票据管理/票据管理", search: D.issueSearch, actions: D.issueActions, columns: D.issueColumns, rows: D.issueRows.filter((r) => r.sub === 0), sel: true, op: true, src: "issue_frontend/src/views/issue/order" },
+    sales: { short: "销售", sys: D.sysName.sales, logo: "销", home: "共赢票据管理/订单管理", extra: "投资者管理/投资者信息", search: D.salesSearch, actions: listActions, columns: D.salesColumns, rows: D.salesRows.filter((r) => r.sub === 0), sel: false, op: true, src: "sales_frontend/src/views/sales/order" },
+    trs: { short: "TRS", sys: D.sysName.trs, logo: "T", home: "用户管理/委托查询", extra: "用户管理/成交查询", search: D.trsSearch, actions: D.trsActions, columns: D.trsColumns, rows: D.trsRows.filter((r) => !r.sub), sel: false, op: false, src: "trs_frontend/src/views/trs/order" },
+  };
+  const leafName = (key) => (key.includes("/") ? key.split("/")[1] : key);
+  const groupName = (key) => (key.includes("/") ? key.split("/")[0] : "");
+
+  // 画布状态:end 端,active 当前页键("首页" / "分组/菜单" / "个人中心" / "404" / "401"),ovls 叠开的弹层
+  function newCanvasState(end, active) {
+    const e = ENDS[end];
+    const act = !active || active === "home" ? e.home : active;
+    const tags = ["首页", e.home, e.extra];
+    if (!tags.includes(act)) tags.push(act);
+    return { end, col: false, open: new Set([groupName(act)].filter(Boolean)), active: act, tags, sel: new Set(), sfOpen: false, pop: null, ctx: null, msq: "", ovls: [], tab: {}, variant: null };
+  }
+
+  /* ---------------- 全局显示选项(设计稿外壳可切换) ---------------- */
+  const OPT = { lblTop: false }; // 短表单弹窗:标签在左(用户 2026-10-08 看过标签在上的效果后定为维持在左);true 时标签在上,单独模式 /lbl=top 可看
+
+  /* ---------------- 弹层状态(一份,和画布无关) ---------------- */
+  const OV = {
+    order: {},
+    fill: { hedgeSys: "自定义", pop: null },
+    investor: { checks: [false, false] },
+    trs: { channel: "QFII", dir: "买入", price: "11.65", qty: "1,000", algo: false },
+    note: { tab: "main" },
+    confirm: { kind: "cancel" },
+    pwd: {},
+  };
+  function resetOV() {
+    Object.assign(OV.fill, { hedgeSys: "自定义", pop: null });
+    OV.investor.checks = [false, false];
+    Object.assign(OV.trs, { channel: "QFII", dir: "买入", price: "11.65", qty: "1,000", algo: false });
+    OV.note.tab = "main";
+    OV.confirm.kind = "cancel";
+  }
+
+  /* ---------------- 通用小件 ---------------- */
+  function pagerHTML(total) {
+    return `<div class="pager"><span class="total">共 <span class="num">${total}</span> 条</span>
+      <div class="ipt" style="width:108px"><span class="val">20 条/页</span>${ic("down")}</div>
+      <span class="pg-b dis">${ic("left")}</span><span class="pg-b on num">1</span><span class="pg-b dis">${ic("right")}</span>
+      <span>前往</span><div class="ipt" style="width:52px;justify-content:center"><span class="num">1</span></div><span>页</span></div>`;
+  }
+  function ctlHTML(f) {
+    if (f.type === "daterange") return `<div class="ipt">${ic("calendar")}<span class="val num">${f.value[0]}</span><span class="sep">${f.sep}</span><span class="val num">${f.value[1]}</span></div>`;
+    if (f.type === "select") {
+      const vals = Array.isArray(f.value) ? f.value : [];
+      return `<div class="ipt">${vals.length ? `<span class="val chips">${vals.map((v) => `<span class="chipv">${esc(v)}</span>`).join("")}</span>` : `<span class="ph">${esc(f.ph)}</span>`}${ic("down")}</div>`;
+    }
+    return `<div class="ipt"><span class="ph">${esc(f.ph)}</span></div>`;
+  }
+  function cellHTML(c, r) {
+    const v = r[c.key];
+    if (v === undefined || v === null || v === "" || v === "--") return '<span class="t4">--</span>';
+    if (c.tag) return tag(c.tag, v);
+    if (c.link) return `<span class="lnk num">${esc(v)}</span>`;
+    if (c.num || c.mono) return `<span class="num">${esc(v)}</span>`;
+    return esc(v);
+  }
+  const NUMLIKE = /^[+\-]?[\d,.]+(%|( \/ [+\-]?[\d,.]+%?)*)?( [A-Z]{3}| 股)?$|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$|^[A-Z]{1,3}\d{6,}[A-Z]?(-\d)?$/;
+  // 通用单元格:字符串、["文字", 字典名] 标签、["文字", "link"] 链接、["文字", "up"|"dn"] 红绿
+  function cellAuto(v, col) {
+    if (v === undefined || v === null || v === "" || v === "--") return '<span class="t4">--</span>';
+    if (Array.isArray(v)) {
+      const [t, kind] = v;
+      if (kind === "link") return `<span class="lnk num" data-act="noop">${esc(t)}</span>`;
+      if (kind === "up" || kind === "dn") return `<span class="num ${kind === "up" ? "buy" : "sell"}">${esc(t)}</span>`;
+      if (dict[kind]) return tag(kind, t);
+      return `<span class="tg tg-${kind}">${esc(t)}</span>`;
+    }
+    const s = String(v);
+    if ((col && col[2] === "r") || NUMLIKE.test(s)) return `<span class="num">${esc(s)}</span>`;
+    return esc(s);
+  }
+  // 通用表格:cols = [[标题, 宽, 对齐]],rows = 单元格数组;opts.ops(row) 返回操作列内容
+  function tableHTML(cols, rows, opts = {}) {
+    const head = cols.map((c) => `<th class="${c[2] === "r" ? "r" : c[2] === "c" ? "c" : ""}" style="min-width:${c[1]}px">${esc(c[0])}</th>`).join("");
+    const body = rows.length
+      ? rows.map((r, ri) => `<tr class="${opts.rowCls ? opts.rowCls(r, ri) : ""}">${cols.map((c, i) => {
+          const al = c[2] === "r" ? "r" : c[2] === "c" ? "c" : "";
+          const v = opts.cell ? opts.cell(r, i, c) : r[i];
+          return `<td class="${al} ${c[3] === "wrap" ? "wrap" : ""}">${v === undefined ? "" : typeof v === "string" && v.startsWith("<") ? v : cellAuto(v, c)}</td>`;
+        }).join("")}</tr>`).join("")
+      : `<tr><td colspan="${cols.length}" style="padding:0;border-bottom:0"><div class="empty" style="height:${opts.emptyH || 200}px;justify-content:center">${EMPTY}<span>${esc(opts.empty || "暂无数据")}</span>${opts.emptyAct ? `<span class="btn txt" data-act="noop">${esc(opts.emptyAct)}</span>` : ""}</div></td></tr>`;
+    return `<div class="tbl-wrap ${opts.cls || ""}" ${opts.keep ? `data-keep="${opts.keep}"` : ""} ${opts.style ? `style="${opts.style}"` : ""}><table class="tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
+  }
+  const btn = (text, type = "", act = "noop", extra = "") => `<span class="btn ${type}" data-act="${act}" ${extra}>${text}</span>`;
+
+  /* ---------------- 列表页(搜索区 + 表格) ---------------- */
+  function listHTML(st) {
+    const e = ENDS[st.end];
+    const lw = Math.max(...e.search.map((f) => f.label.length)) * 14;
+    const collapsible = e.search.length > 5;
+    const fields = st.sfOpen || !collapsible ? e.search : e.search.slice(0, 5);
+    const rem = 3 - (fields.length % 3);
+    const acts = `<div class="sf-act" style="grid-column:span ${rem}">${e.actions.map(([t, type, icn]) => `<span class="btn ${type === "primary" ? "pri" : ""}" data-act="noop">${ic(icn)}${t}</span>`).join("")}${collapsible ? `<span class="btn txt ${st.sfOpen ? "up" : ""}" data-act="sf">${st.sfOpen ? "收起" : "展开"}${ic("down")}</span>` : ""}</div>`;
+    const search = `<div class="card"><div class="sf">${fields.map((f) => `<div class="sf-i"><label style="width:${lw}px">${esc(f.label)}</label>${ctlHTML(f)}</div>`).join("")}${acts}</div></div>`;
+
+    const cols = [];
+    if (e.sel) cols.push({ sel: true, w: 48, fx: true });
+    if (e.op) cols.push({ op: true, title: "操作", w: 72, fx: true });
+    e.columns.forEach((c) => cols.push(c));
+    let left = 0;
+    const fx = cols.filter((c) => c.fx);
+    cols.forEach((c) => { if (c.fx) { c._left = left; left += c.w; } });
+    const lastFx = fx[fx.length - 1];
+    const allOn = e.rows.length && e.rows.every((r) => st.sel.has(r.no));
+    const someOn = e.rows.some((r) => st.sel.has(r.no));
+    const thCls = (c) => [c.fx ? "fx" : "", c === lastFx ? "fx-end" : "", c.num || c.align === "right" ? "r" : ""].join(" ");
+    const pos = (c) => (c.fx ? `left:${c._left}px;` : "");
+    const head = cols.map((c) => {
+      const style = `${pos(c)}min-width:${c.w}px;width:${c.w}px`;
+      if (c.sel) return `<th class="${thCls(c)}" style="${style}"><span class="ck ${allOn ? "on" : someOn ? "half" : ""}" data-act="allsel">${allOn ? CHECK : ""}</span></th>`;
+      return `<th class="${thCls(c)}" style="${style}">${esc(c.title)}</th>`;
+    }).join("");
+    const body = e.rows.map((r) => {
+      const on = st.sel.has(r.no);
+      return `<tr class="${on ? "is-sel" : ""}">${cols.map((c) => {
+        const cls = thCls(c) + (c.key === "org" || c.key === "remark" || c.key === "stock" ? " ellip" : "");
+        if (c.sel) return `<td class="${cls}" style="${pos(c)}"><span class="ck ${on ? "on" : ""}" data-act="rowsel" data-no="${esc(r.no)}">${CHECK}</span></td>`;
+        if (c.op) return `<td class="${cls}" style="${pos(c)}"><span class="btn txt" data-act="${st.end === "issue" ? "ovopen" : "toast"}" data-o="order" data-msg="销售端订单详情用同一个抽屉,只是没有「对冲执行」卡片">详情</span></td>`;
+        return `<td class="${cls}" style="${pos(c)}">${cellHTML(c, r)}</td>`;
+      }).join("")}</tr>`;
+    }).join("");
+    const selbar = st.sel.size ? `<div class="selbar">已选择 <b>${st.sel.size}</b> 项<span class="btn txt" data-act="clearsel">清空</span></div>` : "";
+    return `<div class="pg-list">${search}<div class="card tb-card">${selbar}<div class="tbl-wrap" data-keep="tbl"><table class="tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>${pagerHTML(e.rows.length)}</div></div>`;
+  }
+  function skelHTML(text) {
+    const bars = (n) => Array.from({ length: n }, (_, i) => `<div class="bar" style="width:${[92, 86, 95, 78, 90, 84, 97, 81, 88, 93][i % 10]}%"></div>`).join("");
+    return `<div class="skel"><div class="card"><div class="rowb">${Array.from({ length: 6 }, () => '<div class="bar"></div>').join("")}</div></div>
+      <div class="card" style="flex:1;display:flex;flex-direction:column;gap:18px"><div class="bar" style="height:20px;width:100%"></div><div class="lines">${bars(12)}</div></div>
+      <div class="skel-note">${ic("info")}<span>${esc(text)}</span></div></div>`;
+  }
+  // 页面分发:pages.js 往 pageRenderers 里登记
+  const pageRenderers = {};
+  function pageHTML(st) {
+    const e = ENDS[st.end];
+    const key = st.active === e.home ? "__list" : st.active;
+    const fn = pageRenderers[key] || (st.active === "首页" ? pageRenderers.__home : null);
+    if (fn) return fn(st);
+    return skelHTML(`「${leafName(st.active)}」没有单独画样板,按「列表页」规范实现`);
+  }
+  pageRenderers.__list = listHTML;
+
+  /* ---------------- 外壳部件 ---------------- */
+  function brandHTML(st, extra = "") {
+    const e = ENDS[st.end];
+    return `<div class="brand"><span class="logo">${e.logo}</span><span class="nm txt">${esc(e.sys)}</span>${extra}</div>`;
+  }
+  function menuHTML(st) {
+    const groups = D.menus[st.end];
+    const g = ([name, icon, leaves]) => {
+      const open = st.open.has(name);
+      const hasAct = groupName(st.active) === name;
+      return `<div class="mg ${open ? "is-open" : ""} ${hasAct ? "has-act" : ""}"><div class="mg-t" data-act="grp" data-g="${esc(name)}">${ic(icon)}<span class="txt">${esc(name)}</span>${ic("down", "arr")}</div>
+        <div class="mg-c"><span class="fly-t">${esc(name)}</span>${leaves.map((l) => `<div class="mi ${st.active === name + "/" + l ? "is-act" : ""}" data-act="leaf" data-k="${esc(name + "/" + l)}">${esc(l)}</div>`).join("")}</div></div>`;
+    };
+    return `<div class="menu" data-keep="menu"><div class="mi-top ${st.active === "首页" ? "is-act" : ""}" data-act="leaf" data-k="首页">${ic("home")}<span class="txt">首页</span></div>${groups.map(g).join("")}</div>`;
+  }
+  const tdHTML = () => `<div class="td">${D.tradeDays.map(([k, v]) => `<span>${k}<b>${v}</b></span>`).join("")}<span data-act="tdrf" title="刷新">${ic("refresh", "rf")}</span></div>`;
+  const crumbHTML = (st) => {
+    if (st.active === "首页") return `<div class="crumb"><b>首页</b></div>`;
+    const g = groupName(st.active);
+    return `<div class="crumb"><span>首页</span>${g ? `${ic("right")}<span>${esc(g)}</span>` : ""}${ic("right")}<b>${esc(leafName(st.active))}</b></div>`;
+  };
+
+  function msResults(st) {
+    const q = st.msq.trim();
+    const all = [];
+    D.menus[st.end].forEach(([g, , leaves]) => leaves.forEach((l) => all.push([g, l])));
+    const hits = q ? all.filter(([g, l]) => l.includes(q) || g.includes(q)) : all.slice(0, 6);
+    if (!hits.length) return `<div class="ms-empty">没有匹配的菜单</div>`;
+    const hl = (s) => (q ? esc(s).split(esc(q)).join(`<mark>${esc(q)}</mark>`) : esc(s));
+    return (q ? "" : `<div class="pop-t">全部菜单</div>`) + hits.slice(0, 8).map(([g, l]) => `<div class="pop-i" data-act="leaf" data-k="${esc(g + "/" + l)}"><span>${hl(l)}</span><span class="grp">${esc(g)}</span></div>`).join("");
+  }
+  const msPopHTML = (st, pos) => `<div class="fl ${pos}"><div class="pop ms-pop"><label class="ipt">${ic("search")}<input data-ms placeholder="输入菜单名称" value="${esc(st.msq)}" autocomplete="off"></label><div class="ms-list">${msResults(st)}</div></div></div>`;
+  const NOTICES = [["清算完成", "2026-09-25 交易日清算已完成,营业日已推进到 2026-09-28。", "17:41"], ["追保通知发送失败", "票据 NT001202608134A 的预警邮件发送失败,请在「邮件发送记录」里重发。", "14:03"]];
+  function bellHTML(st, pos = "below-r") {
+    const pop = `<div class="fl ${pos}"><div class="pop bell-pop"><div class="pop-hd">通知<span class="cnt">${NOTICES.length} 条未读</span><span class="btn txt" data-act="noop">全部已读</span></div>
+      ${NOTICES.map(([t, b, tm]) => `<div class="bell-i" data-act="noop"><div class="r"><span>${esc(t)}</span><small>${tm}</small></div><p>${esc(b)}</p></div>`).join("")}
+      <div class="bell-ft"><span class="btn txt" data-act="leaf" data-k="通知和告警管理/通知设置">查看更多${ic("right")}</span></div></div></div>`;
+    return `<div class="anchor"><span class="icb" data-act="pop" data-pop="bell" title="通知">${ic("bell")}<i class="dot"></i></span>${st.pop === "bell" ? pop : ""}</div>`;
+  }
+  const userMenu = `<div class="pop"><div class="pop-i" data-act="leaf" data-k="个人中心">${ic("user")}个人中心</div><div class="pop-i" data-act="theme" title="示例:切换浅色/深色皮肤">${ic("moon")}界面设置 · 切换皮肤</div><div class="pop-sep"></div><div class="pop-i" data-act="ovopen" data-o="confirm" data-kind="logout">${ic("logout")}退出登录</div></div>`;
+  function userHTML(st, pos = "below-r") {
+    return `<div class="anchor"><div class="user" data-act="pop" data-pop="user"><span class="ava">A</span><span class="txt">admin</span>${ic("down")}</div>${st.pop === "user" ? `<div class="fl ${pos}">${userMenu}</div>` : ""}</div>`;
+  }
+  const msIconHTML = (st) => `<div class="anchor"><span class="icb" data-act="pop" data-pop="ms" title="菜单搜索">${ic("search")}</span>${st.pop === "ms" ? msPopHTML(st, "below-r") : ""}</div>`;
+  const fullIcon = `<span class="icb" data-act="toast" data-msg="全屏(浏览器 F11 效果)" title="全屏">${ic("expand")}</span>`;
+  function tagsHTML(st) {
+    const items = st.tags.map((k, i) => {
+      const act = k === st.active;
+      return `<div class="tvi ${act ? "is-act" : ""}" data-act="tag" data-k="${esc(k)}" data-i="${i}">${k === "首页" ? ic("home", "hm") : ""}<span>${esc(leafName(k))}</span>${k === "首页" ? "" : `<span data-act="tagx" data-k="${esc(k)}" title="关闭">${ic("x", "x")}</span>`}</div>`;
+    }).join("");
+    let ctx = "";
+    if (st.ctx) {
+      const home = st.ctx.k === "首页";
+      ctx = `<div class="ctx" style="left:${st.ctx.left}px"><div class="pop"><div class="pop-i" data-act="ctx" data-do="refresh">${ic("refresh")}刷新</div>${home ? "" : `<div class="pop-i" data-act="ctx" data-do="close">${ic("x")}关闭</div>`}<div class="pop-i" data-act="ctx" data-do="others">关闭其它</div><div class="pop-i" data-act="ctx" data-do="all">关闭所有</div></div></div>`;
+    }
+    return `<div class="tv">${items}${ctx}</div>`;
+  }
+
+  /* ---------------- 框架 A:经典三段 ---------------- */
+  const overlayRenderers = {};
+  const OVL_KIND = { order: "dr", note: "dr", fill: "dg", investor: "dg", trs: "dg", confirm: "dg", pwd: "dg" };
+  function overlaysHTML(st) {
+    return st.ovls.map((o, i) => {
+      const fn = overlayRenderers[o];
+      if (!fn) return "";
+      return `<div class="ovl ${OVL_KIND[o] === "dr" ? "dr" : ""} ${i === 1 ? "lv2" : i >= 2 ? "lv3" : ""}" data-act="stop" data-ovl="${o}">${fn(OV[o], st)}</div>`;
+    }).join("");
+  }
+  function shellHTML(st) {
+    const col = st.col ? "is-col" : "";
+    const page = `<main class="pa" data-keep="pa">${pageHTML(st)}</main>`;
+    return `<div class="sh fa ${col}"><aside class="sd">${brandHTML(st)}${menuHTML(st)}</aside>
+      <div class="mn"><header class="nb"><span class="icb" data-act="col" title="${st.col ? "展开侧栏" : "收起侧栏"}">${ic(st.col ? "unfold" : "fold")}</span>${crumbHTML(st)}<span class="sp"></span>${tdHTML()}${msIconHTML(st)}${fullIcon}${bellHTML(st)}${userHTML(st)}</header>
+      ${tagsHTML(st)}${page}</div></div>${overlaysHTML(st)}`;
+  }
+
+  /* ================= 表单与弹窗 J ================= */
+  const FILL_CTX = { no: "ORD001202609250030", dir: "买入", stock: "300750 宁德时代" };
+  const numUnit = (v) => { const m = String(v).match(/^(.*?)\s([A-Z]{3}|股)$/); return m ? `${esc(m[1])}<i>${m[2]}</i>` : esc(v); };
+  const lblW = (fields) => Math.max(...fields.map((f) => [...f.label].reduce((n, ch) => n + (ch.charCodeAt(0) > 255 ? 14 : 8), 0))) + 26;
+  function fillFields(ov) {
+    const common = D.fill.common.map((f) => (f.label === "对冲系统" ? { ...f, value: ov.hedgeSys, act: "hsys" } : f));
+    const REQ = new Set(["交易日", "对冲账号", "资金号", "委托编号/算法母单编号", "成交股数", "成交均价", "发生金额(结算币种)"]);
+    return [...common, ...(ov.hedgeSys === "自定义" ? D.fill.custom : D.fill.trs)].map((f) => (REQ.has(f.label) ? { ...f, req: true } : f));
+  }
+  function ctl(f, ov) {
+    const err = f.error ? " is-err" : "";
+    const unit = f.unit ? `<span class="sfx">${f.unit}</span>` : "";
+    const val = (v, ph, num) => (v ? `<span class="val ${num ? "num" : ""}">${esc(v)}</span>` : `<span class="ph">${esc(ph || "")}</span>`);
+    switch (f.type) {
+      case "select": {
+        const pop = f.act === "hsys" && ov.pop === "hsys" ? `<div class="fl"><div class="pop">${["自定义", "TRS"].map((o) => `<div class="pop-i ${o === f.value ? "on" : ""}" data-act="hsysset" data-v="${o}">${o}</div>`).join("")}</div></div>` : "";
+        return `<div class="ipt${err}${pop ? " is-focus" : ""}" ${f.act ? `data-act="${f.act}" style="cursor:pointer"` : ""}>${val(f.value, f.ph || "请选择")}${ic("down")}</div>${pop}`;
+      }
+      case "date": return `<div class="ipt${err}">${val(f.value, f.ph)}${ic("calendar")}</div>`;
+      case "amount": return `<div class="ipt${err}">${val(f.value, f.ph, true)}${unit}</div>`;
+      case "password": return `<div class="ipt${err}">${ic("lock")}${val(f.value ? "••••••••" : "", f.ph)}${ic("eyeoff")}</div>`;
+      case "text": return `<div class="ro"><span class="num">${esc(f.value)}</span>${f.unit ? `<span class="u">${f.unit}</span>` : ""}</div>`;
+      case "tag": return `<div class="ro">${tag(f.tag, f.value)}</div>`;
+      case "combo": return `<div class="combo">${f.parts.map((p) => `<div class="ipt" style="${p.w ? `flex:none;width:${p.w}px` : "flex:1"}">${val(p.value, p.ph)}${p.select ? ic("down") : ""}</div>`).join("")}</div>`;
+      case "checks": return `<div class="checks">${f.options.map(([l], i) => `<span class="chk-l" data-act="chk" data-i="${i}"><span class="ck ${ov.checks[i] ? "on" : ""}">${CHECK}</span>${l}</span>`).join("")}</div>`;
+      default: return `<div class="ipt${err}">${val(f.value, f.ph)}</div>`;
+    }
+  }
+  const errHTML = (f) => (f.error ? `<div class="fld-err">${ic("error")}${esc(f.error)}</div>` : "");
+  const lbl = (f) => `${f.req ? '<i class="req">*</i>' : ""}${esc(f.label)}`;
+  // 一个表单项:pos = l(标签在左右对齐)/ t(在上)
+  function item(f, ov, pos, lw, cls = "") {
+    const body = `${ctl(f, ov)}${errHTML(f)}`;
+    if (pos === "t") return `<div class="fi fi-t ${cls}"><label>${lbl(f)}</label><div class="fc">${body}</div></div>`;
+    return `<div class="fi fi-l ${cls}"><label style="width:${lw}px">${lbl(f)}</label><div class="fc">${body}</div></div>`;
+  }
+  const isFilled = (f, ov) => (f.type === "combo" ? f.parts.every((p) => p.value) : f.type === "checks" ? ov.checks.some(Boolean) : !!f.value);
+  function gStat(g, ov) {
+    const fs = g.fields || [];
+    const req = fs.filter((f) => f.req);
+    return { req: req.length, ok: req.filter((f) => isFilled(f, ov) && !f.error).length, err: fs.filter((f) => f.error).length };
+  }
+  const gst = (s) => (s.err ? `<span class="gst bad">${s.err} 项有误</span>` : s.req ? `<span class="gst ${s.ok === s.req ? "ok" : ""}">${s.ok}/${s.req}</span>` : `<span class="gst">选填</span>`);
+  const uploadHTML = (g) => (u) => `<div class="upl"><div class="upl-r"><span class="btn" data-act="noop">${ic("upload")}${g.uploadBtn}</span><span class="upl-tip">${g.uploadTip}</span></div>${u.files.map(([n, s]) => `<div class="upl-f">${ic("file")}<span>${esc(n)}</span><span class="sz">${s}</span>${ic("x", "x")}</div>`).join("")}</div>`;
+  function groupBody(g, ov, pos, lw, cols) {
+    let h = "";
+    if (g.fields) {
+      h += `<div class="fgrid ${cols === 2 && g.cols !== 1 ? "c2" : ""}">${g.fields.map((f) => item(f, ov, pos, lw, f.type === "checks" ? "full" : "")).join("")}</div>`;
+      if (g.alert && !ov.checks[0]) h += `<div class="fi fi-t" style="margin-top:8px"><div class="fc"><div class="alt alt-warning" style="margin:0">${ic("warn")}<span>${esc(g.alert)}</span></div></div></div>`;
+    }
+    if (g.uploads) h += `<div class="fgrid">${g.uploads.map((u) => `<div class="fi fi-t"><label>${u.label}</label><div class="fc">${uploadHTML(g)(u)}</div></div>`).join("")}</div>`;
+    return h;
+  }
+  const closeX = `<span class="icb" data-act="ovclose" title="关闭">${ic("x")}</span>`;
+  const btnCancel = `<span class="btn" data-act="ovclose">取消</span>`;
+  const dgCard = (t, extra, body) => `<section class="dgc"><div class="dgc-hd"><b>${t}</b><span class="sp"></span>${extra}</div><div class="dgc-bd">${body}</div></section>`;
+  const dgWrap = (o) => `<div class="dg dg-j ${o.cls || ""}" style="width:${o.w}px" data-act="stop"><div class="dg-hd">${o.hd}</div>${o.mid || ""}<div class="dg-bd" data-keep="${o.keep || "dgbd"}">${o.bd}</div>${o.ft ? `<div class="dg-ft">${o.ft}</div>` : ""}</div>`;
+
+  function kvVal(f) {
+    const [, v, kind = ""] = f;
+    if (v === "--" || v === "") return '<span class="t4">--</span>';
+    if (kind.startsWith("tag:")) return tag(kind.slice(4), v);
+    if (kind === "num:up") return `<span class="num buy">${esc(v)}</span>`;
+    if (kind === "num:dn") return `<span class="num sell">${esc(v)}</span>`;
+    if (kind === "mono" || kind === "num") return `<span class="num">${esc(v)}</span>`;
+    return esc(v);
+  }
+  function kv(fields, cls, perRow = 0) {
+    let cells = fields.map((f) => `<div class="${f[2] === "span2" ? "s2" : ""}"><dt>${f[0]}</dt><dd>${kvVal(f)}</dd></div>`);
+    if (perRow) { const used = fields.reduce((n, f) => n + (f[2] === "span2" ? 2 : 1), 0); for (let i = used; i % perRow; i++) cells.push("<div><dt></dt><dd></dd></div>"); }
+    return `<dl class="kvg ${cls}">${cells.join("")}</dl>`;
+  }
+
+  // 加减数字框(短表单里的股数、价格)
+  const stepper = (v, unit, err = false) => `<div class="stp ${err ? "is-err" : ""}"><span class="stp-b" data-act="noop">−</span><span class="val num">${esc(v)}</span>${unit ? `<span class="sfx">${unit}</span>` : ""}<span class="stp-b" data-act="noop">+</span></div>`;
+  // 短表单弹窗(2026-10-08 用户给的下单弹窗布局):标题下一行写上下文;顶部高亮信息块;标签在左、单列;底部灰色结果块;可带右侧栏
+  function sfDialog(o) {
+    const tiles = o.tiles && o.tiles.length ? `<div class="sf-tiles">${o.tiles.map(([l, v, u]) => `<div class="sf-tile"><span class="l">${l}</span><b class="${/^[\d,.]/.test(String(v)) ? "num" : ""}">${v}${u ? `<i>${u}</i>` : ""}</b></div>`).join("")}</div>` : "";
+    const rows = o.rows.map((r) => `<div class="sf-row"><label>${r.req ? '<i class="req">*</i>' : ""}${esc(r.label)}</label><div class="sf-c">${r.body}${r.extra || ""}</div></div>`).join("");
+    const res = o.results && o.results.length ? `<div class="sf-row"><label></label><div class="sf-c"><div class="sf-res">${o.results.map(([l, v, u, note]) => `<div class="sf-rt"><span class="l">${l}</span><b class="num">${v}${u ? `<i>${u}</i>` : ""}</b>${note ? `<small>${note}</small>` : ""}</div>`).join("")}</div></div></div>` : "";
+    return `<div class="dg dg-s ${OPT.lblTop ? "lt" : ""} ${o.cls || ""}" style="width:${o.w}px" data-act="stop"><div class="dg-hd"><div class="row"><span class="ttl">${o.title}</span>${o.sub ? `<span class="sub">${o.sub}</span>` : ""}</div>${closeX}</div>
+      <div class="dg-bd" data-keep="${o.keep || "dgbd"}"><div class="sf-main" style="--lw:${o.lw || 96}px">${tiles}<div class="sf-rows">${rows}</div>${res}</div>${o.aside || ""}</div><div class="dg-ft">${o.ft}</div></div>`;
+  }
+  // 补录(短表单)
+  overlayRenderers.fill = function (ov) {
+    const fields = fillFields(ov);
+    const calc = fields.filter((f) => f.type === "text");
+    const rows = fields.filter((f) => f.type !== "text").map((f) => {
+      if (f.act === "hsys") return { label: f.label, body: `<div class="segc">${["自定义", "TRS"].map((o) => `<span class="${o === ov.hedgeSys ? "on" : ""}" data-act="hsysset" data-v="${o}">${o}</span>`).join("")}</div>` };
+      if (f.type === "amount" && f.value) return { label: f.label, req: f.req, body: stepper(f.value, f.unit) };
+      return { label: f.label, req: f.req, body: ctl(f, ov), extra: errHTML(f) };
+    });
+    const results = calc.map((f, i) => [f.label, f.value, f.unit || "", i === 0 ? "= 成交股数 × 成交均价" : ""]);
+    return sfDialog({ w: 640, lw: lblW(fields) - 12, title: D.fill.title, sub: `主订单 <span class="num">${FILL_CTX.no}</span> · ${FILL_CTX.dir}`,
+      tiles: [["主订单编号", FILL_CTX.no], ["证券代码/名称", FILL_CTX.stock]], rows, results,
+      ft: `<span class="hint">${ic("info")}补录后可在对冲记录里修改或删除</span><span class="sp"></span>${btnCancel}<span class="btn pri" data-act="ovtoast" data-msg="示例:这里只看排布,不真正提交">确定</span>` });
+  };
+  // 新建投资者(长表单)
+  overlayRenderers.investor = function (ov) {
+    const F = D.salesForm;
+    const stats = F.groups.map((g) => gStat(g, ov));
+    const lack = stats.reduce((n, s) => n + s.req - s.ok, 0);
+    const errs = stats.reduce((n, s) => n + s.err, 0);
+    const hint = `<span class="hint ${lack || errs ? "warn" : ""}">${lack || errs ? ic("warn") : ic("success")}${lack ? `还有 <b>${lack}</b> 项必填未填` : "必填项已填完"}${errs ? `,<b>${errs}</b> 项有误` : ""}</span>`;
+    return dgWrap({ w: 920, hd: `<div class="row"><span class="ttl">${F.title}</span><span class="sub">${F.mode}</span></div>${closeX}`,
+      bd: F.groups.map((g, i) => dgCard(g.title, gst(stats[i]), groupBody(g, ov, "t", 0, 2))).join(""),
+      ft: `${hint}<span class="sp"></span>${btnCancel}<span class="btn pri" data-act="ovtoast" data-msg="示例:这里只看排布,不真正提交">保存</span>` });
+  };
+
+  /* ================= 抽屉 E:订单详情 ================= */
+  function hedgeTable() {
+    const h = D.issueDetail.hedge;
+    const keys = ["ops", "type", "sys", "acct", "fund", "otype", "no", "status", "dir", "qty", "price", "fqty", "fprice", "amt", "samt"];
+    const head = h.columns.map(([t, w, al]) => `<th class="${al === "r" ? "r" : ""}" style="min-width:${w}px">${t}</th>`).join("");
+    const body = h.rows.map((r) => `<tr>${keys.map((key, i) => {
+      const al = h.columns[i][2] === "r" ? "r" : "";
+      const v = r[key];
+      let c;
+      if (key === "ops") c = v.length ? `<span class="btns" style="gap:12px">${v.map(([t, ty]) => `<span class="btn txt ${ty === "danger" ? "dng" : ""}" data-act="${t === "撤单" ? "ovopen" : t === "删除" ? "ovopen" : "noop"}" data-o="confirm" data-kind="${t === "撤单" ? "cancel" : "remove"}">${t}</span>`).join("")}</span>` : '<span class="t4">--</span>';
+      else if (key === "status") c = `<span class="tg tg-${v[1]}">${v[0]}</span>`;
+      else if (key === "dir") c = tag("direction", v);
+      else if (key === "sys") c = tag("hedge_system", v);
+      else if (v === "--") c = '<span class="t4">--</span>';
+      else c = al || key === "no" || key === "fund" ? `<span class="num">${esc(v)}</span>` : esc(v);
+      return `<td class="${al}">${c}</td>`;
+    }).join("")}</tr>`).join("");
+    return `<div class="tbl-wrap" data-keep="hedge"><table class="tbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
+  }
+  function tiles(order, bar) {
+    const s = Object.fromEntries(D.issueDetail.hedge.summary);
+    return `<div class="tiles">${order.map((l) => `<div class="tile"><div class="l">${l}</div><div class="v">${numUnit(s[l])}</div>${bar && l === "对冲比例" ? `<div class="bar"><i style="width:${parseFloat(s[l])}%"></i></div>` : ""}</div>`).join("")}</div>`;
+  }
+  const drCard = (title, extra, body, cls = "") => `<div class="card ${cls}"><div class="sec-hd"><b>${title}</b><span class="sp"></span>${extra}</div>${body}</div>`;
+  overlayRenderers.order = function () {
+    const X = D.issueDetail;
+    const m = Object.fromEntries(X.master.fields.map((f) => [f[0], f[1]]));
+    const a = Object.fromEntries(X.subA.fields.map((f) => [f[0], f[1]]));
+    const tags = `<span class="tags">${tag("direction", m["订单方向"])}${tag("note_order_status", m["状态"])}${tag("process_mode", m["处理方式"])}${tag("hedge_progress", m["对冲进度"])}</span>`;
+    const rf = `<span class="btn txt" data-act="ovrf" style="color:var(--t2)">${ic("refresh", "rf")}${X.toolbar}</span>`;
+    const x = `<span class="icb" data-act="ovclose" title="关闭">${ic("x")}</span>`;
+    const bAuto = btn(X.master.buttons[0][0], "", "ovtoast", 'data-msg="示例:不真正提交"');
+    const bOrder = btn("直接下单", "", "ovtoast", 'data-msg="发行端 HedgeOrderDialog(普通单+算法单)本稿未画,按弹窗 J 的卡片分区实现"');
+    const bFill = btn("补录", "", "ovopen", 'data-o="fill"');
+    const bDone = btn("完成对冲", "pri", "ovtoast", 'data-msg="示例:不真正提交"');
+    const suggest = `<div class="alt alt-info">${ic("info")}<span>${esc(X.hedge.suggest)}</span></div>`;
+    const allTiles = ["对冲比例", "累计成交数量", "累计有效委托数量", "对冲含费成交均价", "累计发生金额(交易币种)", "累计发生金额(结算币种)", "结算汇率"];
+    return `<div class="dw dw-e" style="width:1040px" data-act="stop"><div class="dw-hd"><div class="idc"><div class="idr"><span class="no">${m["主订单编号"]}</span>${tags}</div>
+      <div class="facts"><span>证券<b>${m["证券代码/名称"]}</b></span><span>名义本金<b>${a["订单名义本金"]}</b></span><span>投资者<b>${a["投资者名称"]}</b></span><span>下单时间<b>${m["下单时间"]}</b></span></div></div><span class="sp"></span>${rf}${x}</div>
+      <div class="dw-bd gray" data-keep="dwbd">${drCard(X.master.title, bAuto, kv(X.master.fields, "bd3", 3))}${drCard(X.subA.title, "", kv(X.subA.fields, "bd3", 3))}
+      ${drCard(X.hedge.title, bOrder + bFill + bDone, `${suggest}<div class="gap"></div>${tiles(allTiles, true)}<div class="gap"></div>${hedgeTable()}`)}</div></div>`;
+  };
+
+  /* ---------------- 画布:挂载、缩放、交互 ---------------- */
+  const canvasRenderers = {}; // kind -> fn(spec)
+  function renderCanvas(host) {
+    const sp = host._spec;
+    if (!sp) return;
+    const keep = {};
+    $$("[data-keep]", host).forEach((el) => (keep[el.dataset.keep] = [el.scrollTop, el.scrollLeft]));
+    const hostKeep = [host.scrollTop, host.scrollLeft];
+    let inner, cls;
+    if (sp.kind === "shell") { inner = shellHTML(sp.st); cls = "shell"; }
+    else { const fn = canvasRenderers[sp.kind]; inner = fn ? fn(sp) : ""; cls = sp.kind === "kit" || sp.kind === "feedback" ? "kit" : sp.kind; }
+    host.innerHTML = `<div class="cv ${cls}" data-v="${T.key}" ${T.dark ? 'data-dark=""' : ""} data-end="${sp.st ? sp.st.end : sp.end || ""}">${inner}</div>`;
+    $$("[data-keep]", host).forEach((el) => { const k = keep[el.dataset.keep]; if (k) { el.scrollTop = k[0]; el.scrollLeft = k[1]; } });
+    host.scrollTop = hostKeep[0];
+    host.scrollLeft = hostKeep[1];
+    fitCanvas(host);
+    const ms = $("[data-ms]", host);
+    if (ms) { ms.focus(); ms.setSelectionRange(ms.value.length, ms.value.length); }
+  }
+  function fitCanvas(host) {
+    const cv = host.firstElementChild;
+    if (!cv) return;
+    const z = host._zoom ? host._zoom() : Math.min(1, host.clientWidth / 1440);
+    cv.style.zoom = z;
+    if (host._fitHeight) host.style.height = cv.classList.contains("kit") ? "" : Math.round(900 * z) + "px";
+  }
+  function mount(host, spec, opts = {}) {
+    const fresh = !host._bound;
+    host._spec = spec;
+    host._zoom = opts.zoom || null;
+    host._fitHeight = !!opts.fitHeight;
+    if (fresh && !opts.inert) { bindCanvas(host); host._bound = true; }
+    renderCanvas(host);
+  }
+  function navigate(st, key) {
+    if (!st.tags.includes(key)) st.tags.push(key);
+    st.active = key;
+    if (groupName(key)) st.open.add(groupName(key));
+    st.pop = null; st.ctx = null; st.msq = "";
+  }
+  function closeTag(st, key) {
+    const i = st.tags.indexOf(key);
+    if (i < 0 || key === "首页") return;
+    st.tags.splice(i, 1);
+    if (st.active === key) st.active = st.tags[Math.min(i, st.tags.length - 1)] || "首页";
+  }
+  function toast(host, text) {
+    const cv = host.firstElementChild;
+    if (!cv) return;
+    const t = document.createElement("div");
+    t.className = "toast";
+    t.style.cssText = "position:absolute;left:50%;top:72px;transform:translateX(-50%);z-index:300";
+    t.innerHTML = `${ic("info", "")}<span>${esc(text)}</span>`;
+    t.querySelector(".ic").style.color = "var(--c-primary)";
+    cv.appendChild(t);
+    setTimeout(() => t.remove(), 2200);
+  }
+  const spinRf = (el) => { const svg = el.querySelector(".rf"); if (svg) { svg.classList.remove("spin"); void svg.getBoundingClientRect(); svg.classList.add("spin"); } };
+
+  // 弹层里的动作;返回 true 表示已处理并需要重绘
+  function overlayAction(host, st, el, act) {
+    const o = el.closest("[data-ovl]");
+    const type = o ? o.dataset.ovl : st.ovls[st.ovls.length - 1];
+    const ov = OV[type] || {};
+    switch (act) {
+      case "ovclose": st.ovls.pop(); OV.fill.pop = null; return true;
+      case "ovopen": {
+        const t = el.dataset.o;
+        if (el.dataset.kind) OV.confirm.kind = el.dataset.kind;
+        if (el.dataset.dir) OV.trs.dir = el.dataset.dir;
+        if (!st.ovls.includes(t)) st.ovls.push(t);
+        return true;
+      }
+      case "ovset": ov[el.dataset.k] = el.dataset.v; return true;
+      case "hsys": OV.fill.pop = OV.fill.pop === "hsys" ? null : "hsys"; return true;
+      case "hsysset": OV.fill.hedgeSys = el.dataset.v; OV.fill.pop = null; return true;
+      case "chk": OV.investor.checks[+el.dataset.i] = !OV.investor.checks[+el.dataset.i]; return true;
+      case "ovrf": spinRf(el); return false;
+      case "ovtoast": toast(host, el.dataset.msg); return false;
+      default: return null;
+    }
+  }
+  const extraActions = {}; // pages.js 登记:act -> fn(host, st, el) 返回 true 则重绘
+  // 画面里的换肤入口(登录页月亮按钮、用户菜单「界面设置」):默认只重绘本画布;设计稿外壳会在 book.js 里覆盖成全局重绘
+  extraActions.theme = (host, st) => { if (st && st.pop) st.pop = null; toggleTheme(); return true; };
+  function bindCanvas(host) {
+    host.addEventListener("click", (ev) => {
+      const sp = host._spec;
+      if (!sp) return;
+      const el = ev.target.closest("[data-act]");
+      const act = el && host.contains(el) ? el.dataset.act : null;
+      if (sp.kind !== "shell") {
+        // 登录页等非外壳画布:只处理登记过的动作
+        if (act && extraActions[act]) { if (extraActions[act](host, sp, el)) renderCanvas(host); }
+        else if (act === "toast") toast(host, el.dataset.msg);
+        return;
+      }
+      const st = sp.st;
+      const inFloat = ev.target.closest(".fl, .ctx");
+      if (!act) {
+        if ((st.pop || st.ctx) && !inFloat) { st.pop = null; st.ctx = null; renderCanvas(host); }
+        return;
+      }
+      if (act === "stop") { if (st.pop) { st.pop = null; renderCanvas(host); } if (OV.fill.pop) { OV.fill.pop = null; renderCanvas(host); } return; }
+      const ovr = overlayAction(host, st, el, act);
+      if (ovr !== null) { if (ovr) { st.pop = null; renderCanvas(host); } return; }
+      if (extraActions[act]) { if (extraActions[act](host, st, el)) renderCanvas(host); return; }
+      switch (act) {
+        case "grp": if (!st.col) { st.open.has(el.dataset.g) ? st.open.delete(el.dataset.g) : st.open.add(el.dataset.g); } break;
+        case "leaf": navigate(st, el.dataset.k); st.ovls = []; break;
+        case "tag": st.active = el.dataset.k; st.ctx = null; st.pop = null; break;
+        case "tagx": closeTag(st, el.dataset.k); st.ctx = null; break;
+        case "col": st.col = !st.col; st.pop = null; break;
+        case "pop": { const p = el.dataset.pop; st.pop = st.pop === p ? null : p; st.ctx = null; if (p === "ms") st.msq = ""; break; }
+        case "sf": st.sfOpen = !st.sfOpen; break;
+        case "rowsel": { const no = el.dataset.no; st.sel.has(no) ? st.sel.delete(no) : st.sel.add(no); break; }
+        case "allsel": { const rows = ENDS[st.end].rows; const all = rows.every((r) => st.sel.has(r.no)); st.sel = all ? new Set() : new Set(rows.map((r) => r.no)); break; }
+        case "clearsel": st.sel = new Set(); break;
+        case "ptab": st.tab[el.dataset.g] = el.dataset.v; break;
+        case "ctx": {
+          const k = st.ctx && st.ctx.k;
+          const d = el.dataset.do;
+          if (d === "close") closeTag(st, k);
+          if (d === "others") { st.tags = st.tags.filter((t) => t === "首页" || t === k); st.active = k; }
+          if (d === "all") { st.tags = ["首页"]; st.active = "首页"; }
+          st.ctx = null;
+          break;
+        }
+        case "tdrf": spinRf(el); return;
+        case "toast": toast(host, el.dataset.msg); if (st.pop) { st.pop = null; break; } return;
+        case "noop": if (st.pop) { st.pop = null; break; } return;
+        default: return;
+      }
+      if (act !== "pop" && act !== "tag" && act !== "ctx" && act !== "tagx" && act !== "noop") st.pop = st.pop && inFloat && act !== "leaf" ? st.pop : null;
+      renderCanvas(host);
+    });
+    host.addEventListener("contextmenu", (ev) => {
+      const st = host._spec && host._spec.st;
+      const tab = ev.target.closest(".tvi");
+      if (!st || !tab || !host.contains(tab)) return;
+      ev.preventDefault();
+      st.ctx = { k: tab.dataset.k, left: tab.offsetLeft };
+      st.pop = null;
+      renderCanvas(host);
+    });
+    host.addEventListener("input", (ev) => {
+      const st = host._spec && host._spec.st;
+      if (!st || !ev.target.matches("[data-ms]")) return;
+      st.msq = ev.target.value;
+      const list = $(".ms-list", host);
+      if (list) list.innerHTML = msResults(st);
+    });
+    host.addEventListener("keydown", (ev) => {
+      const st = host._spec && host._spec.st;
+      if (!st || !ev.target.matches("[data-ms]")) return;
+      if (ev.key === "Enter") { const first = $(".ms-list [data-act=leaf]", host); if (first) { navigate(st, first.dataset.k); renderCanvas(host); } }
+      if (ev.key === "Escape") { ev.stopPropagation(); st.pop = null; renderCanvas(host); }
+    });
+  }
+  document.addEventListener("mousedown", (ev) => {
+    $$(".bk-host").forEach((host) => {
+      const st = host._spec && host._spec.st;
+      if (st && (st.pop || st.ctx) && !host.contains(ev.target)) { st.pop = null; st.ctx = null; renderCanvas(host); }
+    });
+  });
+
+  injectTokens();
+  return { D, D2, T, $, $$, esc, ic, CHECK, EMPTY, dict, tag, ENDS, leafName, groupName, newCanvasState, OV, OPT, resetOV,
+    pagerHTML, ctlHTML, cellHTML, cellAuto, tableHTML, btn, listHTML, skelHTML, pageRenderers, overlayRenderers, canvasRenderers, extraActions,
+    kv, kvVal, item, ctl, lblW, numUnit, dgWrap, dgCard, sfDialog, stepper, closeX, btnCancel, drCard, tiles, errHTML,
+    renderCanvas, fitCanvas, mount, navigate, toast, shellHTML, setTheme, toggleTheme, THEME_PICK };
+})();

+ 114 - 0
src/book.css

@@ -0,0 +1,114 @@
+/* ============ 设计稿外壳(不参与设计,保持中性) ============ */
+:root { --bk-bg: #F3F4F6; --bk-card: #FFFFFF; --bk-t1: #1F2329; --bk-t2: #4E5561; --bk-t3: #8A919C; --bk-bd: #E3E5E9; --bk-acc: #2F7CF6; --bk-link: #1D5FD0; --bk-acc-bg: #EAF2FF; --bk-warn: #B45309; --bk-warn-bg: #FFF4E5; }
+* { box-sizing: border-box; }
+html, body { margin: 0; }
+body { min-width: 1100px; background: var(--bk-bg); color: var(--bk-t1); font: 14px/22px -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; -webkit-font-smoothing: antialiased; }
+body.bk-lock { overflow: hidden; }
+button { font: inherit; cursor: pointer; }
+[hidden] { display: none !important; }
+a { color: var(--bk-link); text-decoration: none; }
+
+.bk-hd { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 16px; height: 56px; padding: 0 24px; background: var(--bk-card); border-bottom: 1px solid var(--bk-bd); }
+.bk-hd b { font-size: 16px; white-space: nowrap; }
+.bk-hd .bk-picks { display: flex; gap: 6px; flex-wrap: wrap; font-size: 12px; color: var(--bk-t2); }
+.bk-hd .bk-picks span { padding: 2px 10px; border-radius: 12px; background: var(--bk-acc-bg); color: var(--bk-link); white-space: nowrap; }
+.bk-hd .sp { flex: 1; }
+.bk-hd .bk-date { font-size: 12px; color: var(--bk-t3); white-space: nowrap; }
+.bk-theme { display: flex; align-items: center; gap: 8px; flex: none; }
+.bk-theme-nm { min-width: 56px; font-size: 12px; color: var(--bk-t2); white-space: nowrap; }
+.bk-btn.sm { height: 28px; padding: 0 12px; font-size: 13px; }
+
+.bk-body { display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: 16px; padding: 16px 24px 40px; align-items: start; }
+.bk-nav { position: sticky; top: 72px; display: flex; flex-direction: column; gap: 10px; max-height: calc(100vh - 88px); overflow: auto; }
+.bk-grp { background: var(--bk-card); border: 1px solid var(--bk-bd); border-radius: 10px; padding: 6px; }
+.bk-grp-t { padding: 6px 10px 4px; font-size: 12px; font-weight: 600; color: var(--bk-t3); }
+.bk-item { display: flex; flex-direction: column; gap: 1px; padding: 7px 10px; border-radius: 8px; color: var(--bk-t1); }
+.bk-item:hover { background: var(--bk-bg); }
+.bk-item.on { background: var(--bk-acc-bg); }
+.bk-item b { font-weight: 500; font-size: 13px; }
+.bk-item.on b { color: var(--bk-link); font-weight: 600; }
+.bk-item small { font-size: 11px; line-height: 15px; color: var(--bk-t3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.bk-main { min-width: 0; }
+.bk-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
+.bk-bar-l, .bk-bar-r { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
+.bk-ttl { font-size: 17px; }
+.bk-sub { font-size: 13px; color: var(--bk-t3); }
+.bk-lbl { font-size: 12px; color: var(--bk-t3); }
+.bk-seg { display: inline-flex; padding: 2px; background: #E7E9ED; border-radius: 8px; }
+.bk-seg button { height: 28px; padding: 0 12px; border: 0; border-radius: 6px; background: none; color: var(--bk-t2); font-size: 13px; white-space: nowrap; }
+.bk-seg button:hover { color: var(--bk-t1); }
+.bk-seg button.on { background: var(--bk-card); color: var(--bk-t1); font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
+.bk-btn { height: 32px; padding: 0 14px; border: 1px solid var(--bk-bd); border-radius: 6px; background: var(--bk-card); color: var(--bk-t1); white-space: nowrap; }
+.bk-btn:hover { border-color: #C9CDD4; }
+
+.bk-host { position: relative; overflow: hidden; background: #fff; }
+#bkStage { border-radius: 8px; box-shadow: 0 0 0 1px var(--bk-bd), 0 4px 16px rgba(15,23,42,.06); }
+#bkStage.is-kit { height: auto !important; }
+.bk-notes { display: grid; grid-template-columns: 1fr 1.4fr 1fr; gap: 12px; margin-top: 12px; }
+.bk-note { padding: 12px 16px; background: var(--bk-card); border: 1px solid var(--bk-bd); border-radius: 10px; font-size: 13px; line-height: 20px; color: var(--bk-t2); }
+.bk-note h4 { margin: 0 0 6px; font-size: 13px; color: var(--bk-t1); }
+.bk-note p { margin: 0; word-break: break-all; }
+.bk-note ul { margin: 0; padding-left: 18px; }
+.bk-note li + li { margin-top: 4px; }
+.bk-note.todo { border-color: #F3D9A4; background: var(--bk-warn-bg); }
+.bk-note.todo h4 { color: var(--bk-warn); }
+
+/* 规范摘要 */
+.spec { max-width: 1100px; display: flex; flex-direction: column; gap: 14px; }
+.spec section { padding: 18px 22px; background: var(--bk-card); border: 1px solid var(--bk-bd); border-radius: 10px; }
+.spec h2 { margin: 0 0 10px; font-size: 16px; }
+.spec p { margin: 0 0 8px; color: var(--bk-t2); }
+.spec code { padding: 0 4px; border-radius: 4px; background: var(--bk-bg); font-size: 12px; }
+.sp-picks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
+.sp-pick:last-child:nth-child(odd) { grid-column: 1 / -1; }
+.sp-pick { padding: 12px 14px; border: 1px solid var(--bk-bd); border-radius: 8px; }
+.sp-pick-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
+.sp-pick p { font-size: 13px; line-height: 20px; }
+.rv-letter { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex: none; border-radius: 6px; background: var(--bk-acc-bg); color: var(--bk-link); font-size: 12px; font-weight: 600; }
+.sp-go { display: inline-block; margin: 2px 8px 2px 0; padding: 0 8px; border-radius: 10px; background: var(--bk-acc-bg); color: var(--bk-link); font-size: 12px; line-height: 20px; white-space: nowrap; }
+.sp-tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
+.sp-tbl th, .sp-tbl td { padding: 8px 10px; border-bottom: 1px solid var(--bk-bd); text-align: left; vertical-align: top; }
+.sp-tbl th { width: 110px; color: var(--bk-t3); font-weight: 500; white-space: nowrap; }
+.sp-tbl.map thead th { color: var(--bk-t1); font-weight: 600; width: auto; }
+.sp-grp { margin-bottom: 12px; }
+.sp-grp h4 { margin: 0 0 6px; font-size: 12px; color: var(--bk-t3); font-weight: 500; }
+.sp-sw { display: flex; flex-wrap: wrap; gap: 8px; }
+.sp-swc { width: 112px; font-size: 11px; line-height: 15px; }
+.sp-swc i { display: block; height: 36px; border-radius: 4px; margin-bottom: 4px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.06); }
+.sp-swc b { display: block; color: var(--bk-t1); font-weight: 500; }
+.sp-swc span { color: var(--bk-t3); font-family: Inter, sans-serif; }
+.sp-h2-sub { margin-left: 10px; font-size: 12px; font-weight: 400; color: var(--bk-t3); }
+.sp-themes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; }
+.sp-theme { display: flex; flex-direction: column; padding: 12px 14px; border: 1px solid var(--bk-bd); border-radius: 8px; font-size: 13px; line-height: 20px; }
+.sp-theme.on { border-color: var(--bk-acc); box-shadow: 0 0 0 2px var(--bk-acc-bg); }
+.sp-theme p { margin: 8px 0 6px; color: var(--bk-t2); }
+.sp-theme ul { margin: 0 0 10px; padding-left: 16px; color: var(--bk-t2); font-size: 12px; line-height: 18px; }
+.sp-theme li + li { margin-top: 3px; }
+.sp-theme .bk-btn { margin-top: auto; align-self: flex-start; }
+.sp-cur { margin-left: auto; padding: 0 8px; border-radius: 10px; background: var(--bk-acc-bg); color: var(--bk-link); font-size: 11px; line-height: 18px; }
+.rv-letter.dk { background: #1F2937; color: #fff; }
+.sp-strip { display: flex; gap: 4px; height: 36px; padding: 6px; border-radius: 6px; margin-top: 2px; }
+.sp-strip i { flex: 1; border-radius: 3px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.14); }
+.sp-tbl.cmp thead th { color: var(--bk-t1); font-weight: 600; width: auto; }
+.sp-tbl.cmp tbody th { width: 110px; }
+.sp-todo { margin: 0; padding-left: 18px; color: var(--bk-t2); }
+.sp-todo > li { margin-bottom: 8px; }
+.sp-todo ul { margin: 2px 0 0; padding-left: 18px; }
+
+/* 全屏 */
+.bk-full { position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; align-items: center; background: #0F172A; }
+.bk-full-bar { display: flex; align-items: center; gap: 12px; width: 100%; height: 48px; padding: 0 16px; color: #fff; }
+.bk-full-bar .bk-ttl { color: #fff; }
+.bk-full-bar .sp { flex: 1; }
+.bk-full-bar .bk-seg { background: rgba(255,255,255,.12); }
+.bk-full-bar .bk-seg button { color: rgba(255,255,255,.75); }
+.bk-full-bar .bk-seg button.on { background: #fff; color: var(--bk-t1); }
+.bk-k { font-size: 12px; color: rgba(255,255,255,.55); }
+.bk-x { width: 32px; height: 32px; border: 0; border-radius: 6px; background: rgba(255,255,255,.12); color: #fff; }
+#bkFullStage { margin-top: 8px; border-radius: 6px; }
+#bkFullStage.is-kit { overflow-y: auto; overflow-x: hidden; }
+
+/* 单独模式 */
+body.bk-solo { background: #fff; min-width: 0; }
+body.bk-solo .bk-host { width: 1440px; }

+ 257 - 0
src/book.js

@@ -0,0 +1,257 @@
+/* 设计稿外壳:左侧页面目录、舞台(1440×900 画布缩放显示)、说明面板、规范摘要、全屏、单独模式。 */
+(function () {
+  "use strict";
+  const U = window.UI;
+  const { $, $$, esc, D2, T, ENDS, ic } = U;
+  const PAGES = D2.pages;
+  const byKey = Object.fromEntries(PAGES.map((p) => [p.key, p]));
+  const GROUPS = ["总览", "全局", "弹层", "页面", "反馈"];
+  const ALL_ENDS = ["issue", "sales", "trs"];
+  const S = { page: "spec", end: "issue", variant: {}, theme: "f" }; // theme:当前皮肤(f 浅色;da/db/dc 三版深色候选)
+  const CS = {}; // 画布状态缓存:页面:端:变体
+  const SP = {}; // 画布规格缓存
+
+  const endsOf = (p) => (p.ends ? ALL_ENDS : [p.end || "issue"]);
+  const curEnd = (p) => (endsOf(p).includes(S.end) ? S.end : endsOf(p)[0]);
+  const curVariant = (p) => S.variant[p.key] || (p.variants ? p.variants[0][0] : "");
+
+  function stateFor(p, end, v) {
+    const key = [p.key, end, v].join(":");
+    if (!CS[key]) {
+      const st = U.newCanvasState(end, p.key === "err" ? v : p.active);
+      st.ovls = [...(p.ovl || [])];
+      if (p.key === "profile" && v === "pwd") st.ovls = ["pwd"];
+      CS[key] = st;
+    }
+    return CS[key];
+  }
+  function specFor(p) {
+    const end = curEnd(p);
+    const v = curVariant(p);
+    const key = [p.key, end, v].join(":");
+    if (p.kind === "shell") {
+      if (p.key === "confirm") U.OV.confirm.kind = v;
+      if (p.key === "dlg-trs") Object.assign(U.OV.trs, { dir: v === "sell" ? "卖出" : "买入", qty: v === "sell" ? "500" : "1,000" });
+      return { kind: "shell", st: stateFor(p, end, v) };
+    }
+    if (!SP[key]) SP[key] = p.kind === "login" ? { kind: "login", end, v } : { kind: p.kind };
+    return SP[key];
+  }
+  // 登录页里切换「忘记密码/返回登录」时同步变体按钮
+  U.extraActions.lv = (host, sp, el) => { sp.v = el.dataset.v; S.variant.login = sp.v; renderBar(); return true; };
+  // 画面里的换肤入口(登录页月亮/太阳按钮、用户菜单「界面设置」):浅色和最近看过的深色之间切换;外壳模式下所有画布、顶栏、规范摘要一起重绘
+  U.extraActions.theme = (host, st) => {
+    if (st && st.pop) st.pop = null;
+    U.toggleTheme();
+    S.theme = T.key;
+    if (!$("#bkBar")) return true; // 单独模式:只重绘这张画布
+    writeHash(); renderAll();
+    return false;
+  };
+  // 登录页左侧三端入口:点了切到那个系统(相当于跳到对应系统的登录页),同步上方「看哪一端」
+  U.extraActions.sys = (host, sp, el) => {
+    if (el.dataset.end === curEnd(page())) return false;
+    S.end = el.dataset.end;
+    if (!$("#bkBar")) { sp.end = S.end; return true; } // 单独模式:没有外壳,直接改画布
+    writeHash(); renderBar(); renderStage();
+    if (!$("#bkFull").hidden) renderFull();
+    return false;
+  };
+
+  /* ---------------- 规范摘要 ---------------- */
+  function specHTML() {
+    const picks = [
+      ["基础视觉", "F", "晴空渐变", "主色晴空蓝 #3C8CFF,文字链接 #1668DC;主按钮渐变加淡蓝投影;页面底色蓝白渐变、右上角浅蓝光晕;卡片白底 14px 圆角淡蓝投影;表头淡蓝底;状态是无描边的浅色圆角标签;数字用 Inter。", "kit"],
+      ["整体框架", "A", "经典三段", "浅色侧栏 220px 通高(白底微透、深灰字,深色皮肤下近黑);白色顶栏 52px 放折叠按钮、面包屑、营业日与两个 A 股交易日、菜单搜索、全屏、通知、用户;下面 38px 卡片式标签页;内容区四周留 16px。", "home"],
+      ["弹窗", "J", "卡片分区", "长表单 920px:内容区灰底,每一组一张白卡片,标签在上、双列,卡片标题右侧显示已填/必填。短表单改按 10-08 给的下单弹窗布局:标签在左、单列,顶部高亮信息块,底部灰色结果块(补录 640px、修改密码 480px、TRS 下单 880px 加右侧行情栏)。", "dlg-fill"],
+      ["抽屉", "E", "卡片+描述表", "宽 1040px;头部是编号、状态标签和一行关键信息;灰底上排卡片,键值用带边框的描述表(灰底标签格,一行 3 对);汇总做成数字卡片。", "drawer"],
+      ["登录页", "C", "右侧浮卡·全幅底图", "整页蓝白渐变底色叠淡线条装饰;表单卡片 440px 靠右浮着,输入框按代码现状 40px;左侧一列是系统名、本系统业务模块和三张三端入口卡片,点入口跳到对应系统的登录页。(10-07 追加的分项选型类别,选 C。)", "login"],
+      ["深色皮肤", "B", "石墨灰", "中性暗灰不偏蓝;扁平,卡片靠 1px 描边分区,没有渐变、光晕和投影;主按钮实心蓝;状态标签只描边不铺底。尺寸、圆角、字体、排布和浅色完全相同,只换令牌取值。(10-08 三版深色候选里选 B;A 夜空蓝、C 极光玻璃未选。)", "kit"],
+    ];
+    const dims = [
+      ["设计基准", "1440 × 900;三端完全一致,不做识别色"], ["控件高度", "32px(登录页按代码现状 40px)"], ["表格", "表头 40px、行高 40px;表头 13px、很淡的蓝底;数字右对齐用 Inter"],
+      ["文字", "正文 14px / 22px,辅助 12px / 18px,区块标题 15px,弹窗标题 16px,编号标题 20px;中文系统字体,英文和数字 Inter"],
+      ["圆角", "控件 8px、卡片 14px、标签 6px、弹窗 16px"], ["投影", "卡片和主按钮用淡蓝投影;弹层 0 10px 28px;弹窗与抽屉 0 24px 64px"],
+      ["侧栏与顶栏", "侧栏 220px 浅色(深色皮肤下近黑),收起后 64px;顶栏 52px;标签页栏 38px;内容区四周留 16px,卡片间距 12px"], ["页签", "框架标签页 28px 卡片式,当前页带主色圆点;页面级页签 40px 无底色;卡片内页签 48px 在卡片标题栏里;抽屉内页签 42px"],
+      ["弹窗宽度", "短表单 640px、长表单 920px、确认框 420px、修改密码 480px、TRS 下单 880px"], ["抽屉宽度", "1040px(订单详情、票据详情)"],
+      ["搜索区", "一张卡片,3 列等宽,标签在左、按本页最长标签统一宽度;超过 2 行折叠"], ["描述表", "标签格 116px 灰底,一行 3 对;标签长的分组一行 2 对(标签格 190px);单列 132px"],
+      ["不放的东西", "页面标题(面包屑和标签页已有页面名)、天气问候、界面尺寸切换"],
+    ];
+    const swatches = T.palette.map(([g, items]) => `<div class="sp-grp"><h4>${g}</h4><div class="sp-sw">${items.map(([l, k]) => `<div class="sp-swc"><i style="background:${T.tokens[k]}"></i><b>${l}</b><span>${T.tokens[k]}</span></div>`).join("")}</div></div>`).join("");
+    const map = [
+      ["基础视觉", "F 晴空渐变", ["kit"]], ["整体框架", "A 经典三段", ["home", "list"]], ["登录页", "C 右侧浮卡·全幅底图 + 左侧三端入口", ["login"]],
+      ["搜索区", "3 列标签在左,可折叠;无标签时只放占位", ["list", "tabs"]], ["表格", "固定列、选择条、分页、空状态、加载态", ["list", "clearing", "drawer-note", "feedback"]],
+      ["弹窗", "长表单 J 卡片分区;短表单标签在左+信息块+结果块(10-08 布局);TRS 下单同一布局加行情栏", ["dlg-fill", "dlg-investor", "dlg-trs", "profile"]], ["抽屉", "E 卡片+描述表", ["drawer", "drawer-note"]],
+      ["详情展示", "带边框描述表 + 数字卡片", ["drawer", "drawer-note", "profile", "dlg-trs"]], ["页内标签页", "页面级/卡片内/抽屉内三种位置", ["tabs", "clearing", "drawer-note", "dlg-trs"]],
+      ["反馈类", "确认框、提示、通知、空状态、加载、结果、校验", ["confirm", "feedback"]], ["首页", "工作台:状态数字、待处理表、快捷入口", ["home"]], ["其他页", "个人中心、404 / 401", ["profile", "err"]],
+    ];
+    const go = (k) => `<a class="sp-go" href="#p/${k}">${esc(byKey[k].title)}</a>`;
+    const themes = T.order.map((k) => T.all[k]);
+    const tv = (t, key) => { const v = t.tokens[key]; const m = /^var\(--([\w-]+)\)$/.exec(v || ""); return m ? tv(t, m[1]) : v; }; // 色条在外壳里,var() 不生效,解析成实际值
+    const stripHTML = (t) => `<div class="sp-strip" style="background:${tv(t, "bg-page")}">${T.strip.map(([l, key]) => `<i title="${l}" style="background:${tv(t, key)}"></i>`).join("")}</div>`;
+    const themeCards = themes.map((t) => `<div class="sp-theme ${t.key === T.key ? "on" : ""}" data-theme="${t.key}"><div class="sp-pick-hd"><span class="rv-letter ${t.dark ? "dk" : ""}">${t.letter}</span><b>${t.label} · ${t.name}</b>${t.key === T.key ? '<span class="sp-cur">正在看</span>' : ""}</div>${stripHTML(t)}<p>${esc(t.tagline)}</p><ul>${t.points.map((x) => `<li>${esc(x)}</li>`).join("")}</ul><button type="button" class="bk-btn sm" data-theme-go="${t.key}">用这套皮肤看首页</button></div>`).join("");
+    const cmpRows = Object.keys(themes[0].cmp).map((r) => `<tr><th>${r}</th>${themes.map((t) => `<td>${esc(t.cmp[r])}</td>`).join("")}</tr>`).join("");
+    const todos = PAGES.filter((p) => p.todo && p.todo.length).map((p) => `<li><b>${go(p.key)}</b><ul>${p.todo.map((t) => `<li>${esc(t)}</li>`).join("")}</ul></li>`).join("");
+    return `<article class="spec">
+      <section><h2>这份稿子是什么</h2>
+        <p>2026-10-08 第一批分项选型的评审结果拼成的<b>完整 UI 设计</b>:基础视觉 F 晴空渐变、整体框架 A 经典三段、长表单弹窗 J 卡片分区、抽屉 E 卡片+描述表;短表单弹窗按 10-08 给的下单弹窗布局;登录页按 10-07 追加的分项选型类别选定的 C 右侧浮卡·全幅底图。(基础视觉原选 L,改过 B,最终定 F;框架原选 E,最终定 A。)原计划第二、三批(搜索区、表格、详情展示、页内标签页、反馈类、首页、其他页)不再分批出方案,直接按这些选择画成一套,每一类在本稿里都有对应的页面(见下表)。</p>
+        <p>每个页面都是 1440×900 的真实尺寸画布,可以直接点:菜单、标签页(含右键)、收起侧栏、菜单搜索、通知、用户菜单、搜索展开、勾选表格行、打开详情抽屉、在抽屉里叠开补录弹窗、切换页签、切换方向和价格。菜单、字段、列名取自三端真实代码;表里的数值是示例。</p>
+        <p>换肤:两套皮肤——浅色 F 晴空渐变(默认)和深色 B 石墨灰(10-08 从三版深色候选里选定;A 夜空蓝、C 极光玻璃未选)。顶栏右侧的「皮肤」可以切着看每一个页面,画面里登录页右上角的月亮/太阳按钮、用户菜单的「界面设置」也能来回切。</p></section>
+      <section><h2>选型结果</h2><div class="sp-picks">${picks.map(([c, k, n, d, key]) => `<div class="sp-pick"><div class="sp-pick-hd"><span class="rv-letter">${k}</span><b>${c} · ${n}</b></div><p>${d}</p>${go(key)}</div>`).join("")}</div></section>
+      <section><h2>尺寸与规则</h2><table class="sp-tbl"><tbody>${dims.map(([k, v]) => `<tr><th>${k}</th><td>${v}</td></tr>`).join("")}</tbody></table></section>
+      <section><h2>颜色<small class="sp-h2-sub">当前皮肤:${T.label} ${T.name}(顶栏切换后这里跟着变)</small></h2>${swatches}</section>
+      <section><h2>换肤<small class="sp-h2-sub">两套皮肤尺寸、圆角、字体、排布相同,只改颜色与质感;深色 B 石墨灰为 10-08 选定</small></h2><div class="sp-themes">${themeCards}</div>
+        <table class="sp-tbl cmp"><thead><tr><th></th>${themes.map((t) => `<th>${t.label} · ${t.name}</th>`).join("")}</tr></thead><tbody>${cmpRows}</tbody></table></section>
+      <section><h2>12 类在本稿里的位置</h2><table class="sp-tbl map"><thead><tr><th>分类</th><th>怎么处理</th><th>看哪个页面</th></tr></thead><tbody>${map.map(([c, h, keys]) => `<tr><th>${c}</th><td>${h}</td><td>${keys.map(go).join("")}</td></tr>`).join("")}</tbody></table></section>
+      <section><h2>待确认</h2><ul class="sp-todo">${todos}<li><b>全局</b><ul><li>换肤:深色已定 B 石墨灰。默认是浅色还是跟随系统;用户切换后的选择记在账号上还是本机浏览器——请确认。</li><li>第二、三批不再分批出方案,改为直接按本稿实现——请确认这个做法。</li></ul></li></ul></section>
+      <section><h2>文件</h2><p>本文件由 <code>src/build.py</code> 生成,单文件、离线可用;源码在 <code>src/</code>(<code>tokens.js</code> 令牌、<code>data.js</code>/<code>data2.js</code> 内容、<code>app.js</code>/<code>pages.js</code> 画布、<code>*.css</code> 样式)。<code>node test.mjs</code> 在生成的文件里实际点击每一类交互并断言。</p></section>
+    </article>`;
+  }
+
+  /* ---------------- 外壳渲染 ---------------- */
+  const page = () => byKey[S.page] || byKey.spec;
+  function renderNav() {
+    const p = page();
+    $("#bkNav").innerHTML = GROUPS.map((g) => `<div class="bk-grp"><div class="bk-grp-t">${g}</div>${PAGES.filter((x) => x.group === g).map((x) => `<a class="bk-item ${x.key === p.key ? "on" : ""}" href="#p/${x.key}"><b>${esc(x.title)}</b><small>${esc(x.sub || "")}</small></a>`).join("")}</div>`).join("");
+  }
+  const seg = (name, items, cur) => `<div class="bk-seg" data-seg="${name}">${items.map(([v, label]) => `<button type="button" class="${v === cur ? "on" : ""}" data-v="${v}">${label}</button>`).join("")}</div>`;
+  function renderBar() {
+    const p = page();
+    let left = `<b class="bk-ttl">${esc(p.title)}</b><span class="bk-sub">${esc(p.sub || "")}</span>`;
+    let right = "";
+    if (p.ends) right += `<span class="bk-lbl">看哪一端</span>` + seg("end", ALL_ENDS.map((k) => [k, ENDS[k].short]), curEnd(p));
+    if (p.variants) right += seg("variant", p.variants, curVariant(p));
+    if (p.kind !== "spec") right += `<button type="button" class="bk-btn" data-full>全屏查看</button>`;
+    $("#bkBar").innerHTML = `<div class="bk-bar-l">${left}</div><div class="bk-bar-r">${right}</div>`;
+  }
+  function renderStage() {
+    const p = page();
+    const host = $("#bkStage");
+    const spec = $("#bkSpec");
+    if (p.kind === "spec") {
+      host.hidden = true; spec.hidden = false; spec.innerHTML = specHTML();
+      host._spec = null; host.innerHTML = "";
+      return;
+    }
+    spec.hidden = true; host.hidden = false;
+    host.classList.toggle("is-kit", p.kind === "kit" || p.kind === "feedback");
+    U.mount(host, specFor(p), { fitHeight: true });
+  }
+  function renderNotes() {
+    const p = page();
+    const box = $("#bkNotes");
+    if (p.kind === "spec") { box.hidden = true; return; }
+    box.hidden = false;
+    box.innerHTML = `<div class="bk-note"><h4>来源</h4><p>${esc(p.src || "")}</p></div>
+      <div class="bk-note"><h4>设计要点</h4><ul>${(p.points || []).map((t) => `<li>${esc(t)}</li>`).join("")}</ul></div>
+      ${p.todo && p.todo.length ? `<div class="bk-note todo"><h4>待确认</h4><ul>${p.todo.map((t) => `<li>${esc(t)}</li>`).join("")}</ul></div>` : ""}`;
+  }
+  const themeSeg = () => seg("theme", T.order.map((k) => [k, `${T.all[k].label}${k === "f" ? " F" : ""}`]), T.key);
+  function renderTheme() {
+    const el = $("#bkTheme");
+    if (el) el.innerHTML = `<span class="bk-lbl">皮肤</span>${themeSeg()}<span class="bk-theme-nm" title="${esc(T.tagline)}">${esc(T.name)}</span>`;
+  }
+  function setTheme(k) {
+    if (!U.setTheme(k)) return;
+    if (T.dark) U.THEME_PICK.dark = k;
+    S.theme = k;
+    writeHash(); renderAll();
+  }
+  function renderAll() { renderTheme(); renderNav(); renderBar(); renderStage(); renderNotes(); if (!$("#bkFull").hidden) renderFull(); }
+
+  /* ---------------- 全屏 ---------------- */
+  function renderFull() {
+    const p = page();
+    let bar = `<b class="bk-ttl">${esc(p.title)}</b>`;
+    if (p.ends) bar += seg("end", ALL_ENDS.map((k) => [k, ENDS[k].short]), curEnd(p));
+    if (p.variants) bar += seg("variant", p.variants, curVariant(p));
+    bar += `<span class="bk-lbl">皮肤</span>${themeSeg()}`;
+    bar += `<span class="sp"></span><span class="bk-k">←/→ 切换页面,Esc 退出</span><button type="button" class="bk-x" data-close title="退出全屏">✕</button>`;
+    $("#bkFullBar").innerHTML = bar;
+    const host = $("#bkFullStage");
+    const kit = p.kind === "kit" || p.kind === "feedback";
+    host.classList.toggle("is-kit", kit);
+    U.mount(host, specFor(p), { zoom: () => { const w = window.innerWidth - 32, h = window.innerHeight - 64; return kit ? Math.min(1, w / 1440) : Math.min(1, w / 1440, h / 900); } });
+    host.style.width = Math.round(1440 * host.firstElementChild.style.zoom) + "px";
+    host.style.height = kit ? window.innerHeight - 64 + "px" : Math.round(900 * host.firstElementChild.style.zoom) + "px";
+  }
+  function openFull() { $("#bkFull").hidden = false; document.body.classList.add("bk-lock"); renderFull(); }
+  function closeFull() { $("#bkFull").hidden = true; document.body.classList.remove("bk-lock"); $("#bkFullStage")._spec = null; $("#bkFullStage").innerHTML = ""; renderStage(); }
+
+  /* ---------------- 路由与事件 ---------------- */
+  function readHash() {
+    const m = location.hash.match(/^#p\/([\w-]+)(?:\/(issue|sales|trs))?(?:\/([\w-]+))?(?:\/t=([a-z]+))?$/);
+    if (!m) return false;
+    if (!byKey[m[1]]) return false;
+    S.page = m[1];
+    if (m[2]) S.end = m[2];
+    if (m[3]) S.variant[S.page] = m[3];
+    if (m[4] && T.all[m[4]]) { U.setTheme(m[4]); if (T.dark) U.THEME_PICK.dark = m[4]; S.theme = m[4]; } // 地址里没带皮肤就沿用当前的
+    return true;
+  }
+  function writeHash() {
+    const p = page();
+    let h = `#p/${p.key}`;
+    if (p.ends) h += `/${curEnd(p)}`;
+    if (p.variants) h += (p.ends ? "" : `/${curEnd(p)}`) + `/${curVariant(p)}`;
+    if (T.key !== "f") h += `/t=${T.key}`;
+    if (location.hash !== h) history.replaceState(null, "", h);
+  }
+  function go(key) { S.page = key; writeHash(); renderAll(); window.scrollTo(0, 0); }
+  function bind() {
+    document.addEventListener("click", (e) => {
+      const t = e.target;
+      if (t.closest(".bk-host")) return;
+      const a = t.closest("a[href^='#p/']");
+      if (a) { e.preventDefault(); location.hash = a.getAttribute("href"); readHash(); writeHash(); renderAll(); window.scrollTo(0, 0); return; }
+      if (t.closest("[data-full]")) { openFull(); return; }
+      const tg = t.closest("[data-theme-go]");
+      if (tg) { if (U.setTheme(tg.dataset.themeGo)) { if (T.dark) U.THEME_PICK.dark = T.key; S.theme = T.key; } go("home"); return; }
+      if (t.closest("[data-close]")) { closeFull(); return; }
+      const sb = t.closest(".bk-seg button");
+      if (sb) {
+        const name = sb.closest(".bk-seg").dataset.seg;
+        const v = sb.dataset.v;
+        const p = page();
+        if (name === "theme") { setTheme(v); return; }
+        if (name === "end") S.end = v;
+        if (name === "variant") { S.variant[p.key] = v; if (p.key === "login") { const sp = SP[[p.key, curEnd(p), v].join(":")]; if (sp) sp.v = v; } }
+        writeHash();
+        renderBar(); renderStage();
+        if (!$("#bkFull").hidden) renderFull();
+      }
+    });
+    document.addEventListener("keydown", (e) => {
+      if (e.key === "Escape" && !$("#bkFull").hidden) { closeFull(); return; }
+      if (e.target.closest("input, textarea")) return;
+      if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
+        const i = PAGES.findIndex((p) => p.key === S.page);
+        go(PAGES[(i + (e.key === "ArrowRight" ? 1 : PAGES.length - 1)) % PAGES.length].key);
+        e.preventDefault();
+      }
+    });
+    window.addEventListener("hashchange", () => { if (readHash()) { renderAll(); } });
+    let rt = 0;
+    window.addEventListener("resize", () => { cancelAnimationFrame(rt); rt = requestAnimationFrame(() => { $$(".bk-host").forEach((h) => h._spec && U.fitCanvas(h)); if (!$("#bkFull").hidden) renderFull(); }); });
+  }
+
+  /* ---------------- 单独模式:#solo/<页面>[/<端>][/<变体>],原尺寸渲染一张画布,供截图和自动检查 ---------------- */
+  const solo = location.hash.match(/^#solo\/([\w-]+)(?:\/(issue|sales|trs))?(?:\/([\w-]+))?(?:\/lbl=(top|left))?(?:\/t=([a-z]+))?$/);
+  if (solo && byKey[solo[1]] && byKey[solo[1]].kind !== "spec") {
+    if (solo[4]) U.OPT.lblTop = solo[4] === "top";
+    if (solo[5] && T.all[solo[5]]) { U.setTheme(solo[5]); if (T.dark) U.THEME_PICK.dark = solo[5]; S.theme = solo[5]; }
+    S.page = solo[1];
+    if (solo[2]) S.end = solo[2];
+    if (solo[3]) S.variant[S.page] = solo[3];
+    document.body.className = "bk-solo";
+    document.body.innerHTML = '<div class="bk-host" id="solo"></div>';
+    U.mount($("#solo"), specFor(page()), { zoom: () => 1 });
+    window.__solo = $("#solo");
+    window.__book = { S, PAGES, specFor, page };
+    return;
+  }
+  readHash();
+  bind();
+  renderAll();
+  window.__book = { S, PAGES, specFor, page, go, CS, setTheme };
+})();

+ 28 - 0
src/build.py

@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+"""把 src/ 下的样式、数据、脚本和 Inter 字体内联进模板,生成单文件的完整设计稿。"""
+import base64
+from pathlib import Path
+
+SRC = Path(__file__).resolve().parent
+OUT = SRC.parent / "CMS-UI完整设计.html"
+
+fonts = []
+for weight, name in ((400, "Regular"), (500, "Medium"), (600, "SemiBold")):
+    data = base64.b64encode((SRC / "fonts" / f"Inter-{name}.woff2").read_bytes()).decode()
+    fonts.append(
+        '@font-face{font-family:"Inter";font-style:normal;font-weight:%d;font-display:block;'
+        'src:url(data:font/woff2;base64,%s) format("woff2")}' % (weight, data)
+    )
+parts = {"fonts.css": "\n".join(fonts)}
+for name in ("book.css", "kit.css", "frame.css", "overlay.css", "pages.css", "theme.css", "data.js", "data2.js", "tokens.js", "app.js", "pages.js", "book.js"):
+    text = (SRC / name).read_text(encoding="utf-8")
+    assert "</script>" not in text and "</style>" not in text, name
+    parts[name] = text
+
+html = (SRC / "template.html").read_text(encoding="utf-8")
+for name, text in parts.items():
+    key = "/*{{%s}}*/" % name
+    assert key in html, key
+    html = html.replace(key, text)
+OUT.write_text(html, encoding="utf-8")
+print("已生成", OUT.name, "%.0f KB" % (OUT.stat().st_size / 1024))

+ 443 - 0
src/data.js

@@ -0,0 +1,443 @@
+/* 各批方案共用的内容数据(沿用第五轮从代码提取的内容,只作内容来源,不含任何设计)。
+ * 字段、顺序、文案取自三端前端代码与 ddl 字典 seed;表内数值是按字段格式编写的示例值,未按业务公式计算。
+ * 2026-09-30 同步代码:TRS 委托状态「已过期」改为「过期」。
+ * 修改后执行:python3 build.py
+ */
+window.SPEC_DATA = (function () {
+  // 字典:文案 -> Element Plus tag 类型(取自 ddl/<sys>/public/数据字典/seed.sql)
+  const dict = {
+    direction: { 买入: "danger", 卖出: "success" },
+    op_type: { 正常: "primary", 强平: "danger", 到期: "warning" },
+    process_mode: { 自动: "primary", 人工: "warning" },
+    hedge_progress: { 未开始: "info", 对冲中: "primary", 已结束: "success" },
+    note_order_status: {
+      待处理: "info", 执行中: "primary", 待撤: "warning", 撤单中: "warning", 确认中: "primary",
+      已完成: "success", 已结算: "success", 已撤: "info", 已拒绝: "danger",
+    },
+    note_type: { "Delta A": "primary", "Delta B": "warning" },
+    order_source: { CRM: "primary", IOS: "info", Android: "info", H5: "info", 发行端: "warning" },
+    // 销售端下单方式的文案取自代码注释;标签颜色在 seed 里未核对,这里按「本端 primary、其余 info」示意
+    sales_order_source: { 销售端: "primary", iOS: "info", Android: "info", H5: "info" },
+    hedge_system: { TRS: "primary", 自定义: "info" },
+    hedge_order_type: { 普通单: "primary", 算法TWAP: "warning", 算法POV: "warning", "算法E-Amount": "warning" },
+    trs_order_status: {
+      未报: "info", 待报: "info", 已报: "primary", 部成: "warning", 待撤: "warning", 已撤: "info",
+      部撤: "warning", 已成: "success", 废单: "danger", 过期: "info", 部废: "danger",
+    },
+    trade_channel: { QFII: "primary", 深沪港通: "primary", 港股自营: "info" },
+    price_req_type: { 限价: "primary", 市价: "warning" },
+    order_type: { 正常下单: "primary", 后台下单: "info", 算法下单: "warning", 后台平仓: "danger", 后台补单: "info", 条件单: "success" },
+    order_mode: { Windows: "primary", iOS: "primary", Android: "primary", FIX: "warning", 电话: "info", 邮件: "info", CRM: "info", OpenAPI: "warning", H5: "info" },
+  };
+
+  const menus = {
+    issue: [
+      ["报表管理", "chart", ["共赢票据交易确认书", "日常报表"]],
+      ["风控管理", "shield", ["白名单管理", "共赢票据额度管理", "共赢票据风险监控"]],
+      ["共赢票据管理", "ticket", ["订单管理", "票据管理", "票据日常对冲统计", "票据展期管理", "询价管理"]],
+      ["汇率管理", "coin", ["账面汇率"]],
+      ["清算管理", "check", ["共赢票据成交对账", "共赢票据持仓对账", "共赢票据清算", "营业日日切"]],
+      ["系统设置", "gear", ["对冲账户管理", "费用管理", "交易时间片设置", "平台参数", "日历配置", "自动化配置"]],
+      ["常驻工作进程", "cpu", ["工作进程配置", "实例与分片监控", "操作记录"]],
+      ["销售机构管理", "building", ["机构管理", "平台参数"]],
+      ["计划任务中心", "clock", ["计划任务配置", "任务运行记录"]],
+      ["通知和告警管理", "mail", ["通知设置", "邮件发送记录", "短信发送记录", "飞书发送记录"]],
+    ],
+    sales: [
+      ["报表管理", "chart", ["共赢票据交易确认书", "日常报表", "日结单", "月结单"]],
+      ["费用管理", "coin", ["费用串列表", "投资者费用管理"]],
+      ["风控管理", "shield", ["共赢票据风险监控"]],
+      ["共赢票据管理", "ticket", ["订单管理", "票据管理", "票据展期管理", "询价管理"]],
+      ["机构系统设置", "gear", ["帮助中心", "机构参数", "日历配置"]],
+      ["清算管理", "check", ["共赢票据成交对账", "共赢票据持仓对账", "共赢票据清算"]],
+      ["投资者管理", "user", ["开户审核", "投资者信息"]],
+      ["资金管理", "card", ["出金", "汇率", "票据转外部", "入金", "透支额度管理", "外部转票据", "BizCode管理"]],
+      ["常驻工作进程", "cpu", ["工作进程配置", "实例与分片监控", "操作记录"]],
+      ["计划任务中心", "clock", ["计划任务配置", "任务运行记录"]],
+      ["通知和告警管理", "mail", ["通知设置", "邮件发送记录", "短信发送记录", "飞书发送记录"]],
+    ],
+    trs: [
+      ["报表管理", "chart", ["TRS 报表"]],
+      ["财务管理", "coin", ["收取利息"]],
+      ["对账管理", "scale", ["成交对账", "持仓对账"]],
+      ["风险监控", "shield", ["成交数据统计", "股票融资额度监控", "平台风险监控", "通道风险监控", "用户风险监控", "用户融资额度管理"]],
+      ["清算管理", "check", ["公司行动", "清算"]],
+      ["通道管理", "sliders", ["通道连接", "通道账户"]],
+      ["系统设置", "gear", ["交易补单", "交易下单", "算法下单", "系统参数", "下单风控设置", "交易日历"]],
+      ["用户管理", "user", ["成交查询", "清算群组管理", "委托查询", "用户通道优先级", "用户信息"]],
+      ["资金管理", "card", ["出金", "汇率", "亏损交收", "流水 bizcode 维护", "入金"]],
+      ["常驻工作进程", "cpu", ["工作进程配置", "实例与分片监控", "操作记录"]],
+      ["计划任务中心", "clock", ["计划任务配置", "任务运行记录"]],
+      ["通知和告警管理", "mail", ["通知设置", "邮件发送记录", "短信发送记录", "飞书发送记录"]],
+    ],
+  };
+
+  const sysName = { issue: "发行管理系统", sales: "销售管理系统", trs: "TRS管理系统" };
+  const tradeDays = [["营业日", "2026-09-25"], ["A股交易日 (QFII)", "2026-09-25"], ["A股交易日 (深沪港通)", "2026-09-25"]];
+
+  /* ---------- 发行端 · 订单管理(issue_frontend/src/views/issue/order/index.vue) ---------- */
+  const issueSearch = [
+    { label: "主订单编号", type: "input", ph: "主订单编号" },
+    { label: "子订单编号", type: "input", ph: "子订单编号" },
+    { label: "订单方向", type: "select", ph: "全部" },
+    { label: "订单状态", type: "select", ph: "全部", multiple: true },
+    { label: "票据类型", type: "select", ph: "全部" },
+    { label: "操作类型", type: "select", ph: "全部", multiple: true },
+    { label: "处理方式", type: "select", ph: "全部" },
+    { label: "投资者", type: "input", ph: "投资者编号/账户/名称" },
+    { label: "机构名称", type: "select", ph: "全部" },
+    { label: "证券代码/名称", type: "input", ph: "证券代码/名称" },
+    { label: "交易日期", type: "daterange", value: ["2026-09-25", "2026-09-25"], sep: "~" },
+  ];
+  const issueActions = [["查找", "primary", "search"], ["重置", "", "refresh"], ["数据导出", "", "download"]];
+
+  // merge: 按主订单跨行合并;group: 激进套用的列分组
+  const issueColumns = [
+    { key: "no", title: "主订单编号", w: 200, merge: true, fixed: true, mono: true, group: "订单" },
+    { key: "direction", title: "订单方向", w: 90, tag: "direction", merge: true, group: "订单" },
+    { key: "op", title: "操作类型", w: 100, tag: "op_type", merge: true, group: "订单" },
+    { key: "mode", title: "处理方式", w: 90, tag: "process_mode", merge: true, group: "订单" },
+    { key: "hedge", title: "对冲进度", w: 90, tag: "hedge_progress", merge: true, group: "订单" },
+    { key: "status", title: "订单状态", w: 90, tag: "note_order_status", group: "订单" },
+    { key: "noteType", title: "票据类型", w: 90, tag: "note_type", group: "订单" },
+    { key: "invNo", title: "投资者编号", w: 150, mono: true, group: "投资者" },
+    { key: "invAcct", title: "投资者账户", w: 120, mono: true, group: "投资者" },
+    { key: "invName", title: "投资者名称", w: 110, group: "投资者" },
+    { key: "fundAcct", title: "资金账户", w: 150, mono: true, group: "投资者" },
+    { key: "org", title: "机构名称", w: 160, group: "投资者" },
+    { key: "stock", title: "证券代码/名称", w: 200, group: "标的与条款" },
+    { key: "noteDir", title: "票据方向", w: 90, group: "标的与条款" },
+    { key: "period", title: "周期", w: 90, group: "标的与条款" },
+    { key: "notional", title: "订单名义本金(交易币种)", w: 200, align: "right", num: true, group: "金额与成交" },
+    { key: "priceReq", title: "价格要求(交易币种)", w: 170, align: "center", priceReq: true, group: "金额与成交" },
+    { key: "execNotional", title: "执行名义本金(交易币种)", w: 200, align: "right", num: true, group: "金额与成交" },
+    { key: "avgPrice", title: "订单成交均价(交易币种)", w: 200, align: "right", num: true, group: "金额与成交" },
+    { key: "execQty", title: "执行订单股数", w: 170, align: "right", num: true, group: "金额与成交" },
+    { key: "hedgeQty", title: "对冲股数", w: 110, align: "right", num: true, group: "金额与成交" },
+    { key: "tradeCcy", title: "交易币种", w: 90, group: "金额与成交" },
+    { key: "settleCcy", title: "结算币种", w: 90, group: "金额与成交" },
+    { key: "orderTime", title: "下单时间", w: 165, mono: true, group: "时间" },
+    { key: "tradeDate", title: "交易日", w: 110, mono: true, group: "时间" },
+    { key: "settleDate", title: "预计交收日", w: 110, mono: true, group: "时间" },
+    { key: "subNo", title: "子订单编号", w: 200, mono: true, group: "关联" },
+    { key: "noteNo", title: "关联主票据编号", w: 200, link: true, mono: true, group: "关联" },
+    { key: "subNoteNo", title: "关联子票据编号", w: 200, link: true, mono: true, group: "关联" },
+    { key: "source", title: "下单方式", w: 100, tag: "order_source", group: "关联" },
+    { key: "remark", title: "备注", w: 140, group: "关联" },
+  ];
+
+  const people = [
+    ["张明远", "明德资产管理"], ["李婉清", "远航财富"], ["王子豪", "明德资产管理"], ["陈思雨", "安衡证券"],
+    ["刘建国", "远航财富"], ["赵雅琳", "启原投资"], ["孙浩然", "明德资产管理"], ["周晓彤", "安衡证券"],
+    ["吴俊杰", "远航财富"], ["郑可欣", "启原投资"], ["黄一鸣", "安衡证券"], ["林书瑶", "远航财富"],
+  ];
+  const stocks = ["600519 贵州茅台", "300750 宁德时代", "000858 五粮液", "601318 中国平安", "002594 比亚迪", "600036 招商银行",
+    "601899 紫金矿业", "600900 长江电力", "688981 中芯国际", "000333 美的集团", "601012 隆基绿能", "600276 恒瑞医药"];
+  // [方向, 操作类型, 处理方式, 对冲进度, [子单状态...], 周期, 名义本金, 价格要求, 下单方式]
+  const plan = [
+    ["买入", "正常", "自动", "未开始", ["待处理"], "3M", 3000000, "限价:1,688.00", "CRM"],
+    ["买入", "正常", "人工", "对冲中", ["执行中"], "6M", 2000000, "限价:245.30", "IOS"],
+    ["卖出", "正常", "自动", "对冲中", ["执行中", "执行中"], "", 0, "市价", "CRM"],
+    ["买入", "正常", "自动", "已结束", ["确认中"], "3M", 1500000, "限价:52.18", "Android"],
+    ["卖出", "强平", "人工", "已结束", ["已完成"], "", 0, "市价", "发行端"],
+    ["买入", "正常", "自动", "已结束", ["已完成", "已完成"], "6M", 1000000, "限价:35.60", "CRM"],
+    ["买入", "正常", "自动", "未开始", ["待撤"], "1M", 800000, "限价:16.42", "H5"],
+    ["卖出", "到期", "自动", "已结束", ["已结算"], "", 0, "市价", "发行端"],
+    ["买入", "正常", "自动", "未开始", ["已拒绝"], "3M", 500000, "限价:88.20", "CRM"],
+    ["买入", "正常", "人工", "对冲中", ["撤单中"], "3M", 1200000, "限价:61.75", "IOS"],
+    ["卖出", "正常", "自动", "已结束", ["已完成", "已完成"], "", 0, "限价:1,692.50", "CRM"],
+    ["买入", "正常", "自动", "未开始", ["已撤"], "1M", 600000, "限价:21.06", "Android"],
+  ];
+  const fmt = (n, d = 2) => Number(n).toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
+  const issueRows = [];
+  plan.forEach((p, i) => {
+    const seq = String(31 - i).padStart(4, "0");
+    const no = "ORD00120260925" + seq;
+    const [name, org] = people[i];
+    const acct = String(1100151 - i);
+    const done = ["确认中", "已完成", "已结算"].includes(p[4][0]);
+    const price = p[7].startsWith("限价") ? Number(p[7].slice(3).replace(/,/g, "")) : [168.4, 245.1, 1690.2][i % 3];
+    const qty = [1700, 8100, 12400, 28700, 5200, 28000, 48700, 9300, 5600, 19400, 800, 28400][i];
+    p[4].forEach((st, j) => {
+      issueRows.push({
+        master: i, sub: j, span: j === 0 ? p[4].length : 0,
+        no, direction: p[0], op: p[1], mode: p[2], hedge: p[3], status: st,
+        noteType: j === 0 ? "Delta A" : "Delta B",
+        invNo: "INV00" + acct, invAcct: "HH1002" + String(3431 - i), invName: name, fundAcct: "FA00" + acct, org,
+        stock: stocks[i], noteDir: p[0] === "买入" ? "CALL" : "--", period: p[5] || "--",
+        notional: p[6] ? fmt(p[6]) : "--", priceReq: p[7],
+        execNotional: done && p[6] ? fmt(p[6] * 0.998) : "--",
+        avgPrice: done ? fmt(price) : "--", execQty: done ? fmt(qty, 0) : "--", hedgeQty: p[3] === "未开始" ? "--" : fmt(qty, 0),
+        tradeCcy: "CNY", settleCcy: "HKD",
+        orderTime: "2026-09-25 " + ["09:31:07", "09:42:55", "10:03:18", "10:15:42", "10:28:09", "10:47:31", "11:02:26", "13:05:44", "13:21:10", "13:48:37", "14:06:52", "14:33:15"][i],
+        tradeDate: "2026-09-25", settleDate: done ? "2026-09-29" : "--",
+        subNo: no + (j === 0 ? "A" : "B"),
+        noteNo: done || p[0] === "卖出" ? "NT00120260" + String(8120 + i * 7) : "--",
+        subNoteNo: done || p[0] === "卖出" ? "NT00120260" + String(8120 + i * 7) + (j === 0 ? "A" : "B") : "--",
+        source: p[8], remark: i === 4 ? "触发平仓线,风控发起" : i === 8 ? "额度不足" : "",
+      });
+    });
+  });
+
+  /* ---------- 发行端 · 订单详情抽屉(OrderDetailDrawer 及子组件),以一笔人工买入单为例 ---------- */
+  const issueDetail = {
+    title: "订单详情",
+    toolbar: "刷新数据",
+    master: {
+      title: "主订单信息",
+      buttons: [["转为自动处理", ""]],
+      fields: [
+        ["主订单编号", "ORD001202609250030", "mono"], ["订单方向", "买入", "tag:direction"], ["操作类型", "正常", "tag:op_type"], ["处理方式", "人工", "tag:process_mode"],
+        ["证券代码/名称", "300750 宁德时代"], ["票据方向", "CALL"], ["周期", "6M"], ["价格要求", "限价:245.30", "priceReq"],
+        ["交易币种", "CNY"], ["结算币种", "HKD"], ["下单时间", "2026-09-25 09:42:55", "mono"], ["状态", "执行中", "tag:note_order_status"],
+        ["对冲进度", "对冲中", "tag:hedge_progress"], ["关联主票据编号", "--", "span2"],
+      ],
+    },
+    subA: {
+      title: "A类订单信息",
+      fields: [
+        ["子订单编号", "ORD001202609250030A", "mono"], ["订单状态", "执行中", "tag:note_order_status"], ["交易日", "2026-09-25", "mono"], ["预计交收日", "2026-09-29", "mono"],
+        ["投资者编号", "INV001100150", "mono"], ["投资者账户", "HH10023430", "mono"], ["投资者名称", "李婉清"], ["资金账户", "FA001100150", "mono"],
+        ["机构名称", "远航财富"], ["订单名义本金", "2,000,000.00 CNY", "num"], ["报价", "12.50%", "num"], ["A类收益比例", "80.00%", "num"],
+        ["票据面值", "273,437.50 HKD", "num"], ["发行费用", "2,734.38 HKD", "num"], ["票据发行价", "276,171.88 HKD", "num"],
+      ],
+    },
+    hedge: {
+      title: "对冲执行",
+      buttons: [["直接下单", "success"], ["补录", "success"], ["完成对冲", "primary"]],
+      refresh: "刷新",
+      suggest: "对冲建议:使用TRS系统 hedge01/发行对冲一号-880012 对冲,建议建仓比例(100.00%),建议建仓股数(8,100股)",
+      suggestParts: { system: "TRS", account: "hedge01/发行对冲一号-880012", ratio: "100.00%", qty: "8,100股" },
+      columns: [
+        ["操作", 110], ["对冲类型", 100], ["对冲系统", 100], ["对冲账号", 130], ["资金号", 110], ["下单类型", 110], ["委托编号", 180], ["状态", 168], ["方向", 80],
+        ["委托股数", 110, "r"], ["委托价格(交易币种)", 180, "r"], ["成交股数", 110, "r"], ["成交均价(交易币种)", 180, "r"], ["发生金额(交易币种)", 180, "r"], ["发生金额(结算币种)", 180, "r"],
+      ],
+      rows: [
+        { ops: [["撤单", "danger"]], type: "直接下单", sys: "TRS", acct: "hedge01", fund: "880012", otype: "算法TWAP", no: "ALG202609250007", status: ["执行中", "primary", "已成(执行中)"], dir: "买入", qty: "5,000", price: "245.30", fqty: "3,200", fprice: "245.12", amt: "784,384.00 CNY", samt: "858,429.85 HKD" },
+        { ops: [], type: "直接下单", sys: "TRS", acct: "hedge01", fund: "880012", otype: "普通单", no: "T2026092500418", status: ["已成", "success"], dir: "买入", qty: "2,000", price: "245.20", fqty: "2,000", fprice: "245.18", amt: "490,360.00 CNY", samt: "536,649.98 HKD" },
+        { ops: [["修改", "primary"], ["删除", "danger"]], type: "补录", sys: "自定义", acct: "--", fund: "--", otype: "普通单", no: "--", status: ["已成", "success"], dir: "买入", qty: "--", price: "--", fqty: "1,100", fprice: "245.26", amt: "269,786.00 CNY", samt: "295,253.80 HKD" },
+      ],
+      summary: [
+        ["累计有效委托数量", "8,100"], ["累计成交数量", "6,300"], ["对冲含费成交均价", "245.2112 CNY"], ["对冲比例", "77.78%"],
+        ["累计发生金额(交易币种)", "1,544,530.00 CNY"], ["累计发生金额(结算币种)", "1,690,333.63 HKD"], ["结算汇率", "1.0944"],
+      ],
+    },
+  };
+
+  /* ---------- 发行端 · 补录弹窗(HedgeFillDialog.vue),对冲系统选「自定义」 ---------- */
+  const fill = {
+    title: "补录",
+    common: [
+      { label: "交易日", type: "select", value: "2026-09-25", ph: "请选择" },
+      { label: "对冲系统", type: "select", value: "自定义" },
+    ],
+    custom: [
+      { label: "方向", type: "tag", value: "买入", tag: "direction" },
+      { label: "成交股数", type: "amount", value: "1,100", unit: "股" },
+      { label: "成交均价", type: "amount", value: "245.260000", unit: "CNY" },
+      { label: "发生金额(交易币种)", type: "text", value: "269,786.00", unit: "CNY" },
+      { label: "结算币种", type: "text", value: "HKD" },
+      { label: "发生金额(结算币种)", type: "amount", value: "", ph: "请输入", unit: "HKD", error: "请输入发生金额(HKD)" },
+    ],
+    trs: [
+      { label: "对冲账号", type: "select", value: "hedge01" },
+      { label: "资金号", type: "select", value: "880012", ph: "请选择" },
+      { label: "方向", type: "tag", value: "买入", tag: "direction" },
+      { label: "下单类型", type: "select", value: "普通单" },
+      { label: "委托编号/算法母单编号", type: "input", value: "", ph: "请输入委托编号" },
+    ],
+    footer: [["确定", "primary"]],
+  };
+
+  /* ---------- TRS · 下单详情页 + 下单弹窗(trs/config/order-place/detail) ---------- */
+  const trsTrade = {
+    host: {
+      title: "下单详情", account: "ceshi1", user: "李四测",
+      accountCols: ["币种", "可用金额", "冻结金额", "持仓市值", "总资产", "应付利息(汇总)", "净资产", "预警线", "平仓线"],
+      accountRows: [["HKD", "1,286,430.52", "127,760.00", "2,418,905.00", "3,833,095.52", "1,204.66", "3,831,890.86", "130.00%", "110.00%"]],
+      posCols: [["股票代码", 100], ["股票名称", 110], ["交易渠道", 100], ["持仓数量", 110, "r"], ["可用数量", 110, "r"], ["参考成本价", 110, "r"], ["最新价格", 100, "r"],
+        ["持仓市值(交易币种)", 170, "r"], ["结算币种", 90], ["持仓市值(结算币种)", 170, "r"], ["浮动盈亏(结算币种)", 170, "r"], ["盈亏比例", 100, "r"]],
+      posRows: [
+        ["000007", "全新好", "QFII", "12,000", "12,000", "11.20", "11.64", "139,680.00", "HKD", "152,865.79", "+5,778.43", "+3.93%"],
+        ["600519", "贵州茅台", "深沪港通", "800", "500", "1,702.40", "1,688.00", "1,350,400.00", "HKD", "1,477,877.76", "-12,607.49", "-0.85%"],
+        ["300750", "宁德时代", "QFII", "3,000", "3,000", "238.15", "245.30", "735,900.00", "HKD", "805,368.96", "+23,474.88", "+3.00%"],
+      ],
+      orderCols: [["委托编号", 180], ["股票代码", 100], ["股票名称", 110], ["交易渠道", 100], ["委托时间", 160], ["委托方向", 90], ["委托数量", 110, "r"], ["委托价格", 100, "r"],
+        ["成交数量", 110, "r"], ["成交价格", 100, "r"], ["委托状态", 100], ["备注", 140]],
+      orderRows: [
+        ["T2026092500431", "000007", "全新好", "QFII", "2026-09-25 10:42:16", "买入", "2,000", "11.62", "0", "--", "已报", ""],
+        ["T2026092500418", "300750", "宁德时代", "QFII", "2026-09-25 10:15:03", "买入", "2,000", "245.20", "2,000", "245.18", "已成", ""],
+        ["T2026092500377", "600519", "贵州茅台", "深沪港通", "2026-09-25 09:48:51", "卖出", "300", "1,690.00", "100", "1,690.00", "部成", ""],
+      ],
+    },
+    dialog: {
+      account: "ceshi1", user: "李四测",
+      strip: [["可用余额", "1,286,430.52", "HKD"], ["最大可买股数", "100,900", "股"]],
+      channels: ["QFII", "深沪港通"],
+      stock: { code: "000007", name: "全新好", mkt: "SZ" },
+      price: "11.65", priceCcy: "CNY", quick: [["卖一", "11.65"], ["最新", "11.64"], ["买一", "11.64"]],
+      qty: "1000", qtyQuick: ["1/4", "1/3", "1/2", "全部可买"], qtyHelp: "买入须为 100 股整数倍",
+      amounts: [["买入金额(交易币种)", "11,650.00", "CNY"], ["买入金额(结算币种)", "12,749.76", "HKD"]],
+      quote: {
+        last: "11.64", chg: "0.03", pct: "0.26%", prev: "11.61", source: "实时推送", hint: "点击任一档位填入委托价",
+        asks: [["卖五", "11.70", 254], ["卖四", "11.69", 20], ["卖三", "11.67", 16], ["卖二", "11.66", 38], ["卖一", "11.65", 20]],
+        bids: [["买一", "11.64", 102], ["买二", "11.60", 11], ["买三", "11.59", 22], ["买四", "11.57", 5], ["买五", "11.56", 50]],
+      },
+      footHint: "提交后可在「当日委托」中查看与撤单",
+      footer: [["取消", ""], ["确认买入 1,000 股", "primary"]],
+    },
+  };
+
+  /* ---------- TRS · 委托查询(trs/order/index.vue) ---------- */
+  const trsSearch = [
+    { label: "用户ID", type: "input", ph: "精确搜索" }, { label: "手机号", type: "input", ph: "精确搜索" }, { label: "委托编号", type: "input", ph: "精确搜索" },
+    { label: "账户", type: "input", ph: "模糊搜索" }, { label: "用户名称", type: "input", ph: "模糊搜索" }, { label: "股票代码/名称", type: "input", ph: "模糊搜索" },
+    { label: "委托状态", type: "select", ph: "全部", multiple: true }, { label: "下单类型", type: "select", ph: "全部" }, { label: "下单方式", type: "select", ph: "全部" },
+    { label: "委托日期", type: "daterange", value: ["2026-09-25", "2026-09-25"], sep: "至" },
+  ];
+  const trsActions = [["查找", "primary", "search"], ["重置", "", "refresh"], ["导出", "", "download"]];
+
+  /* ---------- 销售端 · 新建投资者(sales/investor/components/InvestorCreateDialog.vue) ---------- */
+  const salesForm = {
+    title: "新建投资者",
+    mode: "手动新建",
+    host: { title: "投资者信息", button: "新建投资者" },
+    groups: [
+      {
+        title: "基础信息", cols: 2,
+        fields: [
+          { label: "投资者账户", type: "input", req: true, value: "lwq_2026", ph: "请输入投资者账户" },
+          { label: "投资者类别", type: "select", req: true, value: "PI", ph: "请选择" },
+          { label: "投资者名称", type: "input", req: true, value: "李婉清", ph: "请输入投资者名称" },
+          { label: "投资者英文名", type: "input", req: true, value: "LI WANQING", ph: "请输入投资者英文名" },
+          { label: "E-mail", type: "input", req: true, value: "wanqing.li@", ph: "请输入E-mail", error: "邮箱格式不正确" },
+          { label: "手机号码", type: "combo", req: true, parts: [{ w: 92, value: "+86", ph: "区号" }, { value: "13800138000", ph: "请输入手机号码" }] },
+          { label: "联系地址", type: "combo", req: true, parts: [{ w: 150, value: "CN", ph: "国别代码" }, { value: "", ph: "请输入详细地址" }] },
+          { label: "证件号码", type: "combo", req: true, parts: [{ w: 150, value: "", ph: "证件类型", select: true }, { value: "", ph: "请输入证件号码" }] },
+          { label: "投资者状态", type: "select", req: true, value: "正常", ph: "请选择" },
+          { label: "状态说明", type: "input", value: "", ph: "请输入状态说明" },
+          { label: "A.E. Code", type: "input", value: "", ph: "请输入A.E. Code" },
+        ],
+      },
+      {
+        title: "问卷信息", cols: 2,
+        fields: [
+          { label: "投资者风险等级", type: "select", req: true, value: "3-增长型", ph: "请选择" },
+          { label: "投资者年龄", type: "select", req: true, value: "35~50岁", ph: "请选择" },
+          { label: "投资年限", type: "select", req: true, value: "5-10年", ph: "请选择" },
+          { label: "投资者目标", type: "select", req: true, value: "", ph: "请选择" },
+          { label: "场外衍生品经验", type: "select", req: true, value: "", ph: "请选择" },
+          { label: "资产净值(HKD)", type: "amount", req: true, value: "", ph: "请输入大于0的数,最多2位小数" },
+          { label: "问卷有效期", type: "date", req: true, value: "", ph: "请选择日期" },
+          { label: "衍生产品问卷", type: "select", req: true, value: "", ph: "请选择" },
+        ],
+      },
+      {
+        title: "交易品类", cols: 1,
+        fields: [{ label: "可交易品类", type: "checks", req: true, options: [["共赢票据(A类)", false], ["共赢票据(B类)", false]] }],
+        alert: "未启用共赢票据(A类),请确认该投资者当前无需A类交易权限。",
+      },
+      {
+        title: "其它信息", cols: 1,
+        uploads: [
+          { label: "开户文件", files: [["开户申请表-李婉清.pdf", "2.4M"]] },
+          { label: "其它附件", files: [] },
+        ],
+        uploadBtn: "上传附件", uploadTip: "PNG、JPEG、PDF;单个30M;单次100M;最多20个",
+      },
+    ],
+    footer: [["取消", ""], ["保存", "primary"]],
+  };
+
+
+  // 委托查询表格列(trs/order/index.vue)。align 为代码现状;group 为激进套的分组
+  const trsColumns = [
+    { key: "uid", title: "用户ID", w: 90, group: "用户" }, { key: "acct", title: "账户", w: 120, group: "用户" },
+    { key: "uname", title: "用户名称", w: 110, group: "用户" }, { key: "phone", title: "手机号", w: 120, group: "用户" },
+    { key: "channel", title: "交易渠道", w: 100, tag: "trade_channel", group: "委托" },
+    { key: "code", title: "股票代码", w: 100, group: "委托" }, { key: "sname", title: "股票名称", w: 110, group: "委托" },
+    { key: "dir", title: "委托方向", w: 90, tag: "direction", group: "委托" }, { key: "ptype", title: "报价方式", w: 90, tag: "price_req_type", group: "委托" },
+    { key: "status", title: "委托状态", w: 180, tag: "trs_order_status", group: "委托" },
+    { key: "qty", title: "委托数量", w: 110, num: true, group: "委托" }, { key: "price", title: "委托价格", w: 100, num: true, group: "委托" },
+    { key: "otime", title: "委托时间", w: 160, group: "委托" },
+    { key: "fqty", title: "成交数量", w: 110, num: true, group: "成交" }, { key: "fprice", title: "成交价格", w: 100, num: true, group: "成交" },
+    { key: "ccy", title: "交易币种", w: 90, group: "成交" }, { key: "famt", title: "成交金额", w: 130, num: true, group: "成交" },
+    { key: "ftime", title: "成交时间", w: 160, group: "成交" },
+    { key: "comm", title: "佣金", w: 100, num: true, group: "费用" }, { key: "stamp", title: "印花税", w: 100, num: true, group: "费用" },
+    { key: "fee", title: "规费", w: 100, num: true, group: "费用" }, { key: "total", title: "交易总费用", w: 110, num: true, group: "费用" },
+    { key: "amt", title: "发生金额(交易币种)", w: 170, num: true, group: "结算" }, { key: "sccy", title: "结算币种", w: 90, group: "结算" },
+    { key: "samt", title: "发生金额(结算币种)", w: 170, num: true, group: "结算" },
+    { key: "no", title: "委托编号", w: 200, group: "来源" }, { key: "operator", title: "操作方", w: 120, group: "来源" },
+    { key: "otype", title: "下单类型", w: 100, tag: "order_type", group: "来源" }, { key: "omode", title: "下单方式", w: 100, tag: "order_mode", group: "来源" },
+    { key: "ip", title: "IP", w: 130, group: "来源" }, { key: "lbs", title: "LBS", w: 130, group: "来源" },
+    { key: "vpn", title: "是否用VPN", w: 100, group: "来源" }, { key: "remark", title: "备注", w: 140, group: "来源" },
+  ];
+  // [用户, 渠道, 股票, 方向, 报价, 状态, 数量, 价格, 成交数量, 成交价, 下单类型, 下单方式, 子单数]
+  const trsPlan = [
+    [0, "QFII", 0, "买入", "限价", "已报", 2000, 11.62, 0, 0, "正常下单", "iOS", 0],
+    [1, "QFII", 1, "买入", "限价", "已成", 2000, 245.2, 2000, 245.18, "后台下单", "CRM", 2],
+    [2, "深沪港通", 2, "卖出", "限价", "部成", 300, 1690, 100, 1690, "正常下单", "Android", 0],
+    [3, "QFII", 3, "买入", "市价", "已成", 5000, 52.2, 5000, 52.18, "算法下单", "OpenAPI", 0],
+    [4, "深沪港通", 4, "卖出", "限价", "待撤", 1200, 35.6, 0, 0, "正常下单", "Windows", 0],
+    [5, "QFII", 5, "买入", "限价", "已撤", 800, 16.42, 0, 0, "正常下单", "iOS", 0],
+    [0, "QFII", 6, "卖出", "限价", "废单", 600, 88.2, 0, 0, "后台平仓", "CRM", 0],
+    [1, "深沪港通", 7, "买入", "限价", "部撤", 3000, 61.75, 1200, 61.74, "正常下单", "H5", 0],
+    [2, "QFII", 8, "买入", "限价", "已成", 1000, 21.06, 1000, 21.05, "条件单", "Android", 0],
+    [3, "QFII", 9, "卖出", "市价", "已成", 400, 245.3, 400, 245.31, "后台补单", "CRM", 0],
+  ];
+  const trsUsers = [["10021", "ceshi1", "李四测"], ["10034", "zhangmy", "张明远"], ["10058", "wangzh", "王子豪"], ["10077", "chensy", "陈思雨"], ["10092", "liujg", "刘建国"], ["10105", "zhaoyl", "赵雅琳"]];
+  const trsStocks = [["000007", "全新好"], ["300750", "宁德时代"], ["600519", "贵州茅台"], ["601318", "中国平安"], ["600036", "招商银行"], ["601899", "紫金矿业"], ["688981", "中芯国际"], ["000333", "美的集团"], ["601012", "隆基绿能"], ["002594", "比亚迪"]];
+  const trsRows = [];
+  trsPlan.forEach((p, i) => {
+    const u = trsUsers[p[0]], s = trsStocks[p[2]], t = ["10:42:16", "10:15:03", "09:48:51", "09:41:27", "09:38:02", "09:35:44", "09:33:10", "09:31:58", "09:31:05", "09:30:12"][i];
+    const famt = p[8] * p[9], comm = famt * 0.0003, stamp = p[3] === "卖出" ? famt * 0.0005 : 0, fee = famt * 0.0000687, total = comm + stamp + fee;
+    const amt = famt ? (p[3] === "买入" ? -(famt + total) : famt - total) : 0;
+    const mk = (sub, qty, fq) => ({
+      sub, kids: sub ? 0 : p[12], uid: u[0], acct: u[1], uname: u[2], phone: "1380000" + String(1001 + p[0]), channel: p[1],
+      code: s[0], sname: s[1], dir: p[3], ptype: p[4], status: p[5], qty: fmt(qty, 0), price: fmt(p[7]), otime: "2026-09-25 " + t,
+      fqty: fmt(fq, 0), fprice: fq ? fmt(p[9]) : "--", ccy: "CNY", famt: fq ? fmt(fq * p[9]) : "--", ftime: fq ? "2026-09-25 " + t.slice(0, 6) + "59" : "--",
+      comm: fq ? fmt(comm * fq / (p[8] || 1)) : "--", stamp: fq ? fmt(stamp * fq / (p[8] || 1)) : "--", fee: fq ? fmt(fee * fq / (p[8] || 1)) : "--", total: fq ? fmt(total * fq / (p[8] || 1)) : "--",
+      amt: fq ? fmt(amt * fq / (p[8] || 1)) : "--", sccy: sub ? "" : "HKD", samt: fq ? fmt(amt * fq / (p[8] || 1) * 1.0944) : "--",
+      no: "T20260925" + String(431 - i * 9).padStart(5, "0") + (sub ? "-" + sub : ""), operator: sub ? "" : p[10] === "正常下单" ? "--" : "admin",
+      otype: sub ? "" : p[10], omode: sub ? "" : p[11], ip: "203.0.113." + (20 + i), lbs: ["上海", "深圳", "香港", "北京"][i % 4], vpn: i === 3 ? "是" : "否",
+      remark: p[5] === "废单" ? "超出持仓可卖数量" : "", route: sub ? "通道" + sub : "",
+      cancel: p[5] === "待撤" ? ["撤单结果未知", "danger", "上轮撤单未取得明确结果,可人工再次撤单"] : null,
+    });
+    trsRows.push(mk(0, p[6], p[8]));
+    for (let k = 1; k <= p[12]; k++) trsRows.push(mk(k, p[6] / p[12], p[8] / p[12]));
+  });
+
+  /* ---------- 销售端 · 订单管理(sales/order/components/OrderSearchForm.vue、OrderTable.vue,management 模式) ---------- */
+  const salesSearch = [
+    { label: "主订单编号", type: "input", ph: "主订单编号" },
+    { label: "子订单编号", type: "input", ph: "子订单编号" },
+    { label: "订单方向", type: "select", ph: "全部" },
+    { label: "操作类型", type: "select", ph: "全部", multiple: true },
+    { label: "订单状态", type: "select", ph: "全部", multiple: true },
+    { label: "票据类型", type: "select", ph: "全部", multiple: true },
+    { label: "投资者", type: "input", ph: "编号/账户/名称" },
+    { label: "证券代码/名称", type: "input", ph: "证券代码/名称" },
+    { label: "交易日期", type: "daterange", value: ["2026-09-25", "2026-09-25"], sep: "~" },
+  ];
+  const salesStatuses = ["未报", "待报", ...Object.keys(dict.note_order_status)];
+  const SW = { direction: 95, op: 90, invAcct: 110, invName: 100, period: 70, execQty: 120, tradeCcy: 85, settleCcy: 85, tradeDate: 105, settleDate: 105, source: 90 };
+  const salesColumns = issueColumns.filter((c) => !["mode", "hedge", "org", "hedgeQty"].includes(c.key)).map((c) => ({
+    ...c, w: SW[c.key] || c.w, merge: ["no", "direction", "op"].includes(c.key),
+    title: c.key === "execQty" ? "订单执行股数" : c.title, tag: c.key === "source" ? "sales_order_source" : c.tag,
+  }));
+  const srcMap = { CRM: "销售端", IOS: "iOS", Android: "Android", H5: "H5", 发行端: "销售端" };
+  const salesRows = issueRows.map((r) => ({ ...r, source: srcMap[r.source] }));
+
+  /* ---------- 首页 ---------- */
+  // 保守套:现有首页(views/dashboard/index.vue)的内容,文案为代码原文
+  // 激进套:工作台。数字是示例值,上线需要后端提供按状态汇总的接口;字段全部来自订单管理
+  const home = {
+    greeting: "上午好,admin!", weather: "今日天气晴朗,气温在15℃至25℃之间,东南风。",
+    tradeDate: "2026-09-25", total: 128,
+    status: [["待处理", 6, true], ["执行中", 14], ["待撤", 2, true], ["撤单中", 1], ["确认中", 9, true], ["已完成", 61], ["已结算", 27], ["已撤", 5], ["已拒绝", 3]],
+    manualCols: [["主订单编号", 176], ["方向", 60], ["订单状态", 92], ["对冲进度", 92], ["投资者", 88], ["证券代码/名称", 148], ["订单名义本金", 128, "r"], ["下单时间", 150]],
+    shortcuts: [["订单管理", "ticket", "共赢票据管理"], ["票据管理", "file", "共赢票据管理"], ["询价管理", "search", "共赢票据管理"], ["共赢票据风险监控", "shield", "风控管理"], ["共赢票据清算", "check", "清算管理"], ["日常报表", "chart", "报表管理"]],
+  };
+
+  return { dict, menus, sysName, tradeDays, salesSearch, salesStatuses, salesColumns, salesRows, home, issueSearch, issueActions, issueColumns, issueRows, issueDetail, fill, trsTrade, trsSearch, trsActions, trsColumns, trsRows, salesForm };
+})();

+ 295 - 0
src/data2.js

@@ -0,0 +1,295 @@
+/* 完整设计稿补充的内容数据(data.js 之外)。字段、文案取自三端前端代码;表内数值是示例。
+ * 来源标注在各段注释里。修改后执行:python3 build.py
+ */
+window.SPEC_DATA2 = (function () {
+  const D = window.SPEC_DATA;
+
+  // 字典补充(ddl/issue/public/数据字典/seed.sql):票据状态、票据风控状态;变动类型按 DictCode 名称示意
+  const dict = {
+    note_status: { 待生效: "info", 存续期: "success", 已卖出: "info", 已到期: "warning" },
+    note_risk_status: { 正常: "success", 预警: "warning", 危险: "danger", 强平: "danger" },
+    sec_market: { A股: "primary" },
+    note_change_type: { 发行: "primary", 调整: "warning", 卖出: "danger", 展期: "info", 分红: "success" },
+    clearing_step: { 待执行: "info", 执行中: "primary", 执行成功: "success", 跳过: "info", 执行失败: "danger", 停止中: "warning", 已停止: "warning" },
+    clearing_batch: { 执行中: "primary", 已完成: "success", 失败: "danger", 已停止: "warning" },
+    send_status: { 已发送: "success", 发送失败: "danger", 待发送: "info" },
+    gender: { 男: "info", 女: "info" },
+  };
+
+  /* ---------- 发行端 · 票据详情抽屉(NoteDetailDrawer + note-detail/*Panel),以 300750 宁德时代的一张存续期票据为例 ---------- */
+  const noteDetail = {
+    title: "票据详情",
+    noteNo: "NT001202608127", subNo: "NT001202608127A",
+    tags: [["存续期", "note_status"], ["正常", "note_risk_status"], ["Delta A", "note_type"]],
+    facts: [["证券", "300750 宁德时代"], ["投资者", "李婉清"], ["机构", "远航财富"], ["到期日期", "2027-03-29"]],
+    buttons: [["修改到期日", ""], ["票据调整", ""], ["添加分红", ""], ["票据卖出", "dng"]],
+    tabs: [["main", "主页"], ["changes", "变动记录"]],
+    master: {
+      title: "主票据信息",
+      fields: [["主票据编号", "NT001202608127", "mono"], ["证券市场", "A股", "tag:sec_market"], ["证券代码/名称", "300750 宁德时代"], ["票据方向", "CALL"],
+        ["发行日期", "2026-09-29", "mono"], ["到期日期", "2027-03-29", "mono"], ["交易币种", "CNY"], ["结算币种", "HKD"]],
+    },
+    subA: {
+      title: "A类票据信息",
+      fields: [["票据编号", "NT001202608127A", "mono"], ["票据类型", "Delta A", "tag:note_type"], ["票据状态", "存续期", "tag:note_status"], ["风险状态", "正常", "tag:note_risk_status"],
+        ["投资者编号", "INV001100150", "mono"], ["投资者账户", "HH10023430", "mono"], ["投资者名称", "李婉清"], ["资金账户", "FA001100150", "mono"],
+        ["机构名称", "远航财富"], ["报价", "12.50%", "num"], ["A类收益比例", "80.00%", "num"]],
+      init: {
+        title: "初始信息",
+        fields: [["票据名义本金(交易币种)", "2,000,000.00 CNY", "num"], ["票据名义本金(结算币种)", "2,188,800.00 HKD", "num"], ["票据股票数量", "8,100", "num"], ["票据期初价格", "245.26 CNY", "num"],
+          ["初始汇率", "1.0944", "num"], ["票据面值", "273,437.50 HKD", "num"], ["发行费用", "2,734.38 HKD", "num"], ["票据发行价", "276,171.88 HKD", "num"]],
+      },
+      current: {
+        title: "当前信息",
+        fields: [["票据名义本金", "2,000,000.00 CNY", "num"], ["冻结名义本金", "0.00 CNY", "num"], ["票据面值", "273,437.50 HKD", "num"], ["票据股票数量", "8,100", "num"],
+          ["票据期初价格", "245.26 CNY", "num"], ["最新价", "251.80 CNY", "num"], ["账面汇率", "1.0951", "num"], ["增额金额", "0.00 HKD", "num"],
+          ["分红金额", "0.00 HKD", "num"], ["预警/危险/强平价格", "220.73 / 208.47 / 196.21 CNY", "num"], ["预警缓冲比/强平比例", "10.00% / 20.00%", "num"],
+          ["底层资产浮动盈亏及比例", "+58,002.47 HKD / +2.67%", "num:up"], ["A类票据浮动盈亏及比例", "+46,401.98 HKD / +16.97%", "num:up"]],
+      },
+    },
+    hedge: {
+      title: "对冲信息", ratioLabel: "对冲仓位比例", ratio: "100.00%",
+      posTitle: "持仓信息",
+      posCols: [["对冲系统", 90], ["对冲账号", 110], ["资金号", 100], ["持仓股数", 110, "r"], ["冻结股数", 100, "r"], ["可平仓数量", 110, "r"], ["最新价(交易币种)", 160, "r"],
+        ["持仓市值(交易币种)", 180, "r"], ["账面汇率", 100, "r"], ["持仓市值(结算币种)", 180, "r"], ["持仓成本金额(结算币种)", 200, "r"], ["浮动盈亏(结算币种)", 180, "r"],
+        ["分红金额(结算币种)", 180, "r"], ["已实现分红金额(结算币种)", 220, "r"], ["未结算日常盈亏(结算币种)", 220, "r"], ["已结算实现盈亏(结算币种)", 220, "r"], ["合计盈亏(结算币种)", 180, "r"]],
+      posRows: [
+        ["TRS", "hedge01", "880012", "8,100", "0", "8,100", "251.80 CNY", "2,039,580.00 CNY", "1.0951", "2,233,544.06 HKD", "2,188,800.00 HKD", "44,744.06 HKD", "0.00 HKD", "0.00 HKD", "0.00 HKD", "0.00 HKD", "44,744.06 HKD"],
+        ["合计", "", "", "8,100", "0", "8,100", "", "2,039,580.00 CNY", "", "2,233,544.06 HKD", "2,188,800.00 HKD", "44,744.06 HKD", "0.00 HKD", "0.00 HKD", "0.00 HKD", "0.00 HKD", "44,744.06 HKD"],
+      ],
+      tradeTitle: "票据买卖对冲记录信息",
+      tradeCols: [["对冲系统", 90], ["对冲账号", 110], ["资金号", 100], ["对冲类型", 100], ["下单类型", 110], ["委托编号", 180], ["状态", 90], ["方向", 80],
+        ["委托股数", 110, "r"], ["委托价格(交易币种)", 180, "r"], ["成交股数", 110, "r"], ["成交均价(交易币种)", 180, "r"], ["发生金额(交易币种)", 180, "r"], ["发生金额(结算币种)", 180, "r"], ["交易日", 110]],
+      tradeRows: [
+        ["TRS", "hedge01", "880012", "直接下单", "算法TWAP", "ALG202609250007", ["已成", "success"], "买入", "5,000", "245.30", "5,000", "245.12", "1,225,600.00 CNY", "1,341,296.64 HKD", "2026-09-25"],
+        ["TRS", "hedge01", "880012", "直接下单", "普通单", "T2026092500418", ["已成", "success"], "买入", "2,000", "245.20", "2,000", "245.18", "490,360.00 CNY", "536,649.98 HKD", "2026-09-25"],
+        ["自定义", "--", "--", "补录", "普通单", "--", ["已成", "success"], "买入", "--", "--", "1,100", "245.26", "269,786.00 CNY", "295,253.80 HKD", "2026-09-25"],
+      ],
+      dailyTitle: "日常买卖对冲记录",
+      dailyButtons: [["下单", ""], ["补录", ""], ["刷新", "txt"]],
+      dailyRows: [
+        [[["撤单", "danger"]], "TRS", "hedge01", "880012", "直接下单", "普通单", "T2026100700122", ["已报", "primary"], "卖出", "500", "252.00", "0", "--", "--", "--", "2026-10-07"],
+        [[["修改", "primary"], ["删除", "danger"]], "自定义", "--", "--", "补录", "普通单", "--", ["已成", "success"], "买入", "--", "--", "300", "248.90", "74,670.00 CNY", "81,771.12 HKD", "2026-10-06"],
+      ],
+    },
+    changes: {
+      sections: [
+        { title: "票据变动记录", cols: [["变动类型", 110], ["关联编号", 170], ["变动后A类票据名义本金(交易币种)", 280, "r"], ["变动后A类票据股票数量", 190, "r"], ["变动后A类票据期初价格(交易币种)", 280, "r"], ["变动后持仓股数", 130, "r"], ["系统处理备注", 150], ["创建人", 110], ["创建时间", 165]],
+          rows: [[["发行", "note_change_type"], ["ORD001202609250030", "link"], "2,000,000.00 CNY", "8,100", "245.26 CNY", ["8,100", "link"], "--", "admin", "2026-09-25 16:02:11"]] },
+        { title: "分红变动记录", cols: [["变动类型", 120], ["变动后A类票据分红金额(结算币种)", 280, "r"], ["变动后对冲分红金额(结算币种)", 240, "r"], ["备注", 150], ["创建人", 110], ["创建时间", 165]], rows: [], empty: "暂无分红变动记录" },
+        { title: "增额变动记录", cols: [["变动类型", 120], ["变动后增额金额(结算币种)", 240, "r"], ["备注", 150], ["创建人", 110], ["创建时间", 165]], rows: [], empty: "暂无增额变动记录" },
+        { title: "展期记录", cols: [["展期任务编号", 180], ["原到期日", 120], ["新到期日", 120], ["状态", 100], ["操作人", 110], ["创建时间", 165]], rows: [], empty: "暂无展期记录" },
+      ],
+    },
+  };
+
+  /* ---------- 发行端 · 共赢票据清算(views/issue/clearing/index.vue,步骤名与代码 createPendingClearingSteps 逐字一致) ---------- */
+  const clearing = {
+    market: "A股", tradeDate: "2026-09-25", businessDate: "2026-09-25",
+    tabs: [["clearing", "清算"], ["logs", "清算日志"]],
+    batchStatus: "执行中", batchNo: "CLR20260925001",
+    stepCols: [["步骤序号", 72, "c"], ["步骤名称", 320, "", "wrap"], ["执行状态", 100, "c"], ["开始时间", 165], ["结束时间", 165], ["备注", 180, "", "wrap"], ["操作人", 90]],
+    steps: [
+      [1, "开启清算", "执行成功", "2026-09-25 17:30:00", "2026-09-25 17:30:02", "", "admin"],
+      [2, "备份清算前数据", "执行成功", "2026-09-25 17:30:02", "2026-09-25 17:31:48", "备份 12 张表", "admin"],
+      [3, "订单及票据处理", "执行成功", "2026-09-25 17:31:48", "2026-09-25 17:33:10", "处理订单 128 笔、票据 96 张", "admin"],
+      [4, "交易交收处理", "执行中", "2026-09-25 17:33:10", "", "", "admin"],
+      [5, "备份清算完成的数据、生成营业日日终数据与核心日常报表、更新T+1实时数据", "待执行", "", "", "", ""],
+      [6, "确认交易日切换、推进营业日、票据激活、清算完成", "待执行", "", "", "", ""],
+    ],
+    blockedReason: "",
+    errorDetail: "",
+    logCols: [["步骤", 64, "c"], ["次数", 64, "c"], ["动作", 80], ["状态", 100], ["操作人", 100], ["开始时间", 165], ["结束时间", 165], ["明细", 260, "", "wrap"]],
+    logs: [
+      [4, 1, "执行", "执行中", "admin", "2026-09-25 17:33:10", "", "交收 A 类订单 41 笔,已完成 27 笔"],
+      [3, 1, "执行", "执行成功", "admin", "2026-09-25 17:31:48", "2026-09-25 17:33:10", "处理订单 128 笔、票据 96 张"],
+      [2, 1, "执行", "执行成功", "admin", "2026-09-25 17:30:02", "2026-09-25 17:31:48", "备份 12 张表"],
+      [1, 1, "执行", "执行成功", "admin", "2026-09-25 17:30:00", "2026-09-25 17:30:02", ""],
+    ],
+    historyTitle: "清算记录",
+    historyCols: [["清算批次号", 170], ["交易日", 110], ["类型", 90], ["状态", 100], ["操作人", 100], ["开始时间", 165], ["完成时间", 165], ["备注", 220, "", "wrap"]],
+    history: [
+      ["CLR20260925001", "2026-09-25", "正常清算", "执行中", "admin", "2026-09-25 17:30:00", "", ""],
+      ["CLR20260924001", "2026-09-24", "正常清算", "已完成", "admin", "2026-09-24 17:30:00", "2026-09-24 17:41:26", ""],
+      ["CLR20260923002", "2026-09-23", "重新清算", "已完成", "admin", "2026-09-23 19:02:14", "2026-09-23 19:13:40", "回拨后重新清算"],
+      ["CLR20260923001", "2026-09-23", "正常清算", "失败", "admin", "2026-09-23 17:30:00", "2026-09-23 17:36:51", "第 4 步交收处理失败:对冲账户 880012 资金不足"],
+    ],
+  };
+
+  /* ---------- 发行端 · 共赢票据风险监控(views/issue/risk-monitor:三个页签;MonitorPane 的搜索项只有占位文字,没有标签) ---------- */
+  const risk = {
+    tabs: [["monitor", "共赢票据风险监控"], ["status-log", "风险状态变更记录"], ["notification", "追保通知发送记录"]],
+    search: [
+      { ph: "风险状态", type: "select", multiple: true }, { ph: "票据状态", type: "select", multiple: true, value: ["待生效", "存续期"] }, { ph: "票据编号", type: "input" },
+      { ph: "投资者账户", type: "input" }, { ph: "投资者编号/账户/名称", type: "input" }, { ph: "机构名称", type: "select" },
+      { ph: "证券市场", type: "select" }, { ph: "证券代码/名称", type: "input" },
+    ],
+    actions: [["查找", "primary", "search"], ["重置", "", "refresh"], ["数据导出", "", "download"]],
+    // 列为示意(RiskMonitorTable 的列由配置生成),字段名取自票据详情
+    cols: [["操作", 120], ["票据编号", 170], ["票据状态", 90], ["风险状态", 90], ["投资者账户", 120], ["投资者名称", 110], ["机构名称", 140], ["证券代码/名称", 170],
+      ["票据股票数量", 120, "r"], ["最新价", 100, "r"], ["预警/危险/强平价格", 200, "r"], ["底层资产浮动盈亏及比例", 220, "r"], ["到期日期", 110]],
+    rows: [
+      ["NT001202608127A", "存续期", "正常", "HH10023430", "李婉清", "远航财富", "300750 宁德时代", "8,100", "251.80", "220.73 / 208.47 / 196.21", "+58,002.47 HKD / +2.67%", "2027-03-29"],
+      ["NT001202608134A", "存续期", "预警", "HH10023429", "王子豪", "明德资产管理", "000858 五粮液", "12,400", "118.20", "121.46 / 114.71 / 107.96", "-31,886.40 HKD / -2.92%", "2026-12-29"],
+      ["NT001202608141A", "存续期", "危险", "HH10023428", "陈思雨", "安衡证券", "601318 中国平安", "28,700", "48.12", "52.18 / 49.28 / 46.38", "-128,776.52 HKD / -7.78%", "2026-12-29"],
+      ["NT001202608148A", "存续期", "正常", "HH10023427", "刘建国", "远航财富", "002594 比亚迪", "5,200", "268.40", "225.36 / 212.84 / 200.32", "+92,513.60 HKD / +7.21%", "2027-03-29"],
+      ["NT001202608155A", "存续期", "正常", "HH10023426", "赵雅琳", "启原投资", "600036 招商银行", "28,000", "36.95", "32.04 / 30.26 / 28.48", "+41,328.00 HKD / +3.79%", "2027-03-29"],
+      ["NT001202608162A", "待生效", "正常", "HH10023425", "孙浩然", "明德资产管理", "601899 紫金矿业", "48,700", "16.42", "-- / -- / --", "--", "2026-10-29"],
+      ["NT001202608169A", "存续期", "预警", "HH10023424", "周晓彤", "安衡证券", "600900 长江电力", "9,300", "20.14", "20.86 / 19.70 / 18.55", "-9,356.84 HKD / -4.56%", "2026-12-29"],
+      ["NT001202608176A", "存续期", "正常", "HH10023423", "吴俊杰", "远航财富", "688981 中芯国际", "5,600", "90.12", "79.38 / 74.97 / 70.56", "+66,135.52 HKD / +12.60%", "2027-03-29"],
+    ],
+    statusLogCols: [["票据编号", 170], ["投资者名称", 110], ["机构名称", 140], ["证券代码/名称", 170], ["变更前", 90], ["变更后", 90], ["触发价格", 110, "r"], ["变更时间", 165], ["备注", 220]],
+    statusLog: [
+      ["NT001202608141A", "陈思雨", "安衡证券", "601318 中国平安", "预警", "危险", "49.10", "2026-09-25 10:12:40", "连续 3 个交易日低于危险价格"],
+      ["NT001202608134A", "王子豪", "明德资产管理", "000858 五粮液", "正常", "预警", "121.30", "2026-09-24 14:02:17", ""],
+      ["NT001202608169A", "周晓彤", "安衡证券", "600900 长江电力", "正常", "预警", "20.80", "2026-09-23 09:48:05", ""],
+      ["NT001202608141A", "陈思雨", "安衡证券", "601318 中国平安", "正常", "预警", "52.02", "2026-09-22 13:31:52", ""],
+    ],
+    notifyCols: [["票据编号", 170], ["投资者名称", 110], ["机构名称", 140], ["通知渠道", 100], ["发送状态", 100], ["发送时间", 165], ["内容摘要", 360]],
+    notify: [
+      ["NT001202608141A", "陈思雨", "安衡证券", "邮件", "已发送", "2026-09-25 10:13:02", "票据 NT001202608141A 进入危险状态,请于 2026-09-26 15:00 前追加保证金"],
+      ["NT001202608141A", "陈思雨", "安衡证券", "短信", "已发送", "2026-09-25 10:13:02", "【共赢票据】票据进入危险状态,请及时追加保证金"],
+      ["NT001202608134A", "王子豪", "明德资产管理", "邮件", "发送失败", "2026-09-24 14:03:10", "票据 NT001202608134A 进入预警状态"],
+    ],
+  };
+
+  /* ---------- 首页(工作台):数字为示例,上线需要后端提供按状态汇总的接口。字段全部来自各端列表页 ---------- */
+  const home = {
+    issue: {
+      noun: "订单", dictKey: "note_order_status", listKey: "共赢票据管理/订单管理",
+      status: [["待处理", 6, true], ["执行中", 14], ["待撤", 2, true], ["撤单中", 1], ["确认中", 9, true], ["已完成", 61], ["已结算", 27], ["已撤", 5], ["已拒绝", 3]],
+      manualTitle: "待人工处理的订单", manualSub: "处理方式为「人工」或状态为待处理、待撤、确认中",
+      manualCols: [["主订单编号", 190, "no", "mono"], ["方向", 70, "direction", "tag:direction"], ["订单状态", 90, "status", "tag:note_order_status"], ["对冲进度", 90, "hedge", "tag:hedge_progress"], ["投资者", 100, "invName"], ["证券代码/名称", 150, "stock"], ["订单名义本金", 150, "notional", "num", "r"], ["下单时间", 165, "orderTime", "mono"]],
+      pick: (r) => r.mode === "人工" || ["待处理", "待撤", "确认中"].includes(r.status),
+      shortcuts: [["共赢票据管理", "订单管理", "ticket"], ["共赢票据管理", "票据管理", "file"], ["共赢票据管理", "询价管理", "search"], ["风控管理", "共赢票据风险监控", "shield"], ["清算管理", "共赢票据清算", "check"], ["报表管理", "日常报表", "chart"]],
+    },
+    sales: {
+      noun: "订单", dictKey: "note_order_status", listKey: "共赢票据管理/订单管理",
+      status: [["待处理", 4, true], ["执行中", 11], ["待撤", 1, true], ["撤单中", 0], ["确认中", 7, true], ["已完成", 48], ["已结算", 22], ["已撤", 4], ["已拒绝", 2]],
+      manualTitle: "待处理的订单", manualSub: "状态为待处理、待撤、确认中",
+      manualCols: [["主订单编号", 190, "no", "mono"], ["方向", 70, "direction", "tag:direction"], ["订单状态", 90, "status", "tag:note_order_status"], ["投资者", 100, "invName"], ["证券代码/名称", 150, "stock"], ["订单名义本金", 150, "notional", "num", "r"], ["下单方式", 90, "source", "tag:sales_order_source"], ["下单时间", 165, "orderTime", "mono"]],
+      pick: (r) => ["待处理", "待撤", "确认中"].includes(r.status),
+      shortcuts: [["共赢票据管理", "订单管理", "ticket"], ["共赢票据管理", "票据管理", "file"], ["共赢票据管理", "询价管理", "search"], ["投资者管理", "投资者信息", "user"], ["投资者管理", "开户审核", "check"], ["报表管理", "日结单", "chart"]],
+    },
+    trs: {
+      noun: "委托", dictKey: "trs_order_status", listKey: "用户管理/委托查询",
+      status: [["未报", 1, true], ["待报", 0], ["已报", 5], ["部成", 2], ["待撤", 1, true], ["已撤", 3], ["部撤", 1], ["已成", 24], ["废单", 1, true], ["过期", 0], ["部废", 0]],
+      manualTitle: "需要关注的委托", manualSub: "状态为未报、待撤、部撤、废单",
+      manualCols: [["委托编号", 170, "no", "mono"], ["用户名称", 100, "uname"], ["股票", 150, "stock"], ["方向", 70, "dir", "tag:direction"], ["委托状态", 90, "status", "tag:trs_order_status"], ["委托数量", 100, "qty", "num", "r"], ["委托价格", 100, "price", "num", "r"], ["委托时间", 165, "otime", "mono"]],
+      pick: (r) => ["未报", "待撤", "部撤", "废单"].includes(r.status),
+      shortcuts: [["用户管理", "委托查询", "search"], ["用户管理", "成交查询", "check"], ["系统设置", "交易下单", "ticket"], ["用户管理", "用户信息", "user"], ["清算管理", "清算", "scale"], ["报表管理", "TRS 报表", "chart"]],
+    },
+  };
+
+  /* ---------- 个人中心(views/profile/index.vue):账号信息、安全设置、修改密码弹窗 ---------- */
+  const profile = {
+    nickname: "admin", role: "系统管理员", username: "admin", gender: "男", mobile: "", email: "", dept: "平台运营", createTime: "2026-07-01 10:00:00",
+    security: [["账户密码", "定期修改密码有助于保护账户安全", "修改"], ["手机号码", "绑定后可用于接收告警短信", "绑定"], ["邮箱", "绑定后可用于接收通知邮件和重置密码", "绑定"]],
+    pwdForm: [{ label: "原密码", type: "password", req: true, ph: "请输入原密码" }, { label: "新密码", type: "password", req: true, ph: "6~20 位,至少两类字符" }, { label: "确认密码", type: "password", req: true, ph: "请再次输入新密码" }],
+  };
+
+  /* ---------- 登录页(views/login):文案取自 lang/package/zh-cn.json;三端入口的一句话职责取自各端菜单 ---------- */
+  const login = {
+    copy: { username: "用户名", password: "密码", captcha: "验证码", remember: "记住我", forget: "忘记密码?", login: "登 录", reset: "重置密码", email: "邮箱", enterEmail: "请输入您的邮箱地址", sendReset: "发送重置链接", think: "想起密码?", back: "返回登录", theme: "主题切换", lang: "语言切换" },
+    version: "v3.4.2",
+    // 三端入口卡片上的系统名和一句话职责
+    nodes: { sales: ["销售管理系统", "投资者 · 订单 · 资金"], issue: ["发行管理系统", "票据 · 对冲 · 清算"], trs: ["TRS管理系统", "通道 · 委托 · 风控"] },
+    // 登录页上列出的本系统业务模块:取各端菜单的业务分组(运维与配置类不列)
+    opsGroups: ["系统设置", "机构系统设置", "常驻工作进程", "计划任务中心", "通知和告警管理"],
+  };
+
+  /* ---------- 反馈类:文案尽量取自真实代码(command-result.ts、各页 ElMessageBox / el-empty / el-alert) ---------- */
+  const feedback = {
+    messages: [["success", "保存成功"], ["error", "导出失败,请稍后重试"], ["info", "正在导出,完成后自动下载"], ["warning", "已受理并进入重试队列,尚未送达 TRS。请勿重复下单;若长时间仍显示「未报」,请联系运维查消息状态。", true]],
+    notification: { title: "清算完成", body: "2026-09-25 交易日清算已完成,营业日已推进到 2026-09-28。", time: "17:41", link: "查看清算记录" },
+    alerts: [
+      ["info", "自开始日期(含)起同步交易日,重复同步不会产生重复数据。"],
+      ["warning", "批量设置后,原券池将被清空,新券池为本次上传的内容。"],
+      ["danger", "撤单结果未知:上轮撤单未取得明确结果,可人工再次撤单。"],
+      ["success", "交易日历已同步到 2026-12-31。"],
+    ],
+    alertTitled: ["warning", "未启用共赢票据(A类)", "请确认该投资者当前无需A类交易权限。启用后需在「交易品类」里勾选。"],
+    confirms: {
+      cancel: { title: "撤单", msg: "确定要进行撤单吗?", kind: "danger", ok: "确定撤单", kv: [["委托编号", "ALG202609250007"], ["方向 / 股数", "买入 5,000 股"], ["对冲账号", "hedge01 / 880012"]] },
+      remove: { title: "删除对冲记录", msg: "确定删除此对冲记录吗?删除后不能恢复。", kind: "danger", ok: "删除", kv: [["对冲类型", "补录"], ["成交", "1,100 股 × 245.26 CNY"], ["发生金额", "269,786.00 CNY"]] },
+      ratio: { title: "确认调整", msg: "建仓比例过低,请确认是否需要调整买入名义本金数值?", kind: "warning", ok: "继续提交", cancel: "返回修改", kv: [["建仓比例", "62.50%"], ["买入名义本金", "2,000,000.00 CNY"]] },
+      logout: { title: "退出登录", msg: "确定退出当前账号吗?", kind: "info", ok: "退出", kv: [] },
+    },
+    empties: [
+      ["表格无数据", "暂无数据", "", ""],
+      ["查无结果", "没有符合条件的数据", "调整筛选条件后重新查找", "重置筛选"],
+      ["需要先选择", "请选择 Worker 查看分片覆盖率", "", ""],
+      ["需要先操作", "当前月份暂无日历数据", "请先同步交易日历和交收日历", "同步日历"],
+      ["通知为空", "暂无消息", "", ""],
+    ],
+    results: [
+      ["success", "清算完成", "2026-09-25 交易日清算已完成,营业日已推进到 2026-09-28。", [["查看清算记录", "primary"], ["返回", ""]]],
+      ["error", "导出失败", "报表生成超时,请缩小日期范围后重试;连续失败请联系运维。", [["重试", "primary"], ["返回", ""]]],
+    ],
+  };
+
+  /* ---------- 设计稿目录:每个页面的来源、设计要点、待确认 ---------- */
+  const pages = [
+    { key: "spec", group: "总览", title: "规范摘要", kind: "spec", sub: "选型结果、尺寸、颜色、12 类对应关系、待确认清单" },
+    { key: "kit", group: "总览", title: "组件总览", kind: "kit", sub: "两套皮肤(浅色 F、深色 B 石墨灰)下的全部基础组件,顶栏「皮肤」切换",
+      src: "分项选型 · 基础视觉 F(晴空蓝 #3C8CFF、文字链接 #1668DC)", points: ["颜色、文字、按钮、输入控件、状态标签、表格、提示、键值、弹窗外观,全部读同一份令牌;换肤只换令牌取值,尺寸、圆角、字体、排布不变。", "深色皮肤 B 石墨灰(10-08 从三版候选里选定):中性灰、扁平、1px 描边、无渐变无投影,主按钮实心蓝,状态标签只描边。对照见「规范摘要 → 换肤」。", "主按钮是左上浅、右下深的渐变,下面有一层淡蓝投影;页面底色是蓝白渐变,右上角一团浅蓝光晕;卡片白底、14px 圆角、淡蓝投影。", "表头很淡的蓝底,行高 40px;状态是无描边的浅色圆角标签;中性色带蓝调;数字用 Inter。"], todo: ["深色默认跟随系统还是手动切换;切换后的选择记在账号还是本机。"] },
+    { key: "login", group: "全局", title: "登录页", kind: "login", sub: "分项选型登录页 C:右侧浮卡·全幅底图 + 左侧三端入口", variants: [["login", "登录"], ["reset", "重置密码"]], ends: true,
+      src: "分项选型 · 登录页 C(2026-10-07 选定);views/login/index.vue、components/Login.vue、ResetPwd.vue;文案取自 lang/package/zh-cn.json",
+      points: ["整页是视觉 F 的蓝白渐变底色,叠一层很淡的网格、几条主色曲线和右上角两个圆(内联 SVG,不用图片文件)。", "左侧一列(垂直居中,和卡片对齐):系统名 32px 和版本号、本系统业务模块(取自本系统菜单的业务分组,不列运维配置类)、「三端系统」三张入口卡片——当前系统标「当前」,另外两个点一下跳到对应系统的登录页。", "右侧 440px 浮卡,离右边 96px:小标志+系统名、「登录」标题、用户名/密码/验证码标签在上,输入框按代码现状 40px(登录页 size=large),其余页面仍是 32px;记住我、忘记密码;登录按钮通宽。", "右上角的主题按钮可以点:在浅色和最近看过的深色皮肤之间来回切(月亮/太阳);语言切换只保留入口;「注册」组件代码里存在但登录页没有入口,本稿不画。", "画面里可以点:三端入口(切到那个系统)、忘记密码/返回登录、换一张验证码、显示密码、记住我。"],
+      todo: ["验证码图片是示意;登录页的控件要不要也统一成 32px。", "三端入口是跳到各系统各自的登录地址,三个地址要放在前端配置里维护;跳转时要不要带上当前输入的用户名。", "三端登录页只换系统名、标志、模块清单和入口里的高亮,其余完全一样——是否需要三端不同的底色做区分(口径是三端完全一致,本稿没有区分)。"] },
+    { key: "home", group: "全局", title: "整体框架 · 首页", kind: "shell", active: "首页", ends: true, sub: "框架 A 经典三段 + 首页工作台",
+      src: "layouts/(侧栏、标签页、菜单搜索、通知、用户菜单、交易日);views/dashboard/index.vue 现状只有问候语",
+      points: ["框架 A:浅色侧栏 220px 通高(白底微透、深灰字、选中项淡蓝块,标志是晴空蓝渐变;深色皮肤下是近黑侧栏),收起后是 64px 图标栏,悬停弹出二级菜单。白色顶栏 52px:折叠按钮、面包屑|营业日与两个 A 股交易日、菜单搜索、全屏、通知、用户(界面设置收进用户菜单)。标签页栏 38px,卡片式页签,右键有刷新、关闭其它、关闭所有。内容区四周留 16px。", "首页改成工作台:今日订单按状态的数字(需要处理的高亮)、待人工处理的订单表、快捷入口(取自本端菜单)。问候语保留,天气那句是模板里写死的假内容,去掉。", "三端同一结构:发行端看订单,销售端看订单,TRS 看委托;数字是示例,上线要后端提供按状态汇总的接口。", "面包屑和标签页都显示页面名(这是框架 A 的特点);页面内不再放标题。"],
+      todo: ["「界面尺寸」切换已去掉(密度已锁定);「界面设置」收进用户菜单——请确认。", "首页的数字口径(今日=营业日当天?)和表格条数(本稿取前 6 条)。"] },
+    { key: "list", group: "全局", title: "列表页 · 搜索区 + 表格", kind: "shell", active: "home", ends: true, sub: "发行端订单管理、销售端订单管理、TRS 委托查询",
+      src: "views/issue/order、views/sales/order、views/trs/order;SearchForm 组件(固定 3 列、超过 2 行折叠)",
+      points: ["搜索区一张卡片:标签在左、3 列等宽,标签宽度按本页最长标签统一;条件超过两行时折叠,「展开」在按钮组末尾。查找是主按钮,重置、导出是次按钮。", "表格一张卡片:勾选列和操作列固定在左;表头 40px、行高 40px;数字右对齐;状态用字典标签;空值显示「--」。", "勾选后表格顶部出现浅蓝选择条,显示已选条数和「清空」。分页固定在卡片底部,左边是总数。", "点「详情」打开订单详情抽屉(抽屉 E);在抽屉里点「补录」会叠开补录弹窗(弹窗 J)。"],
+      todo: ["表格列宽和固定列按代码现状,没有加列设置、密度切换(代码里没有)。"] },
+    { key: "drawer", group: "弹层", title: "订单详情抽屉", kind: "shell", end: "issue", active: "home", ovl: ["order"], sub: "抽屉 E 卡片+描述表",
+      src: "views/issue/order/components/OrderDetailDrawer 及子组件",
+      points: ["宽 1040px;头部是编号、4 个状态标签和一行关键信息(证券、名义本金、投资者、下单时间),右侧刷新、关闭。", "灰底上排卡片:主订单信息、A类订单信息用带边框的描述表(灰底标签格,一行 3 对);对冲执行卡片里是建议、7 个汇总数字、记录表格。", "流程按钮在卡片标题右侧:直接下单、补录是次按钮,完成对冲是主按钮;「转为自动处理」在主订单卡片。", "点「补录」叠开补录弹窗;弹窗关闭后回到抽屉。"],
+      todo: ["「直接下单」会打开发行端 HedgeOrderDialog(普通单 + 算法单),本稿没有画,按弹窗 J 的卡片分区实现。"] },
+    { key: "dlg-fill", group: "弹层", title: "补录弹窗(短表单)", kind: "shell", end: "issue", active: "home", ovl: ["order", "fill"], sub: "短表单布局:标签在左 + 顶部信息块 + 底部结果块",
+      src: "views/issue/order/components/HedgeFillDialog.vue;布局按 2026-10-08 给的下单弹窗",
+      points: ["宽 640px;标题下一行写操作的是哪一笔主订单和方向;顶部两个高亮信息块放主订单编号、证券,表单里不再重复。", "标签在左、右对齐、单列;对冲系统是「自定义|TRS」两段切换,点一下下面的字段就变;成交股数、成交均价用带加减的数字框。", "算出来的值放在表单底部的灰色结果块里,写明算法,不可修改;必填项前有红星,校验失败的输入框变红并在下方显示原因。", "底部左侧一句提示,右侧取消、确定。长表单(新建投资者)仍用弹窗 J 的卡片分区。"],
+      todo: ["代码现状只有「确定」、校验失败用全局提示报错;本稿加了「取消」和行内校验——请确认。"] },
+    { key: "dlg-investor", group: "弹层", title: "新建投资者弹窗(长表单)", kind: "shell", end: "sales", active: "投资者管理/投资者信息", ovl: ["investor"], sub: "弹窗 J 卡片分区",
+      src: "views/sales/investor/components/InvestorCreateDialog.vue",
+      points: ["宽 920px,四张卡片:基础信息、问卷信息、交易品类、其它信息;卡片标题右侧显示「已填/必填」,有错的组显示红字。", "标签在上、双列;组合项(区号+手机、国别+地址、证件类型+号码)在一格里并排。", "底部左侧实时显示「还有几项必填未填、几项有误」;右侧取消、保存。", "勾选「共赢票据(A类)」后,下面的警告提示条消失(可以点画面试)。"],
+      todo: [] },
+    { key: "dlg-trs", group: "弹层", title: "TRS 下单弹窗(专用布局)", kind: "shell", end: "trs", active: "系统设置/交易下单", ovl: ["trs"], sub: "交易下单页 + 下单弹窗", variants: [["buy", "买入"], ["sell", "卖出"]],
+      src: "views/trs/config/order-place/detail(下单详情页:资金信息、持仓、当日委托;下单弹窗:买卖、价格、数量、五档行情);布局按 2026-10-08 给的下单弹窗",
+      points: ["页面:账户选择 + 资金信息描述表 + 「买入」「卖出」两个按钮;下面一张卡片用页内页签切换持仓、当日委托。", "弹窗宽 880px:标题就是方向,标题下一行写账户;顶部两个高亮信息块(可用余额、最大可买/可卖股数);标签在左、单列:交易渠道两段切换、股票代码、委托价格和委托数量用带加减的数字框,下面是快捷档位和份额;底部两个灰色结果块是金额。", "右侧五档行情:名称与代码、实时推送标记、最新价和涨跌、昨收提示;卖五到卖一、中间一行最新价、买一到买五,量用色条表示,点任一档位填入委托价。", "确认按钮跟随方向变色(买入红、卖出绿),文字写明「确认买入 1,000 股」。"],
+      todo: ["算法单(TWAP/POV/E-Amount)的参数区本稿没有画,按同一布局在数量下方增加「算法参数」几行实现。"] },
+    { key: "drawer-note", group: "弹层", title: "票据详情抽屉(页签 + 描述表)", kind: "shell", end: "issue", active: "共赢票据管理/票据管理", ovl: ["note"], sub: "详情展示 + 页内标签页在抽屉里的样子",
+      src: "views/issue/components/NoteDetailDrawer.vue、note-detail/NoteInfoPanel、NoteHedgePanel、NoteChangesPanel",
+      points: ["头部:编号、状态标签、关键信息;操作按钮(修改到期日、票据调整、添加分红、票据卖出)放在头部右侧,按票据状态显示。", "头部下方一行页签:主页、变动记录;页签内容是灰底上的卡片。", "描述表:标签长的分组(当前信息)改成一行 2 对,避免标签换行;盈亏用红绿文字。", "对冲信息卡片里三张表:持仓(带合计行)、票据买卖对冲记录、日常买卖对冲记录(标题右侧放下单、补录、刷新)。", "变动记录页签里没有数据的表显示空状态,不显示空表头。"],
+      todo: ["票据买卖对冲记录的列按订单对冲记录示意,以代码为准。"] },
+    { key: "tabs", group: "页面", title: "页内标签页 · 风险监控", kind: "shell", end: "issue", active: "风控管理/共赢票据风险监控", sub: "页面级页签(三个页签各是一套搜索 + 表格)",
+      src: "views/issue/risk-monitor/index.vue(页签)、components/MonitorPane.vue(搜索项只有占位文字)",
+      points: ["页面级页签放在内容区最上面一行,不套卡片:40px 高,当前页签蓝字加 2px 下划线;下面才是搜索卡片和表格卡片。", "和浏览器式标签页(框架)、卡片内页签(清算)三者形态不同,一眼能分出层级。", "搜索项按代码现状只有占位文字、没有标签;多选项(票据状态默认选了待生效、存续期)显示成小标签。", "风险状态「危险」的行,操作列出现「强平卖出」;其它行只有「详情」。"],
+      todo: ["表格列为示意(代码里的列由配置生成)。", "搜索项要不要补标签(和订单管理页统一)。"] },
+    { key: "clearing", group: "页面", title: "清算管理(卡片内页签)", kind: "shell", end: "issue", active: "清算管理/共赢票据清算", sub: "页签在卡片里 + 步骤表 + 日志",
+      src: "views/issue/clearing/index.vue(步骤名与代码逐字一致)",
+      points: ["一张卡片:标题栏左边是页签(清算、清算日志),右边是工具(刷新、一键清算、重新清算;执行中时日志页签多一个「停止当前任务」)。", "本次清算的交易日、批次号和批次状态放在页签下方的一行信息条里;营业日不重复显示(侧栏已有)。", "步骤表:执行状态用标签;执行中的行高亮;失败时在表格下方出现红色提示条。", "代码里的「A股清算」市场页签只有一项,本稿收成信息条里的「市场 A股」,多市场时再变回页签。"],
+      todo: ["市场页签的处理请确认。"] },
+    { key: "profile", group: "页面", title: "个人中心", kind: "shell", end: "issue", active: "个人中心", ends: true, sub: "从用户菜单进入", variants: [["page", "页面"], ["pwd", "修改密码弹窗"]],
+      src: "views/profile/index.vue",
+      points: ["左边一张身份卡:头像、昵称、角色,昵称旁可以改;右边账号信息描述表(用户名、手机、邮箱、部门、创建时间),未绑定的项直接给「绑定」入口。", "安全设置列表:账户密码、手机号码、邮箱,每行右侧一个操作。", "修改密码弹窗按短表单布局:标签在左、单列,宽 480px;密码规则写在新密码下方。"],
+      todo: ["头像上传入口代码里有(隐藏的文件选择),本稿放在头像上悬停显示——请确认。"] },
+    { key: "err", group: "页面", title: "404 / 401", kind: "shell", end: "issue", active: "404", variants: [["404", "404 页面不存在"], ["401", "401 没有权限"]], sub: "在框架内显示,导航仍可用",
+      src: "views/error/404.vue、401.vue",
+      points: ["放在框架的内容区里,侧栏和标签页仍可用,不做整页跳转。", "主色描边的大数字,下面是一句说明和两个按钮:返回首页(主按钮)、返回上一页。", "401 写清楚是哪个页面没有权限、找谁开通。"],
+      todo: ["代码里 401 页的「如有不满请联系你领导」和外链「随便看看」是模板遗留,本稿改成正式文案——请确认。"] },
+    { key: "confirm", group: "反馈", title: "确认框(在页面里)", kind: "shell", end: "issue", active: "home", ovl: ["confirm"], variants: [["cancel", "撤单"], ["remove", "删除记录"], ["ratio", "比例确认"], ["logout", "退出登录"]], sub: "ElMessageBox.confirm 的样子",
+      src: "各页 ElMessageBox.confirm 的文案",
+      points: ["宽 420px;状态图标和标题同一行(危险红、警告橙、信息蓝),下面的问题和小描述表与图标左对齐,不缩进。", "危险操作的确定按钮是红色实心并写明动作(确定撤单、删除);警告类用主色;按钮靠右,取消在左。", "不放关闭叉号,只能取消或确定。"],
+      todo: ["确认框里要不要显示操作对象(代码现状只有一句话)。"] },
+    { key: "feedback", group: "反馈", title: "反馈类总览", kind: "feedback", sub: "全局提示、通知、提示条、空状态、加载态、结果提示、校验",
+      src: "ElMessage、ElNotification、el-alert、el-empty、v-loading、el-result 的用法;文案尽量取自代码",
+      points: ["全局提示(ElMessage):顶部居中,白底硬投影,左侧彩色图标;跨系统命令「已受理未送达」的提示停留 8 秒并可手动关闭。", "通知(ElNotification):右上角卡片,状态图标和标题同一行,正文和链接与图标左对齐。", "提示条:左侧 3px 色条,白底;可带标题;可关闭。", "空状态:一句话说明原因,必要时给一个动作;表格空状态高度固定 240px。", "加载态:表格用遮罩加转圈,按钮用内嵌转圈并禁用;首屏用骨架条。", "结果提示:整块居中,图标、标题、说明、按钮。"],
+      todo: [] },
+  ];
+
+  return { dict, noteDetail, clearing, risk, home, profile, login, feedback, pages };
+})();

BIN
src/fonts/Inter-Medium.woff2


BIN
src/fonts/Inter-Regular.woff2


BIN
src/fonts/Inter-SemiBold.woff2


+ 139 - 0
src/frame.css

@@ -0,0 +1,139 @@
+/* ============ 整体框架 A「经典三段」(取自分项选型 frames.css 的通用部分 + A,去掉其他框架) ============ */
+.cv.shell { height: 900px; overflow: hidden; background: var(--canvas, var(--frame-outer)); }
+.sh { display: flex; height: 100%; }
+.mn { flex: 1; min-width: 0; display: flex; flex-direction: column; }
+.pa { flex: 1; min-height: 0; overflow: auto; padding: 16px; background: var(--pa-bg, var(--bg-page)); }
+.sp { flex: 1; }
+
+/* 侧栏色调全部读皮肤的 sd-* 令牌:浅色 F 是白底微透的浅色侧栏(2026-10-08 按分项选型框架 E 的配色),深色 B 是近黑侧栏 */
+.sd { position: relative; z-index: 5; flex: none; display: flex; flex-direction: column; width: 220px; transition: width .2s; }
+.sd { --m-bg: var(--sd-bg); --m-sub: var(--sd-sub); --m-t: var(--sd-t); --m-t-dim: var(--sd-t-dim); --m-act-bg: var(--sd-act-bg); --m-act-t: var(--sd-act-t); --m-hover: var(--sd-hover); --m-hi: var(--sd-hi); --m-fly: var(--sd-fly); }
+.sd { background: var(--m-bg); color: var(--m-t); backdrop-filter: var(--sd-bf); border-right: var(--sd-bd); }
+
+.brand { flex: none; display: flex; align-items: center; gap: 10px; height: 52px; padding: 0 16px; white-space: nowrap; overflow: hidden; }
+.brand .logo { width: 28px; height: 28px; flex: none; border-radius: calc(var(--r-ctl) + 4px); background: var(--logo-bg); color: var(--logo-t); font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
+.brand .nm { font-size: 15px; font-weight: 600; color: var(--m-hi); letter-spacing: .01em; }
+.sd .brand .logo { background: var(--sd-logo-bg); color: var(--sd-logo-t); }
+
+.menu { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 6px 8px 12px; scrollbar-width: thin; }
+.mg { position: relative; }
+.mg-t, .mi-top {
+  display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 12px; margin: 2px 0;
+  border-radius: var(--r-ctl); cursor: pointer; color: var(--m-t); white-space: nowrap; transition: background .15s, color .15s;
+}
+.mg-t:hover, .mi-top:hover, .mi:hover { background: var(--m-hover); color: var(--m-hi); }
+.mg-t .ic, .mi-top .ic { width: 16px; height: 16px; }
+.mg-t .arr { margin-left: auto; width: 14px; height: 14px; opacity: .6; transition: transform .2s; }
+.mg.is-open > .mg-t .arr { transform: rotate(180deg); }
+.mg.has-act > .mg-t { color: var(--m-hi); }
+.mg-c { display: none; }
+.mg.is-open > .mg-c { display: block; }
+.mi { display: flex; align-items: center; height: 36px; padding: 0 12px 0 38px; margin: 2px 0; border-radius: var(--r-ctl); cursor: pointer; color: var(--m-t); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background .15s, color .15s; }
+.mi.is-act, .mi-top.is-act { background: var(--m-act-bg); color: var(--m-act-t); font-weight: 500; }
+.sec-t { padding: 14px 12px 4px; font-size: 12px; color: var(--m-t-dim); white-space: nowrap; }
+
+/* 收起态:图标栏 + 悬停弹出二级菜单 */
+.sh.is-col .sd { width: 64px; }
+.sh.is-col .sd .txt, .sh.is-col .sd .arr, .sh.is-col .sec-t, .sh.is-col .brand .nm { display: none; }
+.sh.is-col .brand { justify-content: center; padding: 0; }
+.sh.is-col .menu { overflow: visible; padding: 6px 10px; }
+.sh.is-col .mg-t, .sh.is-col .mi-top { justify-content: center; padding: 0; }
+.sh.is-col .mg.has-act > .mg-t { background: var(--m-act-bg); color: var(--m-act-t); }
+.sh.is-col .mg > .mg-c { display: none !important; }
+.sh.is-col .mg:hover > .mg-c {
+  display: block !important; position: absolute; left: calc(100% + 10px); top: 0; min-width: 180px; padding: 4px;
+  background: var(--m-fly); border-radius: var(--r-pop); box-shadow: var(--sh-pop); border: var(--pop-bd); z-index: 30;
+}
+.sh.is-col .mg:hover > .mg-c::before { content: ""; position: absolute; left: -12px; top: 0; bottom: 0; width: 12px; }
+.sh.is-col .mg-c .mi { padding-left: 12px; }
+.sh.is-col .mg-c .fly-t { display: block; padding: 6px 12px 2px; font-size: 12px; color: var(--m-t-dim); }
+.fly-t { display: none; }
+
+/* 通用小部件 */
+.icb { position: relative; width: 32px; height: 32px; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-ctl); cursor: pointer; color: var(--t2); transition: background .15s; }
+.icb:hover { background: var(--fill-2); color: var(--t1); }
+.icb .ic { width: 18px; height: 18px; }
+.icb .dot { position: absolute; top: 6px; right: 7px; width: 7px; height: 7px; border-radius: 50%; background: var(--dang); box-shadow: 0 0 0 2px var(--bg-card); }
+.anchor { position: relative; display: inline-flex; align-items: center; }
+.user { display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 6px; border-radius: var(--r-ctl); cursor: pointer; color: var(--t1); white-space: nowrap; }
+.user:hover { background: var(--fill-2); }
+.ava { width: 26px; height: 26px; flex: none; border-radius: 50%; background: var(--c-primary-bg); color: var(--c-link); font-size: 12px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; }
+.user .ic { width: 12px; height: 12px; color: var(--t3); }
+.td { display: flex; align-items: center; gap: 16px; font-size: 13px; color: var(--t3); white-space: nowrap; }
+.td b { color: var(--t1); font-weight: 500; font-family: var(--font-num); font-variant-numeric: tabular-nums; margin-left: 6px; }
+.td .rf { width: 14px; height: 14px; cursor: pointer; color: var(--t3); }
+.td .rf.spin, .tdb .rf.spin { animation: cv-spin .6s linear; }
+@keyframes cv-spin { to { transform: rotate(360deg); } }
+.vsep { width: 1px; height: 16px; background: var(--bd); flex: none; }
+
+/* 弹出层(用户菜单、通知、菜单搜索、标签右键) */
+.fl { position: absolute; z-index: 40; }
+.fl.below-r { top: calc(100% + 6px); right: 0; }
+.fl.below-l { top: calc(100% + 6px); left: 0; }
+.fl.above-l { bottom: calc(100% + 6px); left: 0; }
+.fl.right-b { left: calc(100% + 10px); bottom: 0; }
+.bell-pop { width: 320px; padding: 0; }
+.bell-pop .pop-hd { display: flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px; border-bottom: 1px solid var(--bd); font-weight: 500; color: var(--t1); }
+.bell-pop .pop-hd .cnt { font-size: 12px; color: var(--t3); font-weight: 400; }
+.bell-pop .pop-hd .btn.txt { margin-left: auto; font-size: 12px; }
+.bell-i { display: flex; flex-direction: column; gap: 2px; padding: 10px 16px; border-bottom: 1px solid var(--bd-light); cursor: pointer; }
+.bell-i:hover { background: var(--fill-1); }
+.bell-i .r { display: flex; align-items: center; gap: 8px; }
+.bell-i .r span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--t1); }
+.bell-i .r small { font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.bell-i p { margin: 0; font-size: 12px; color: var(--t3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bell-ft { display: flex; justify-content: space-between; padding: 8px 16px; font-size: 12px; }
+.ms-pop { width: 380px; padding: 0; overflow: hidden; }
+.ms-pop .ipt { margin: 8px; }
+.ms-list { max-height: 300px; overflow: auto; padding: 0 4px 4px; }
+.ms-list .pop-i { justify-content: space-between; }
+.ms-list .grp { color: var(--t3); font-size: 12px; }
+.ms-list mark { background: none; color: var(--c-link); font-weight: 600; }
+.ms-empty { padding: 20px; text-align: center; color: var(--t3); font-size: 13px; }
+.ctx { position: absolute; top: calc(100% - 2px); z-index: 45; min-width: 128px; }
+
+/* 标签页基础 */
+.tv { position: relative; z-index: 4; flex: none; display: flex; align-items: center; }
+.tvi { display: inline-flex; align-items: center; gap: 6px; flex: none; cursor: pointer; white-space: nowrap; font-size: 13px; color: var(--t2); transition: color .15s, background .15s; }
+.tvi:hover { color: var(--t1); }
+.tvi .x { width: 14px; height: 14px; padding: 2px; border-radius: 50%; color: var(--t3); }
+.tvi .x:hover { background: var(--fill-3); color: var(--t1); }
+.tvi .ic.hm { width: 14px; height: 14px; }
+
+/* 页面内容:列表页与占位页 */
+.pg-list { display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 0; }
+.sf { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px 24px; padding: 16px 20px; }
+.sf-i { display: flex; align-items: center; gap: 12px; min-width: 0; }
+.sf-i > label { flex: none; text-align: right; color: var(--t2); white-space: nowrap; }
+.sf-i > .ipt { flex: 1; }
+.sf-act { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
+.sf-act .btn.txt { margin-left: 4px; }
+.sf-act .btn.txt .ic { transition: transform .2s; }
+.sf-act .btn.txt.up .ic { transform: rotate(180deg); }
+.tb-card { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
+.tb-card .tbl-wrap { flex: 1; }
+.tb-card .pager { border-top: 1px solid var(--row-bd); flex: none; }
+.tb-bar { flex: none; display: flex; align-items: center; gap: 8px; min-height: 48px; padding: 0 16px; border-bottom: 1px solid var(--card-hd-bd); }
+.tb-bar .ttl { font-size: 14px; font-weight: var(--fw-title); color: var(--t1); }
+.selbar { flex: none; display: flex; align-items: center; gap: 12px; height: 40px; padding: 0 16px; background: var(--c-primary-bg); color: var(--t1); font-size: 13px; }
+.selbar b { color: var(--c-link); font-family: var(--font-num); }
+.skel { position: relative; display: flex; flex-direction: column; gap: 12px; height: 100%; }
+.skel .card { padding: 16px 20px; }
+.skel .bar { height: 12px; border-radius: 6px; background: var(--fill-2); }
+.skel .rowb { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px 24px; }
+.skel .rowb .bar { height: 32px; border-radius: var(--r-ctl); }
+.skel .lines { flex: 1; display: flex; flex-direction: column; gap: 18px; }
+.skel-note { position: absolute; left: 50%; top: 46%; transform: translate(-50%, -50%); display: flex; align-items: center; gap: 8px; padding: 12px 18px; background: var(--bg-card); border-radius: var(--r-pop); box-shadow: var(--sh-pop); border: var(--pop-bd); color: var(--t2); font-size: 14px; white-space: nowrap; }
+.skel-note .ic { color: var(--c-primary); }
+
+/* ---------- A 经典三段:侧栏通高(颜色看皮肤的 sd-* 令牌);白色顶栏放折叠按钮、面包屑、交易日和工具;下面一行卡片式标签页 ---------- */
+.fa .sd { width: 220px; }
+.fa .brand { height: 52px; }
+.fa .brand .nm { color: var(--m-hi); }
+.fa .nb { flex: none; position: relative; z-index: 6; display: flex; align-items: center; gap: 8px; height: 52px; padding: 0 12px 0 8px; background: var(--bg-card); border-bottom: 1px solid var(--bd); }
+.fa .nb .td { margin-right: 8px; }
+.fa .tv { height: 38px; gap: 6px; padding: 0 12px; background: var(--bg-card); border-bottom: 1px solid var(--bd); }
+.fa .tvi { height: 28px; padding: 0 10px; border: 1px solid var(--bd); border-radius: var(--r-ctl); background: var(--bg-card); }
+.fa .tvi.is-act { background: var(--c-primary-bg); border-color: var(--c-primary-border); color: var(--c-link); }
+.fa .tvi.is-act::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--c-primary); }
+.fa .tvi.is-act .x { color: var(--c-link); }

+ 256 - 0
src/kit.css

@@ -0,0 +1,256 @@
+/* ============ 画布内的组件样式:全部读 .cv[data-v] 上的令牌,5 套视觉共用一份结构 ============ */
+.cv {
+  position: relative;
+  width: 1440px;
+  font-family: var(--font);
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--t2);
+  background: var(--canvas, var(--bg-page));
+  -webkit-font-smoothing: antialiased;
+  text-align: left;
+}
+.cv *, .cv *::before, .cv *::after { box-sizing: border-box; }
+.cv button, .cv input { font: inherit; color: inherit; }
+.cv .num { font-family: var(--font-num); font-variant-numeric: tabular-nums; }
+.cv .ic { width: 16px; height: 16px; flex: none; }
+.cv .t1 { color: var(--t1); }
+.cv .t3 { color: var(--t3); }
+.cv .t4 { color: var(--t4); }
+.cv a, .cv .lnk { color: var(--c-link); text-decoration: none; cursor: pointer; }
+.cv .buy { color: var(--buy); }
+.cv .sell { color: var(--sell); }
+
+/* ---------- 按钮 ---------- */
+.cv .btn {
+  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
+  height: 32px; padding: 0 15px; border-radius: var(--r-ctl);
+  border: 1px solid var(--btn-bd); background: var(--btn-bg); color: var(--btn-t);
+  box-shadow: var(--btn-sh); font-size: 14px; font-weight: var(--btn-fw);
+  white-space: nowrap; cursor: pointer; transition: background .15s, border-color .15s, color .15s, box-shadow .15s;
+}
+.cv .btn .ic { width: 14px; height: 14px; }
+.cv .btn:hover, .cv .btn.is-hover { border-color: var(--btn-bd-h); background: var(--btn-bg-h); color: var(--btn-t-h); box-shadow: var(--btn-sh-h); }
+.cv .btn.pri { background: var(--pri-bg); background-origin: border-box; border-color: var(--pri-bd); color: var(--pri-t); box-shadow: var(--btn-pri-sh); }
+.cv .btn.pri:hover, .cv .btn.pri.is-hover { background: var(--pri-bg-h); background-origin: border-box; border-color: var(--pri-bd-h); color: var(--pri-t); }
+.cv .btn.dng { color: var(--dang-t); }
+.cv .btn.dng:hover { color: var(--dang); }
+.cv .btn.dng-pri { background: var(--dang); border-color: var(--dang); color: #fff; box-shadow: none; }
+.cv .btn.txt { height: auto; padding: 0 2px; border-color: transparent; background: none; box-shadow: none; color: var(--c-link); }
+.cv .btn.txt:hover { color: var(--c-primary-hover); background: none; }
+.cv .btn.txt.dng { color: var(--dang-t); }
+.cv .btn.sm { height: 24px; padding: 0 8px; font-size: 13px; }
+.cv .btn.icon { width: 32px; padding: 0; }
+.cv .btn[disabled], .cv .btn.is-dis { opacity: .45; cursor: not-allowed; }
+.cv .btns { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
+
+/* ---------- 输入类 ---------- */
+.cv .ipt {
+  display: flex; align-items: center; gap: 6px; min-width: 0;
+  height: 32px; padding: 0 11px; border-radius: var(--r-ctl);
+  background: var(--ipt-bg); border: 1px solid var(--ipt-bd); box-shadow: var(--ipt-sh);
+  color: var(--t1); font-size: 14px; transition: background .15s, border-color .15s, box-shadow .15s; cursor: text;
+}
+.cv .ipt:hover { background: var(--ipt-bg-h); border-color: var(--ipt-bd-h); }
+.cv .ipt.is-focus, .cv .ipt:focus-within { background: var(--bg-ctl); border-color: var(--focus-bd); box-shadow: var(--ipt-sh), var(--focus); }
+.cv[data-v="e"] .ipt.is-focus, .cv[data-v="e"] .ipt:focus-within { box-shadow: var(--focus); }
+.cv .ipt.is-err { border-color: var(--dang); background: var(--bg-ctl); }
+.cv[data-v="e"] .ipt.is-err { box-shadow: 0 0 0 1px var(--dang), 0 0 0 4px rgba(223,27,65,.14); }
+.cv .ipt.is-dis { background: var(--fill-1); color: var(--t4); cursor: not-allowed; }
+.cv[data-v="a"] .ipt.is-dis { background: var(--fill-1); }
+.cv .ipt .ph { color: var(--t4); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.cv .ipt .val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.cv .ipt .ic { width: 14px; height: 14px; color: var(--t3); }
+.cv .ipt input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; padding: 0; height: 30px; color: var(--t1); }
+.cv .ipt input::placeholder { color: var(--t4); }
+.cv .ipt .sep { color: var(--t3); padding: 0 4px; }
+.cv .ipt .sfx { color: var(--t3); font-size: 13px; }
+.cv .ipt .kbd { font-size: 11px; line-height: 16px; padding: 0 5px; border-radius: 4px; color: var(--t3); border: 1px solid var(--bd); background: var(--bg-card); font-family: var(--font-num); }
+.cv .fld-err { margin-top: 4px; font-size: 12px; line-height: 18px; color: var(--dang-t); display: flex; gap: 4px; align-items: center; }
+.cv .fld-err .ic { width: 12px; height: 12px; }
+
+.cv .ck {
+  width: 16px; height: 16px; flex: none; border-radius: var(--ck-r);
+  border: 1px solid var(--ck-bd); background: var(--bg-ctl); display: inline-flex; align-items: center; justify-content: center;
+  cursor: pointer; transition: .15s; vertical-align: middle;
+}
+.cv[data-v="e"] .ck { border-color: transparent; box-shadow: 0 0 0 1px rgba(60,66,87,.2), 0 1px 1px rgba(0,0,0,.06); }
+.cv .ck svg { width: 12px; height: 12px; color: #fff; opacity: 0; }
+.cv .ck.on { background: var(--c-primary); border-color: var(--c-primary); box-shadow: none; }
+.cv .ck.on svg { opacity: 1; }
+.cv .ck.half { background: var(--c-primary); border-color: var(--c-primary); }
+.cv .ck.half::after { content: ""; width: 8px; height: 2px; background: #fff; border-radius: 1px; }
+.cv .rd { width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--ck-bd); background: var(--bg-ctl); display: inline-flex; align-items: center; justify-content: center; flex: none; }
+.cv .rd.on { border-color: var(--c-primary); border-width: 5px; }
+.cv .sw { width: 36px; height: 20px; border-radius: 10px; background: var(--sw-off); position: relative; flex: none; }
+.cv .sw::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.2); transition: .15s; }
+.cv .sw.on { background: var(--c-primary); }
+.cv .sw.on::after { left: 18px; }
+.cv[data-v="c"] .sw, .cv[data-v="a"] .sw { border-radius: 10px; }
+.cv .chk-l { display: inline-flex; align-items: center; gap: 8px; color: var(--t1); cursor: pointer; }
+
+/* ---------- 标签(状态、字典) ---------- */
+.cv .tg {
+  --x: var(--info); --x-bg: var(--info-bg); --x-t: var(--info-t); --x-bd: var(--info-bd);
+  display: inline-flex; align-items: center; gap: 6px; height: var(--tag-h); padding: 0 8px;
+  border-radius: var(--r-tag); border: 1px solid var(--x-bd); background: var(--x-bg); color: var(--x-t);
+  font-size: var(--tag-fs); font-weight: var(--tag-fw); line-height: 1; white-space: nowrap; vertical-align: middle;
+}
+.cv .tg-primary { --x: var(--c-primary); --x-bg: var(--prim-bg); --x-t: var(--prim-t); --x-bd: var(--prim-bd); }
+.cv .tg-success { --x: var(--succ); --x-bg: var(--succ-bg); --x-t: var(--succ-t); --x-bd: var(--succ-bd); }
+.cv .tg-warning { --x: var(--warn); --x-bg: var(--warn-bg); --x-t: var(--warn-t); --x-bd: var(--warn-bd); }
+.cv .tg-danger { --x: var(--dang); --x-bg: var(--dang-bg); --x-t: var(--dang-t); --x-bd: var(--dang-bd); }
+/* C:圆点+文字,不用底色;异常文字变红 */
+.cv[data-v="c"] .tg { background: none; border: 0; padding: 0; color: var(--td-t); gap: 7px; }
+.cv[data-v="c"] .tg-danger { color: var(--dang-t); }
+/* D:白底描边小标签,颜色只在圆点 */
+.cv[data-v="d"] .tg { background: #fff; color: var(--t2); padding: 0 7px; }
+.cv[data-v="c"] .tg::before, .cv[data-v="d"] .tg::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--x); flex: none; }
+/* E:胶囊 */
+.cv[data-v="e"] .tg { padding: 0 9px; }
+
+/* ---------- 卡片 ---------- */
+.cv .card { background: var(--bg-card); border: var(--card-bd); border-radius: var(--r-card); box-shadow: var(--card-sh); backdrop-filter: var(--card-bf); min-width: 0; }
+.cv .card-hd {
+  display: flex; align-items: center; gap: 12px; height: var(--card-hd-h); padding: 0 16px;
+  border-bottom: 1px solid var(--card-hd-bd); background: var(--card-hd-bg);
+  border-radius: var(--r-card) var(--r-card) 0 0;
+  color: var(--t1); font-size: 15px; font-weight: var(--fw-title);
+}
+.cv .card-hd .sub { font-size: 12px; font-weight: 400; color: var(--t3); }
+.cv .card-hd .sp { flex: 1; }
+.cv .card-bd { padding: 16px; }
+.cv[data-v="c"] .card-hd { font-size: 14px; }
+
+/* ---------- 键值 ---------- */
+.cv .kv { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 24px; margin: 0; }
+.cv .kv > div { display: flex; gap: 12px; min-width: 0; }
+.cv .kv dt { flex: none; width: 84px; color: var(--t3); }
+.cv .kv dd { margin: 0; color: var(--t1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+/* ---------- 表格 ---------- */
+.cv .tbl-wrap { overflow: auto; min-height: 0; position: relative; }
+.cv .tbl { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; font-size: 14px; }
+.cv .tbl th {
+  position: sticky; top: 0; z-index: 2; height: 40px; padding: 0 12px;
+  background: var(--th-bg); color: var(--th-t); font-weight: var(--th-fw); font-size: var(--th-fs);
+  text-align: left; white-space: nowrap; border-bottom: 1px solid var(--row-bd);
+}
+.cv .tbl td { height: 40px; padding: 0 12px; color: var(--td-t); white-space: nowrap; border-bottom: 1px solid var(--row-bd); background: var(--td-bg); }
+.cv .tbl tbody tr:nth-child(even) td { background: var(--row-stripe); }
+.cv .tbl tbody tr:hover td, .cv .tbl tbody tr.is-hover td { background: var(--row-hover); }
+.cv .tbl tbody tr.is-sel td { background: var(--row-sel); }
+.cv .tbl .r { text-align: right; }
+.cv .tbl .c { text-align: center; }
+.cv .tbl .fx { position: sticky; z-index: 1; }
+.cv .tbl th.fx { z-index: 3; }
+.cv .tbl .fx-end { box-shadow: var(--fx-sh); }
+.cv .tbl .ck { vertical-align: middle; }
+.cv .tbl .ellip { max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
+/* B:表头列之间的短分隔线 */
+.cv[data-v="b"] .tbl th:not(:last-child) { background-image: linear-gradient(#F0F0F0, #F0F0F0); background-size: 1px 22px; background-position: right center; background-repeat: no-repeat; }
+/* C:全网格 */
+.cv[data-v="c"] .tbl th, .cv[data-v="c"] .tbl td { border-right: 1px solid var(--row-bd); }
+.cv[data-v="c"] .tbl th:last-child, .cv[data-v="c"] .tbl td:last-child { border-right: 0; }
+/* D / E:表头小字 */
+.cv[data-v="e"] .tbl th { letter-spacing: .02em; }
+
+/* ---------- 分页 ---------- */
+.cv .pager { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 16px; font-size: 14px; color: var(--t2); }
+.cv .pager .total { margin-right: auto; }
+.cv .pg-b {
+  min-width: 32px; height: 32px; padding: 0 6px; display: inline-flex; align-items: center; justify-content: center;
+  border-radius: var(--r-ctl); background: var(--pg-bg); border: 1px solid var(--pg-bd); color: var(--t2); cursor: pointer;
+}
+.cv[data-v="e"] .pg-b { box-shadow: 0 0 0 1px rgba(60,66,87,.12); }
+.cv .pg-b.on { background: var(--pg-on-bg); color: var(--pg-on-t); border-color: var(--pg-on-bd); font-weight: 500; }
+.cv[data-v="e"] .pg-b.on { box-shadow: 0 0 0 1px rgba(64,128,255,.35); }
+.cv .pg-b.dis { color: var(--t4); cursor: not-allowed; }
+.cv .pg-b .ic { width: 14px; height: 14px; }
+.cv .pager .ipt { height: 32px; }
+
+/* ---------- 弹层 ---------- */
+.cv .pop { background: var(--bg-pop); backdrop-filter: var(--pop-bf); border-radius: var(--r-pop); box-shadow: var(--sh-pop); border: var(--pop-bd); padding: 4px; min-width: 160px; color: var(--t1); }
+.cv .pop-i { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 12px; border-radius: calc(var(--r-pop) - 2px); cursor: pointer; white-space: nowrap; }
+.cv .pop-i:hover, .cv .pop-i.is-hover { background: var(--fill-1); }
+.cv .pop-i.on { color: var(--c-link); font-weight: 500; }
+.cv .pop-i .ic { width: 14px; height: 14px; color: var(--t3); }
+.cv .pop-i.on .ic { color: var(--c-link); }
+.cv .pop-sep { height: 1px; margin: 4px 0; background: var(--bd); }
+.cv .pop-t { padding: 6px 12px 2px; font-size: 12px; color: var(--t3); }
+.cv .toast .ic.ok { color: var(--succ); }
+.cv .toast .ic.no { color: var(--dang); }
+.cv .toast { display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; background: var(--bg-pop); backdrop-filter: var(--pop-bf); border-radius: var(--r-pop); box-shadow: var(--sh-pop); border: var(--pop-bd); color: var(--t1); }
+.cv .tip { display: inline-block; padding: 6px 10px; font-size: 12px; line-height: 18px; color: var(--tip-t); background: var(--tip-bg); border-radius: calc(var(--r-ctl) + 2px); }
+
+/* ---------- 提示条 ---------- */
+.cv .alt {
+  --x: var(--info); --x-bg: var(--info-bg); --x-bd: var(--info-bd);
+  display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px;
+  border-radius: var(--r-ctl); background: var(--x-bg); border: 1px solid var(--x-bd); color: var(--t1); font-size: 14px;
+}
+.cv .alt .ic { width: 16px; height: 16px; color: var(--x); margin-top: 3px; }
+.cv .alt-info { --x: var(--c-primary); --x-bg: var(--prim-bg); --x-bd: var(--prim-bd); }
+.cv .alt-success { --x: var(--succ); --x-bg: var(--succ-bg); --x-bd: var(--succ-bd); }
+.cv .alt-warning { --x: var(--warn); --x-bg: var(--warn-bg); --x-bd: var(--warn-bd); }
+.cv .alt-danger { --x: var(--dang); --x-bg: var(--dang-bg); --x-bd: var(--dang-bd); }
+.cv[data-v="c"] .alt { border: 0; border-left: 3px solid var(--x); border-radius: 0 var(--r-ctl) var(--r-ctl) 0; }
+.cv[data-v="d"] .alt { background: #fff; border-color: var(--bd-ctl); }
+.cv[data-v="e"] .alt { border: 0; border-radius: 8px; }
+.cv[data-v="e"] .alt-info { background: #EEF4FF; }
+.cv[data-v="e"] .alt-success { background: #EFFBEA; }
+.cv[data-v="e"] .alt-warning { background: #FEF7E0; }
+.cv[data-v="e"] .alt-danger { background: #FFF0F5; }
+
+/* ---------- 页签(仅视觉层面,结构在第三批定) ---------- */
+.cv .tabs { display: flex; gap: 28px; border-bottom: 1px solid var(--bd); }
+.cv .tab { position: relative; height: 40px; display: inline-flex; align-items: center; color: var(--t2); cursor: pointer; }
+.cv .tab.on { color: var(--c-link); font-weight: var(--fw-strong); }
+.cv .tab.on::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--c-primary); border-radius: 1px; }
+.cv[data-v="d"] .tab.on { color: var(--t1); }
+.cv[data-v="d"] .tab.on::after { background: var(--t1); }
+.cv[data-v="e"] .tab.on::after { height: 3px; border-radius: 3px 3px 0 0; }
+.cv .tab .cnt { margin-left: 6px; font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+
+/* ---------- 弹窗外观(布局在第二批定) ---------- */
+.cv .dlg { width: 480px; background: var(--bg-dlg); backdrop-filter: var(--dlg-bf); border-radius: var(--r-dlg); box-shadow: var(--sh-dlg); overflow: hidden; }
+.cv .dlg-hd { display: flex; align-items: center; gap: 8px; height: 56px; padding: 0 20px; font-size: 16px; font-weight: var(--fw-title); color: var(--t1); border-bottom: 1px solid var(--bd); }
+.cv[data-v="d"] .dlg-hd, .cv[data-v="e"] .dlg-hd { border-bottom: 0; }
+.cv .dlg-hd .x { margin-left: auto; color: var(--t3); cursor: pointer; }
+.cv .dlg-bd { padding: 20px; color: var(--t1); }
+.cv[data-v="d"] .dlg-bd, .cv[data-v="e"] .dlg-bd { padding-top: 4px; }
+.cv .dlg-ft { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--bd); }
+.cv[data-v="d"] .dlg-ft { border-top: 0; padding-bottom: 20px; }
+.cv[data-v="e"] .dlg-ft { border-top: 0; background: var(--fill-1); }
+
+/* ---------- 空状态 ---------- */
+.cv .empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 24px; color: var(--t3); font-size: 13px; }
+.cv .empty svg { width: 64px; height: 48px; flex: none; }
+
+/* ============ 组件总览(基础视觉的第一视图) ============ */
+.cv.kit { padding: 24px 32px 40px; min-height: 900px; }
+.kit-top { display: flex; align-items: baseline; gap: 12px; margin-bottom: 20px; }
+.kit-top h1 { margin: 0; font-size: 20px; line-height: 28px; font-weight: var(--fw-title); color: var(--t1); }
+.kit-top span { color: var(--t3); font-size: 13px; }
+.kit-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
+.kit-grid > .card { grid-column: span 12; }
+.kit-grid > .s6 { grid-column: span 6; }
+.kit-grid > .s4 { grid-column: span 4; }
+.kit-grid > .s8 { grid-column: span 8; }
+.kit-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
+.kit-row + .kit-row { margin-top: 16px; }
+.kit-lbl { width: 72px; flex: none; font-size: 12px; color: var(--t3); }
+.sw-group { display: flex; flex-wrap: wrap; gap: 10px; }
+.sw-group + .sw-group { margin-top: 14px; }
+.swc { width: 112px; font-size: 12px; line-height: 16px; }
+.swc i { display: block; height: 40px; border-radius: var(--r-ctl); margin-bottom: 6px; box-shadow: inset 0 0 0 1px var(--sw-ring); }
+.swc b { display: block; color: var(--t1); font-weight: 500; }
+.swc span { color: var(--t3); font-family: var(--font-num); }
+.ty-row { display: flex; align-items: baseline; gap: 16px; padding: 8px 0; border-bottom: 1px dashed var(--bd); }
+.ty-row:last-child { border-bottom: 0; }
+.ty-row .spec { width: 132px; flex: none; font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.kit-fields { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px 20px; }
+.kit-fields .f-l { font-size: 13px; color: var(--t2); margin-bottom: 6px; }
+.kit-mask { position: relative; padding: 28px; border-radius: var(--r-card); background: var(--mask); display: flex; justify-content: center; align-items: center; min-height: 320px; overflow: hidden; }
+.kit-mask::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(0deg, rgba(255,255,255,.05) 0 40px, transparent 40px 41px); }
+.kit-mask .dlg { position: relative; }

+ 1 - 0
src/node_modules

@@ -0,0 +1 @@
+/Users/guoguo/Desktop/HeHuiDeepBlue/hehui/node_modules

+ 154 - 0
src/overlay.css

@@ -0,0 +1,154 @@
+/* ============ 弹窗 J「卡片分区」与抽屉 E「卡片+描述表」(取自分项选型 overlay.css / overlay2.css 的相关部分) ============ */
+.ovl { position: absolute; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; background: var(--mask); }
+.ovl.dr { justify-content: flex-end; align-items: stretch; }
+.ovl.lv2 { z-index: 120; }
+.ovl.lv3 { z-index: 140; }
+
+/* ---------- 弹窗骨架 ---------- */
+.dg { display: flex; flex-direction: column; max-height: 820px; background: var(--bg-dlg); backdrop-filter: var(--dlg-bf); border-radius: var(--r-dlg); box-shadow: var(--sh-dlg); overflow: hidden; color: var(--t1); }
+.dg-hd { flex: none; display: flex; align-items: center; gap: 12px; min-height: 56px; padding: 0 16px 0 24px; border-bottom: 1px solid var(--bd); }
+.dg-hd .ttl { font-size: 16px; line-height: 24px; font-weight: var(--fw-title); color: var(--t1); white-space: nowrap; }
+.dg-hd .sub { font-size: 13px; color: var(--t3); white-space: nowrap; }
+/* 标题和副标题(账户、模式、操作对象)同一行,基线对齐(2026-10-07 用户要求「一行展示」) */
+.dg-hd .row { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
+.dg-hd .icb { margin-left: auto; }
+.dg-hd .chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
+.chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border-radius: var(--r-ctl); background: var(--fill-2); color: var(--t2); font-size: 13px; white-space: nowrap; }
+.chip b { font-weight: 500; color: var(--t1); font-family: var(--font-num); font-variant-numeric: tabular-nums; }
+.dg-bd { flex: 1; min-height: 0; overflow: auto; padding: 20px 24px; position: relative; }
+.dg-ft { flex: none; display: flex; align-items: center; gap: 8px; padding: 12px 24px; border-top: 1px solid var(--bd); }
+.dg .hint { font-size: 13px; color: var(--t3); display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
+.dg .hint.warn { color: var(--warn-t); }
+.dg .hint .ic { width: 14px; height: 14px; }
+.dg .hint b { font-family: var(--font-num); font-weight: 600; }
+
+/* ---------- 表单项 ---------- */
+.req { color: var(--dang); font-style: normal; margin-right: 4px; }
+.fg + .fg { margin-top: 24px; }
+.fg-t { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; font-size: 15px; line-height: 22px; font-weight: var(--fw-title); color: var(--t1); }
+.fg-t small { font-size: 12px; font-weight: 400; color: var(--t3); }
+.fgrid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px 24px; }
+.fgrid.c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.fgrid.c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.fgrid .full { grid-column: 1 / -1; }
+.fi { display: flex; min-width: 0; }
+.fi > label { flex: none; color: var(--t2); line-height: 32px; white-space: nowrap; }
+.fi .fc { flex: 1; min-width: 0; position: relative; }
+.fi-l > label { text-align: right; padding-right: 12px; }
+.fi-l.al > label { text-align: left; }
+.fi-t { flex-direction: column; }
+.fi-t > label { line-height: 20px; margin-bottom: 6px; font-size: 13px; }
+.ro { display: flex; align-items: center; gap: 6px; min-height: 32px; color: var(--t1); }
+.ro .u { color: var(--t3); font-size: 13px; }
+.combo { display: flex; gap: 8px; }
+.checks { display: flex; align-items: center; gap: 24px; min-height: 32px; }
+.fc > .alt { margin-top: 8px; font-size: 13px; }
+.fc .fl { left: 0; right: 0; top: 36px; }
+.fc .fl .pop { min-width: 0; }
+.upl { display: flex; flex-direction: column; gap: 8px; }
+.upl-r { display: flex; align-items: center; gap: 12px; }
+.upl-tip { font-size: 12px; color: var(--t3); }
+.upl-f { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px; border-radius: var(--r-ctl); background: var(--fill-1); color: var(--t1); font-size: 13px; }
+.upl-f .ic { width: 14px; height: 14px; color: var(--t3); }
+.upl-f .sz { color: var(--t3); font-family: var(--font-num); }
+.upl-f .x { margin-left: auto; cursor: pointer; }
+.gst { margin-left: 8px; font-size: 12px; font-weight: 400; color: var(--t3); font-family: var(--font-num); white-space: nowrap; }
+.gst.ok { color: var(--succ-t); }
+.gst.bad { color: var(--dang-t); }
+
+/* 两段切换 */
+.segc { display: inline-flex; padding: 2px; border-radius: var(--r-ctl); background: var(--fill-2); }
+.segc span { min-width: 80px; height: 28px; padding: 0 14px; display: inline-flex; align-items: center; justify-content: center; border-radius: calc(var(--r-ctl) - 1px); color: var(--t2); cursor: pointer; white-space: nowrap; }
+.segc span.on { background: var(--bg-ctl); color: var(--c-link); font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,.12); }
+
+/* ---------- 弹窗 J:卡片分区 ---------- */
+.dg-j .dg-bd { background: var(--bg-page); padding: 16px; }
+.dgc { background: var(--bg-card); border: var(--card-bd); border-radius: var(--r-card); box-shadow: var(--card-sh); }
+.dgc + .dgc { margin-top: 12px; }
+.dgc-hd { display: flex; align-items: center; gap: 8px; height: 44px; padding: 0 20px; border-bottom: 1px solid var(--bd-light); }
+.dgc-hd b { font-size: 15px; font-weight: var(--fw-title); color: var(--t1); }
+.dgc-hd small { font-size: 12px; color: var(--t3); }
+.dgc-hd .gst { margin-left: 0; }
+.dgc-bd { padding: 16px 20px 20px; }
+.calcrow { display: flex; flex-direction: column; gap: 10px; }
+.calcrow > div { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; color: var(--t2); }
+.calcrow small { margin-left: 10px; font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.calcrow b { font-size: 16px; font-weight: var(--fw-title); color: var(--t1); font-family: var(--font-num); font-variant-numeric: tabular-nums; white-space: nowrap; }
+.calcrow b i { font-style: normal; font-size: 13px; font-weight: 400; color: var(--t3); margin-left: 6px; }
+
+/* ---------- 抽屉骨架 ---------- */
+.dw { display: flex; flex-direction: column; height: 100%; background: var(--bg-dlg); backdrop-filter: var(--dlg-bf); box-shadow: var(--sh-dlg); color: var(--t1); }
+.dw-hd { flex: none; display: flex; align-items: center; gap: 12px; height: 56px; padding: 0 16px 0 24px; border-bottom: 1px solid var(--bd); }
+.dw-hd .ttl { font-size: 16px; font-weight: var(--fw-title); color: var(--t1); white-space: nowrap; }
+.dw-hd .no { font-size: 15px; font-weight: 500; color: var(--t1); font-family: var(--font-num); font-variant-numeric: tabular-nums; }
+.dw-hd .tags { display: flex; gap: 6px; align-items: center; }
+.dw-bd { flex: 1; min-height: 0; overflow: auto; padding: 20px 24px; position: relative; }
+.dw-bd.gray { background: var(--bg-page); padding: 16px; }
+.dw-ft { flex: none; display: flex; align-items: center; gap: 8px; height: 56px; padding: 0 24px; border-top: 1px solid var(--bd); }
+.sec + .sec { margin-top: 28px; }
+.sec-hd { display: flex; align-items: center; gap: 8px; min-height: 32px; margin-bottom: 14px; }
+.sec-hd b { font-size: 15px; font-weight: var(--fw-title); color: var(--t1); }
+.sec-hd small { font-size: 12px; color: var(--t3); }
+.sec > .alt, .dw .alt { font-size: 13px; }
+.sec .tbl-wrap, .dw .tbl-wrap, .card .tbl-wrap.bd { border: 1px solid var(--row-bd); border-radius: var(--r-ctl); }
+.sec .tbl-wrap .tbl tr:last-child td, .dw .tbl-wrap .tbl tr:last-child td { border-bottom: 0; }
+.dw .gap { height: 12px; }
+.dw-hd .facts, .facts { display: flex; align-items: center; gap: 20px; color: var(--t3); font-size: 13px; white-space: nowrap; }
+.dw-hd .facts b, .facts b { font-weight: 400; color: var(--t1); margin-left: 6px; font-family: var(--font-num); font-variant-numeric: tabular-nums; }
+
+/* 键值排布 */
+.kvg { display: grid; gap: 12px 24px; margin: 0; }
+.kvg > div { display: flex; gap: 12px; min-width: 0; line-height: 22px; }
+.kvg dt { flex: none; color: var(--t3); white-space: nowrap; }
+.kvg dd { margin: 0; min-width: 0; color: var(--t1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.kvg.l3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.kvg.l3 dt { width: 98px; }
+.kvg .s2 { grid-column: span 2; }
+.kvg.t4 { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; }
+.kvg.t4 > div { flex-direction: column; gap: 2px; }
+.kvg.t4 dt { font-size: 12px; line-height: 18px; }
+.kvg.t4.c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.kvg.t4.c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.kvg.bd3, .kvg.bd2, .kvg.bd1 { gap: 0; border-top: 1px solid var(--bd); border-left: 1px solid var(--bd); }
+.kvg.bd3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.kvg.bd2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.kvg.bd1 { grid-template-columns: minmax(0, 1fr); }
+.kvg.bd3 > div, .kvg.bd2 > div, .kvg.bd1 > div { gap: 0; border-right: 1px solid var(--bd); border-bottom: 1px solid var(--bd); min-height: 40px; }
+.kvg.bd3 dt, .kvg.bd2 dt, .kvg.bd1 dt { width: 116px; padding: 9px 12px; background: var(--fill-1); border-right: 1px solid var(--bd); color: var(--t2); }
+.kvg.bd2 dt { width: 190px; }
+.kvg.bd1 dt { width: 132px; }
+.kvg.bd3 dd, .kvg.bd2 dd, .kvg.bd1 dd { flex: 1; padding: 9px 12px; }
+.kvg.bd3 .s2 { grid-column: span 2; }
+.kvg .up { color: var(--buy); }
+.kvg .dn { color: var(--sell); }
+
+/* 数字卡片 */
+.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
+.tile { padding: 12px 14px; border-radius: var(--r-card); background: var(--fill-1); min-width: 0; }
+.tile .l { font-size: 12px; line-height: 18px; color: var(--t3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.tile .v { margin-top: 2px; font-size: 18px; line-height: 26px; font-weight: var(--fw-title); color: var(--t1); font-family: var(--font-num); font-variant-numeric: tabular-nums; white-space: nowrap; }
+.tile .v i { font-style: normal; font-size: 12px; font-weight: 400; color: var(--t3); margin-left: 4px; }
+.tile .bar { height: 4px; margin-top: 8px; border-radius: 2px; background: var(--fill-3); overflow: hidden; }
+.tile .bar i { display: block; height: 100%; background: var(--c-primary); }
+
+/* ---------- 抽屉 E:卡片+描述表 ---------- */
+.dw-e .dw-hd { height: auto; padding-top: 12px; padding-bottom: 12px; align-items: flex-start; }
+.dw-e .dw-hd .idc { display: flex; flex-direction: column; gap: 6px; }
+.dw-e .dw-hd .idr { display: flex; align-items: center; gap: 12px; }
+.dw-e .dw-hd .no { font-size: 16px; font-weight: var(--fw-title); }
+.dw-e .dw-hd .hbtns { display: flex; gap: 8px; align-items: center; margin-top: 4px; }
+.dw-e .card { padding: 0 16px 16px; }
+.dw-e .card + .card { margin-top: 12px; }
+.dw-e .sec-hd { min-height: 48px; margin-bottom: 0; }
+.dw-e .sec-hd.sub { min-height: 36px; margin-top: 4px; }
+.dw-e .sec-hd.sub b { font-size: 13px; color: var(--t2); }
+.dw-e .sec-hd.sub::before { display: none; }
+.dw-e .dw-tabs { flex: none; display: flex; gap: 28px; padding: 0 24px; border-bottom: 1px solid var(--bd); background: var(--bg-card); }
+.dw-e .dw-tabs .tab { height: 42px; }
+.kvline { display: flex; flex-wrap: wrap; gap: 6px 28px; color: var(--t3); }
+.kvline span { white-space: nowrap; }
+.kvline b { margin-left: 8px; font-weight: 400; color: var(--t1); font-family: var(--font-num); font-variant-numeric: tabular-nums; }
+.sumkv { margin: 0; display: flex; flex-direction: column; gap: 8px; font-size: 13px; line-height: 20px; }
+.sumkv > div { display: flex; justify-content: space-between; gap: 12px; }
+.sumkv dt { flex: none; color: var(--t3); white-space: nowrap; }
+.sumkv dd { margin: 0; min-width: 0; color: var(--t1); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

+ 28 - 0
src/pack.py

@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+"""构建 → 跑点击测试 → 打包发给同事。测试不通过不出包。用法:python3 pack.py"""
+import subprocess
+import sys
+import zipfile
+from datetime import date
+from pathlib import Path
+
+SRC = Path(__file__).resolve().parent
+ROOT = SRC.parent
+HTML = ROOT / "CMS-UI完整设计.html"
+
+subprocess.run([sys.executable, str(SRC / "build.py")], check=True)
+if subprocess.run(["node", str(SRC / "test.mjs")], cwd=SRC).returncode != 0:
+    sys.exit("点击测试没通过,不出包。")
+
+out_dir = ROOT / "发给同事"
+out_dir.mkdir(exist_ok=True)
+name = "CMS-UI完整设计"
+zpath = out_dir / f"{name}-{date.today():%Y%m%d}.zip"
+guide = "" + (SRC / "使用说明.txt").read_text(encoding="utf-8").replace("\n", "\r\n")
+with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z:
+    z.write(HTML, f"{name}/{HTML.name}")
+    info = zipfile.ZipInfo(f"{name}/使用说明.txt", date_time=zipfile.ZipInfo.from_file(HTML).date_time)
+    info.compress_type = zipfile.ZIP_DEFLATED
+    info.external_attr = 0o644 << 16
+    z.writestr(info, guide.encode("utf-8"))
+print("已打包", zpath.relative_to(ROOT), "%.0f KB" % (zpath.stat().st_size / 1024))

+ 279 - 0
src/pages.css

@@ -0,0 +1,279 @@
+/* ============ 完整设计稿新增页面的样式(首页、页内页签、清算、TRS 下单、票据详情、确认框、个人中心、404/401、登录、反馈类) ============ */
+
+/* 多选的值显示成小标签 */
+.cv .ipt .chips { display: flex; gap: 4px; flex: 1; min-width: 0; overflow: hidden; }
+.cv .chipv { display: inline-flex; align-items: center; height: 22px; padding: 0 6px; border-radius: var(--r-tag); background: var(--fill-2); color: var(--t2); font-size: 12px; white-space: nowrap; }
+.cv .btn.ok-pri { background: var(--sell); border-color: var(--sell); color: #fff; box-shadow: none; }
+.cv .btn.dng-pri, .cv .btn.ok-pri { box-shadow: var(--btn-pri-sh); }
+.cv .btn.is-loading { opacity: .7; cursor: progress; }
+.cv .spin { display: inline-block; width: 14px; height: 14px; border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent; animation: cv-spin .8s linear infinite; }
+.cv .spin.big { width: 24px; height: 24px; color: var(--c-primary); }
+.cv .icb.sm { width: 24px; height: 24px; }
+.cv .icb.sm .ic { width: 14px; height: 14px; }
+.cv .ic.gender { width: 14px; height: 14px; color: var(--c-link); margin-left: 6px; vertical-align: -2px; }
+.cv .fld-tip { margin-top: 4px; font-size: 12px; line-height: 18px; color: var(--t3); }
+.cv .chip b { margin-left: 6px; }
+.cv .chip .tg { margin-left: 6px; }
+
+/* ---------- 页内页签 ---------- */
+.ptabs { display: flex; gap: 28px; flex: none; padding: 0 4px; border-bottom: 1px solid var(--bd); }
+.ptabs .tab { height: 40px; font-size: 14px; }
+.ctabs { flex: none; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 0 16px; border-bottom: 1px solid var(--card-hd-bd); }
+.ctabs .tabs { border-bottom: 0; align-self: stretch; }
+.ctabs .tab { height: 48px; }
+.ctabs .tab.on::after { bottom: 0; }
+.cv .tab .cnt { margin-left: 6px; font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.cv .tab.on .cnt { color: var(--c-link); }
+
+/* ---------- 首页工作台 ---------- */
+.hm { display: grid; grid-template-columns: minmax(0, 1fr) 352px; grid-template-rows: auto minmax(0, 1fr); gap: 12px; height: 100%; min-height: 0; }
+.hm .card-hd .sub b { color: var(--t1); font-weight: 600; }
+.hm .card-hd .sub b.hot { color: var(--c-link); }
+.hm-st { grid-column: 1; grid-row: 1; }
+.hm-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 10px; padding: 16px; }
+.hm-tile { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border: 1px solid var(--bd); border-radius: var(--r-card); background: var(--fill-1); cursor: pointer; min-width: 0; transition: border-color .15s, background .15s; }
+.hm-tile:hover { border-color: var(--c-primary); background: var(--bg-card); }
+.hm-tile .l { font-size: 12px; line-height: 18px; color: var(--t3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.hm-tile b { font-size: 22px; line-height: 28px; font-weight: 600; color: var(--t1); }
+.hm-tile.hot { background: var(--bg-card); border-color: var(--c-primary-border); box-shadow: inset 3px 0 0 var(--c-primary); }
+.hm-tile.hot b { color: var(--c-link); }
+.hm-tile.zero b { color: var(--t4); }
+.hm-sc-card { grid-column: 2; grid-row: 1; }
+.hm-scs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; padding: 16px; }
+.hm-sc { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 12px 6px 10px; border: 1px solid var(--bd); border-radius: var(--r-card); background: var(--fill-1); cursor: pointer; text-align: center; min-width: 0; transition: border-color .15s, background .15s; }
+.hm-sc:hover { border-color: var(--c-primary-border); background: var(--c-primary-bg); }
+.hm-sc .ic { width: 20px; height: 20px; color: var(--c-primary); margin-bottom: 2px; }
+.hm-sc b { font-size: 13px; font-weight: 500; color: var(--t1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
+.hm-sc small { font-size: 11px; color: var(--t3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
+.hm-tb { grid-column: 1 / -1; grid-row: 2; }
+
+/* ---------- 清算 ---------- */
+.clr { display: flex; flex-direction: column; height: 100%; min-height: 0; }
+.clr > .card-bd { flex: 1; min-height: 0; overflow: auto; }
+.clr-info { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
+.cv .tbl tbody tr.is-sel td { font-weight: 500; }
+
+/* ---------- TRS 交易下单页 ---------- */
+.trs-acct { display: flex; align-items: center; gap: 8px; padding: 12px 16px; }
+.trs-acct .sf-i > label { color: var(--t2); }
+
+/* ---------- 短表单弹窗(2026-10-08 用户给的下单弹窗布局):标签在左、单列;顶部高亮信息块;底部灰色结果块 ---------- */
+.dg-s .dg-hd { border-bottom: 0; padding-top: 4px; }
+.dg-s .dg-bd { padding: 8px 24px 24px; display: flex; gap: 20px; }
+.dg-s .sf-main { flex: 1; min-width: 0; }
+.sf-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 20px; }
+.sf-tile { padding: 12px 16px; border-radius: var(--r-card); background: var(--c-primary-bg); min-width: 0; }
+.sf-tile .l { display: block; font-size: 13px; color: var(--t2); }
+.sf-tile b { display: block; margin-top: 2px; font-size: 20px; line-height: 28px; font-weight: 600; color: var(--c-link); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.sf-tile b i { font-style: normal; font-size: 13px; font-weight: 400; color: var(--c-link); opacity: .7; margin-left: 4px; }
+.sf-rows { display: flex; flex-direction: column; gap: 18px; }
+.sf-row { display: flex; gap: 16px; align-items: flex-start; }
+.sf-row > label { flex: none; width: var(--lw); text-align: right; line-height: 32px; color: var(--t2); white-space: nowrap; }
+.sf-c { flex: 1; min-width: 0; }
+/* 标签在上:标签独占一行,结果块不再空出标签列 */
+.dg-s.lt .sf-rows { gap: 16px; }
+.dg-s.lt .sf-row { flex-direction: column; align-items: stretch; gap: 6px; }
+.dg-s.lt .sf-row > label { width: auto; text-align: left; line-height: 20px; font-size: 13px; }
+.dg-s.lt .sf-row > label:empty { display: none; }
+.dg-s.lt .sf-res { margin-top: 6px; }
+.sf-res { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 2px; }
+.sf-row + .sf-row .sf-res { margin-top: 0; }
+.sf-rt { padding: 12px 16px; background: var(--fill-1); border-radius: var(--r-card); min-width: 0; }
+.sf-rt .l { display: block; font-size: 12px; color: var(--t3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.sf-rt > small { display: block; margin-top: 2px; font-size: 12px; color: var(--t4); }
+.sf-rt b { display: block; margin-top: 2px; font-size: 18px; line-height: 26px; font-weight: 600; color: var(--t1); white-space: nowrap; }
+.sf-rt b i { font-style: normal; font-size: 12px; font-weight: 400; color: var(--t3); margin-left: 4px; }
+.cv .stp { display: flex; align-items: center; height: 32px; border: 1px solid var(--ipt-bd); border-radius: var(--r-ctl); background: var(--ipt-bg); overflow: hidden; }
+.cv .stp:hover { border-color: var(--ipt-bd-h); }
+.cv .stp.is-err { border-color: var(--dang); }
+.cv .stp-b { flex: none; width: 32px; align-self: stretch; display: inline-flex; align-items: center; justify-content: center; background: var(--fill-1); color: var(--t2); font-size: 16px; cursor: pointer; user-select: none; }
+.cv .stp-b:first-child { border-right: 1px solid var(--bd); }
+.cv .stp-b:last-child { border-left: 1px solid var(--bd); }
+.cv .stp-b:hover { background: var(--fill-2); color: var(--c-link); }
+.cv .stp .val { flex: 1; text-align: center; font-weight: 600; color: var(--t1); }
+.cv .stp .sfx { padding-right: 10px; color: var(--t3); font-size: 13px; }
+.qchips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
+.qchip { display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 10px; border: 1px solid var(--bd-ctl); border-radius: var(--r-ctl); background: var(--bg-card); font-size: 12px; color: var(--t2); cursor: pointer; white-space: nowrap; }
+.qchip b { font-weight: 500; color: var(--t1); }
+.qchip:hover { border-color: var(--c-primary-hover); color: var(--c-link); }
+.qchip.on { border-color: var(--c-primary); background: var(--c-primary-bg); color: var(--c-link); }
+.qchip.on b { color: var(--c-link); }
+
+/* 右侧五档行情 */
+.qpanel { flex: none; width: 280px; display: flex; flex-direction: column; padding: 14px 16px 10px; background: var(--fill-1); border: 1px solid var(--bd); border-radius: var(--r-card); }
+.qname { display: flex; align-items: baseline; gap: 8px; }
+.qname b { font-size: 16px; font-weight: 600; color: var(--t1); }
+.qname .code { font-size: 13px; color: var(--t3); }
+.qname .live { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--t3); white-space: nowrap; }
+.qname .live::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--succ); }
+.qprice { display: flex; align-items: baseline; gap: 10px; margin-top: 4px; }
+.qprice .last { font-size: 26px; line-height: 32px; font-weight: 600; }
+.qprice .chg { font-size: 13px; }
+.qsub { margin: 2px 0 8px; font-size: 12px; color: var(--t3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.qsub b { font-weight: 400; color: var(--t2); }
+.ql { display: grid; grid-template-columns: 32px 56px minmax(0, 1fr); gap: 8px; align-items: center; height: 28px; padding: 0 6px; border-radius: 4px; font-size: 13px; cursor: pointer; }
+.ql:hover { background: var(--fill-2); }
+.ql.on { background: var(--c-primary-bg); }
+.ql > span:first-child { color: var(--t3); }
+.ql b { font-weight: 500; }
+.ql .vol { position: relative; text-align: right; height: 20px; line-height: 20px; }
+.ql .vol em { position: absolute; right: 0; top: 2px; bottom: 2px; border-radius: 3px; opacity: .16; }
+.ql.ask .vol em { background: var(--sell); }
+.ql.bid .vol em { background: var(--buy); }
+.ql .vol i { position: relative; font-style: normal; color: var(--t1); padding-right: 4px; }
+.qmid { display: flex; align-items: baseline; gap: 8px; margin: 6px 0; padding: 6px 6px; border-top: 1px dashed var(--bd-ctl); border-bottom: 1px dashed var(--bd-ctl); }
+.qmid b { font-size: 16px; font-weight: 600; }
+.qmid span { margin-left: auto; font-size: 13px; }
+
+/* ---------- 票据详情抽屉 ---------- */
+.dw-e .dw-hd .hbtns { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
+.dw-e .dw-hd .hbtns.col { flex-direction: column; align-items: flex-end; gap: 6px; margin-top: 0; }
+.dw-e .dw-hd .hbtns .r { display: flex; align-items: center; gap: 8px; }
+.cv .tbl td.wrap { white-space: normal; line-height: 20px; padding-top: 9px; padding-bottom: 9px; }
+.cv .kvg.bd2.n dt { width: 116px; }
+.dw-e .card .tbl-wrap + .sec-hd.sub { margin-top: 12px; }
+.dw-e .card .kvg + .sec-hd.sub { margin-top: 10px; }
+
+/* ---------- 确认框 ---------- */
+.cfm { width: 420px; background: var(--bg-dlg); border-radius: var(--r-dlg); box-shadow: var(--sh-dlg); color: var(--t1); }
+/* 状态图标和标题同一行,下面的说明和描述表与图标左对齐,不缩进 */
+.cfm-bd { padding: 22px 24px 18px; }
+.cfm-hd { display: flex; align-items: center; gap: 10px; }
+.cfm-hd b { font-size: 16px; line-height: 24px; font-weight: var(--fw-title); color: var(--t1); }
+.cfm-ic { flex: none; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; }
+.cfm-ic .ic { width: 24px; height: 24px; }
+.cfm-ic.danger { color: var(--dang); }
+.cfm-ic.warning { color: var(--warn); }
+.cfm-ic.info { color: var(--c-primary); }
+.cfm-bd > p { margin: 10px 0 0; color: var(--t2); }
+.cfm-kv { margin: 12px 0 0; display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--bd); }
+.cfm-kv > div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; font-size: 13px; border-bottom: 1px solid var(--bd-light); }
+.cfm-kv dt { color: var(--t3); white-space: nowrap; }
+.cfm-kv dd { margin: 0; color: var(--t1); text-align: right; }
+.cfm-ft { display: flex; justify-content: flex-end; gap: 8px; padding: 0 24px 20px; }
+
+/* ---------- 个人中心 ---------- */
+.prof { display: grid; grid-template-columns: 360px minmax(0, 1fr); gap: 12px; align-items: start; }
+.prof-id { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 36px 24px; }
+.prof-ava { position: relative; cursor: pointer; }
+.cv .ava.big { width: 88px; height: 88px; font-size: 32px; }
+.prof-cam { position: absolute; right: -2px; bottom: 2px; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--bg-card); border: 1px solid var(--bd-ctl); color: var(--t2); }
+.prof-cam .ic { width: 14px; height: 14px; }
+.prof-nm { display: flex; align-items: center; gap: 4px; margin-top: 8px; }
+.prof-nm b { font-size: 18px; font-weight: 600; color: var(--t1); }
+.prof-role { font-size: 13px; color: var(--t3); }
+.prof-r { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
+.prof-sec { display: flex; flex-direction: column; }
+.prof-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 16px; border-bottom: 1px solid var(--bd); }
+.prof-row:last-child { border-bottom: 0; }
+.prof-row b { display: block; font-weight: 500; color: var(--t1); }
+.prof-row small { display: block; margin-top: 2px; font-size: 12px; color: var(--t3); }
+
+/* ---------- 404 / 401 ---------- */
+.errp { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; height: 100%; min-height: 560px; }
+.err-art { width: 420px; height: 160px; border-radius: var(--r-card); background: var(--bg-card); box-shadow: var(--card-sh); }
+.err-t { text-align: center; }
+.err-t b { display: block; font-size: 20px; line-height: 28px; font-weight: 600; color: var(--t1); }
+.err-t p { margin: 8px 0 20px; color: var(--t2); }
+
+/* ---------- 登录页:分项选型 C「右侧浮卡·全幅底图」(表单卡片靠右浮着,左侧一列是系统名、业务模块、三端入口) ---------- */
+.cv.login { height: 900px; overflow: hidden; }
+.lg { position: relative; height: 100%; display: flex; align-items: center; justify-content: flex-end; padding-right: 96px; background: var(--bg-page); color: var(--t2); }
+.lg-actions { position: absolute; top: 20px; right: 28px; z-index: 5; display: flex; gap: 4px; }
+.lg-actions .icb { color: var(--t3); }
+.lg-deco { position: absolute; inset: 0; width: 100%; height: 100%; color: var(--c-primary); pointer-events: none; }
+.lg-deco path, .lg-deco circle { fill: none; stroke: currentColor; stroke-width: 1.5; }
+.lg-logo { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 36px; height: 36px; border-radius: calc(var(--r-ctl) + 4px); background: var(--logo-bg); color: var(--logo-t); font-size: 17px; font-weight: 600; }
+.lg-logo.sm { width: 28px; height: 28px; font-size: 14px; border-radius: calc(var(--r-ctl) + 2px); }
+.lg-brand { display: flex; align-items: center; gap: 12px; white-space: nowrap; }
+.lg-brand b { font-size: 20px; font-weight: 600; color: var(--t1); letter-spacing: .02em; }
+.lg-brand.sm b { font-size: 15px; }
+/* 左侧一列:垂直居中,和右边的卡片对齐 */
+.lg-c-left { position: absolute; left: 72px; top: 0; bottom: 0; width: 440px; display: flex; flex-direction: column; justify-content: center; gap: 32px; }
+.lg-c-sys { display: block; font-size: 32px; line-height: 40px; font-weight: 600; color: var(--t1); letter-spacing: .02em; }
+.lg-c-ver { margin-top: 4px; font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.lg-mods-t { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 12px; color: var(--t3); }
+.lg-mods-t span { font-family: var(--font-num); }
+.lg-chips { display: flex; flex-wrap: wrap; gap: 6px; }
+.lg-chip { display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border: 1px solid var(--prim-bd); border-radius: var(--r-tag); background: var(--prim-bg); color: var(--prim-t); font-size: 12px; white-space: nowrap; }
+.lg-ents { display: flex; flex-direction: column; gap: 8px; }
+.lg-ents-t { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; font-size: 12px; color: var(--t3); }
+.lg-ents-t span { color: var(--t4); }
+.lg-ent { display: flex; align-items: center; gap: 12px; height: 60px; padding: 0 14px; border-radius: var(--r-card); background: var(--bg-card); border: var(--card-bd); box-shadow: var(--card-sh); color: var(--t1); cursor: pointer; transition: box-shadow .15s, transform .15s; }
+.lg-ent:hover { box-shadow: var(--sh-pop); transform: translateX(2px); }
+.lg-ent.on { cursor: default; outline: 1.5px solid var(--c-primary); outline-offset: -1.5px; }
+.lg-ent.on:hover { box-shadow: var(--card-sh); transform: none; }
+.lg-ent-n { display: flex; flex-direction: column; min-width: 0; line-height: 18px; }
+.lg-ent-n b { font-size: 14px; font-weight: 600; white-space: nowrap; }
+.lg-ent-n small { font-size: 12px; color: var(--t3); white-space: nowrap; }
+.lg-ent-go { margin-left: auto; display: inline-flex; align-items: center; gap: 2px; font-size: 13px; color: var(--c-link); white-space: nowrap; }
+.lg-ent-go .ic { width: 14px; height: 14px; }
+.lg-ent.on .lg-ent-go { color: var(--t3); }
+/* 右侧浮卡 */
+.lg-card { position: relative; width: 440px; padding: 36px 40px 32px; background: var(--bg-dlg); border-radius: var(--r-dlg); box-shadow: var(--sh-dlg); color: var(--t1); }
+.lg-card .lg-brand { margin-bottom: 24px; }
+.lg-ttl { font-size: 22px; line-height: 30px; font-weight: 600; color: var(--t1); margin-bottom: 20px; }
+.lg-p { margin: -12px 0 20px; font-size: 13px; line-height: 20px; color: var(--t3); }
+.lg-fi { margin-bottom: 18px; }
+.lg-fi > label { display: block; margin-bottom: 6px; font-size: 13px; line-height: 18px; color: var(--t2); }
+.cv .lg-ipt { height: 40px; padding: 0 12px; }
+.cv .lg-ipt > .ic { width: 16px; height: 16px; }
+.cv .lg-ipt .val { color: var(--t1); }
+.lg-eye { display: inline-flex; margin-left: auto; cursor: pointer; color: var(--t3); }
+.lg-eye:hover { color: var(--t1); }
+.lg-eye .ic { width: 16px; height: 16px; }
+.lg-cap-row { display: flex; gap: 10px; align-items: center; }
+.lg-cap { flex: none; width: 120px; height: 40px; border: 1px solid var(--bd-ctl); border-radius: var(--r-ctl); background: var(--fill-2); cursor: pointer; overflow: hidden; }
+.lg-cap:hover { border-color: var(--ipt-bd-h); }
+.lg-cap svg { display: block; width: 100%; height: 100%; }
+.lg-cap text { font-family: var(--font-num); }
+.lg-row { display: flex; align-items: center; justify-content: space-between; margin: -2px 0 20px; font-size: 13px; }
+.lg-row .chk-l { font-size: 13px; }
+.lg-row.center { justify-content: center; gap: 6px; margin: 16px 0 0; }
+.cv .lg-btn { width: 100%; height: 40px; font-size: 15px; }
+
+/* ---------- 反馈类总览 ---------- */
+.fb-note { margin-top: 12px; font-size: 12px; line-height: 18px; color: var(--t3); }
+.fb-msgs { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 8px 0; }
+.cv .toast { max-width: 100%; }
+.cv .toast .x { margin-left: 8px; color: var(--t3); cursor: pointer; display: inline-flex; }
+.cv .toast .x .ic { width: 14px; height: 14px; }
+.cv .toast .ic.success { color: var(--succ); }
+.cv .toast .ic.error { color: var(--dang); }
+.cv .toast .ic.warning { color: var(--warn); }
+.cv .toast .ic.info { color: var(--c-primary); }
+.ntf { width: 360px; margin: 0 0 0 auto; padding: 14px 16px; background: var(--bg-pop); border: var(--pop-bd); box-shadow: var(--sh-pop); border-radius: var(--r-pop); }
+.ntf-hd { display: flex; align-items: center; gap: 8px; }
+.ntf-hd .ic { width: 18px; height: 18px; }
+.ntf-hd .ic.ok { color: var(--succ); }
+.ntf-hd b { flex: 1; font-size: 14px; font-weight: 600; color: var(--t1); }
+.ntf-hd small { font-size: 12px; color: var(--t3); font-family: var(--font-num); }
+.ntf-hd .x { color: var(--t3); cursor: pointer; display: inline-flex; }
+.ntf-hd .x .ic { width: 14px; height: 14px; }
+.ntf p { margin: 8px 0 6px; font-size: 13px; line-height: 20px; color: var(--t2); }
+.ntf .lnk { font-size: 13px; display: inline-flex; align-items: center; gap: 2px; }
+.ntf .lnk .ic { width: 12px; height: 12px; }
+.fb-alts { display: flex; flex-direction: column; gap: 10px; }
+.cv .alt.titled { align-items: flex-start; }
+.cv .alt.titled > div { flex: 1; }
+.cv .alt.titled b { display: block; font-weight: 600; }
+.cv .alt.titled p { margin: 2px 0 0; font-size: 13px; color: var(--t2); }
+.cv .alt .x { margin-left: auto; color: var(--t3); cursor: pointer; display: inline-flex; margin-top: 3px; }
+.cv .alt .x .ic { width: 14px; height: 14px; color: var(--t3); margin: 0; }
+.fb-empties { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
+.fb-lbl { font-size: 12px; color: var(--t3); margin-bottom: 6px; }
+.cv .empty.box { height: 200px; justify-content: center; border: 1px solid var(--bd); border-radius: var(--r-card); background: var(--fill-1); }
+.cv .empty small { font-size: 12px; color: var(--t4); }
+.cv .empty .btn.sm { margin-top: 4px; }
+.fb-load { display: flex; gap: 20px; }
+.fb-load .tbl-wrap { flex: 1; min-width: 0; }
+.fb-load-r { flex: none; width: 240px; }
+.cv .loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; background: var(--veil); color: var(--t2); font-size: 13px; z-index: 5; }
+.fb-results { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
+.result { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 28px 16px; border: 1px solid var(--bd); border-radius: var(--r-card); background: var(--fill-1); }
+.res-ic { width: 56px; height: 56px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; border: 2px solid currentColor; margin-bottom: 14px; }
+.res-ic .ic { width: 30px; height: 30px; }
+.res-ic.success { color: var(--succ); }
+.res-ic.error { color: var(--dang); }
+.result b { font-size: 18px; line-height: 26px; font-weight: 600; color: var(--t1); }
+.result p { margin: 6px 0 18px; font-size: 13px; color: var(--t2); max-width: 360px; }

+ 355 - 0
src/pages.js

@@ -0,0 +1,355 @@
+/* 完整设计稿新增的页面与弹层:首页、页内标签页(风险监控)、清算、TRS 交易下单页与下单弹窗、票据详情抽屉、
+ * 确认框、修改密码弹窗、个人中心、404/401、登录页、组件总览、反馈类总览。登记到 UI.pageRenderers / overlayRenderers / canvasRenderers。
+ */
+(function () {
+  "use strict";
+  const U = window.UI;
+  const { D, D2: P, T, esc, ic, CHECK, EMPTY, tag, ENDS, kv, tableHTML, btn, item, dgWrap, dgCard, sfDialog, stepper, closeX, btnCancel, drCard } = U;
+
+  /* ================= 首页(工作台) ================= */
+  U.pageRenderers.__home = function (st) {
+    const e = ENDS[st.end];
+    const H = P.home[st.end];
+    const total = H.status.reduce((n, s) => n + s[1], 0);
+    const hot = H.status.filter((s) => s[2]).reduce((n, s) => n + s[1], 0);
+    const tiles = H.status.map(([n, c, h]) => `<div class="hm-tile ${h ? "hot" : ""} ${c === 0 ? "zero" : ""}" data-act="leaf" data-k="${H.listKey}" title="打开${H.noun}列表"><span class="l">${n}</span><b class="num">${c}</b></div>`).join("");
+    const statusCard = `<div class="card hm-st"><div class="card-hd">上午好,admin<span class="sub">今日${H.noun}共 <b class="num">${total}</b> 笔,其中 <b class="num hot">${hot}</b> 笔需要处理(高亮的状态)</span><span class="sp"></span><span class="btn txt" data-act="leaf" data-k="${H.listKey}">查看全部${ic("right")}</span></div><div class="hm-tiles">${tiles}</div></div>`;
+    const sc = H.shortcuts.map(([g, l, icn]) => `<div class="hm-sc" data-act="leaf" data-k="${esc(g + "/" + l)}">${ic(icn)}<b>${esc(l)}</b><small>${esc(g)}</small></div>`).join("");
+    const scCard = `<div class="card hm-sc-card"><div class="card-hd">快捷入口<span class="sub">取自本系统菜单</span></div><div class="hm-scs">${sc}</div></div>`;
+    const rows = e.rows.filter(H.pick).slice(0, 6);
+    const cols = H.manualCols.map((c) => [c[0], c[1], c[4] === "r" ? "r" : ""]).concat([["操作", 72]]);
+    const cell = (r, i) => {
+      const def = H.manualCols[i];
+      if (!def) return `<span class="btn txt" data-act="${st.end === "issue" ? "ovopen" : "leaf"}" data-o="order" data-k="${H.listKey}">详情</span>`;
+      const key = def[2];
+      const v = key === "stock" && st.end === "trs" ? `${r.code} ${r.sname}` : r[key];
+      const kind = def[3] || "";
+      if (kind.startsWith("tag:")) return v ? tag(kind.slice(4), v) : "--";
+      return v;
+    };
+    const tblCard = `<div class="card tb-card hm-tb"><div class="tb-bar"><span class="ttl">${H.manualTitle}</span><span class="t3" style="font-size:12px">${H.manualSub};按下单时间倒序,最多 6 条</span><span class="sp"></span><span class="btn txt" data-act="leaf" data-k="${H.listKey}">查看全部${ic("right")}</span></div>${tableHTML(cols, rows, { cell, keep: "hm-tbl", empty: "没有需要处理的" + H.noun })}</div>`;
+    return `<div class="hm">${statusCard}${scCard}${tblCard}</div>`;
+  };
+
+  /* ================= 页内标签页 ================= */
+  // 页面级(ptabs)或卡片内(tabs)的页签条;选中项记在 st.tab[g]
+  const ptabs = (st, g, tabs, cls = "ptabs") => {
+    const cur = st.tab[g] || tabs[0][0];
+    return `<div class="${cls}">${tabs.map(([k, l]) => `<span class="tab ${k === cur ? "on" : ""}" data-act="ptab" data-g="${g}" data-v="${k}">${l}</span>`).join("")}</div>`;
+  };
+  const searchCard = (fields, actions, withLabel) => {
+    const lw = withLabel ? Math.max(...fields.map((f) => f.label.length)) * 14 : 0;
+    const rem = 3 - (fields.length % 3);
+    const acts = `<div class="sf-act" style="grid-column:span ${rem}">${actions.map(([t, type, icn]) => `<span class="btn ${type === "primary" ? "pri" : ""}" data-act="noop">${ic(icn)}${t}</span>`).join("")}</div>`;
+    return `<div class="card"><div class="sf">${fields.map((f) => `<div class="sf-i">${withLabel ? `<label style="width:${lw}px">${esc(f.label)}</label>` : ""}${U.ctlHTML(f)}</div>`).join("")}${acts}</div></div>`;
+  };
+
+  // 风险监控:页面级页签,三个页签各是一套搜索 + 表格
+  U.pageRenderers["风控管理/共赢票据风险监控"] = function (st) {
+    const R = P.risk;
+    const cur = st.tab.risk || "monitor";
+    let body;
+    if (cur === "monitor") {
+      const cell = (r, i) => {
+        if (i === 0) {
+          const danger = r[2] === "危险" || r[2] === "强平";
+          return `<span class="btns" style="gap:12px"><span class="btn txt" data-act="ovopen" data-o="note">详情</span>${danger ? `<span class="btn txt dng" data-act="ovopen" data-o="confirm" data-kind="force">强平卖出</span>` : ""}</span>`;
+        }
+        const v = r[i - 1];
+        if (i === 2) return [v, "note_status"];
+        if (i === 3) return [v, "note_risk_status"];
+        if (i === 11) return v.startsWith("+") ? [v, "up"] : v.startsWith("-") ? [v, "dn"] : v;
+        return v;
+      };
+      body = `${searchCard(R.search, R.actions, false)}<div class="card tb-card">${tableHTML(R.cols, R.rows, { cell, keep: "risk-tbl" })}${U.pagerHTML(R.rows.length)}</div>`;
+    } else if (cur === "status-log") {
+      const cell = (r, i) => (i === 4 || i === 5 ? [r[i], "note_risk_status"] : r[i]);
+      body = `${searchCard(R.search.slice(2, 8), R.actions.slice(0, 2), false)}<div class="card tb-card">${tableHTML(R.statusLogCols, R.statusLog, { cell, keep: "risk-log" })}${U.pagerHTML(R.statusLog.length)}</div>`;
+    } else {
+      const cell = (r, i) => (i === 4 ? [r[i], "send_status"] : r[i]);
+      body = `${searchCard(R.search.slice(2, 5), R.actions.slice(0, 2), false)}<div class="card tb-card">${tableHTML(R.notifyCols, R.notify, { cell, keep: "risk-ntf" })}${U.pagerHTML(R.notify.length)}</div>`;
+    }
+    return `<div class="pg-list">${ptabs(st, "risk", R.tabs)}${body}</div>`;
+  };
+  // 销售端同名菜单复用
+  U.pageRenderers["风控管理/共赢票据风险监控"].sales = true;
+
+  // 清算管理:页签在卡片标题栏里
+  U.pageRenderers["清算管理/共赢票据清算"] = function (st) {
+    const C = P.clearing;
+    const cur = st.tab.clr || "clearing";
+    const running = C.batchStatus === "执行中";
+    const tools = cur === "clearing"
+      ? `${btn(ic("refresh") + "刷新")}${btn("一键清算", running ? "pri is-dis" : "pri")}${btn("重新清算", "", "ovopen", 'data-o="confirm" data-kind="redo"')}`
+      : `${btn(ic("refresh") + "刷新")}${running ? btn(ic("stop") + "停止当前任务", "dng", "ovopen", 'data-o="confirm" data-kind="stop"') : ""}`;
+    const info = `<div class="clr-info"><span class="chip">市场<b>${C.market}</b></span><span class="chip">本次清算交易日<b>${C.tradeDate}</b></span><span class="chip">批次<b>${C.batchNo}</b></span><span class="chip">状态${tag("clearing_batch", C.batchStatus)}</span>${running ? `<span class="t3" style="font-size:12px;margin-left:4px">执行中不能再次发起;页面每 5 秒自动刷新</span>` : ""}</div>`;
+    let body;
+    if (cur === "clearing") {
+      const cell = (r, i) => (i === 2 ? [r[i], "clearing_step"] : r[i]);
+      const rowCls = (r) => (r[2] === "执行中" ? "is-sel" : "");
+      body = `${info}${C.blockedReason ? `<div class="alt alt-info" style="margin-bottom:12px">${ic("info")}<span>${esc(C.blockedReason)}</span></div>` : ""}${tableHTML(C.stepCols, C.steps, { cell, rowCls, keep: "clr-steps", cls: "bd" })}${C.errorDetail ? `<div class="alt alt-danger" style="margin-top:12px">${ic("error")}<span>${esc(C.errorDetail)}</span></div>` : ""}`;
+    } else {
+      const cell = (r, i) => (i === 3 ? [r[i], "clearing_step"] : r[i]);
+      const hcell = (r, i) => (i === 3 ? [r[i], "clearing_batch"] : r[i]);
+      body = `${info}${tableHTML(C.logCols, C.logs, { cell, keep: "clr-logs", cls: "bd" })}<div class="sec-hd" style="margin:20px 0 10px"><b>${C.historyTitle}</b><small>${C.history.length} 条</small></div>${tableHTML(C.historyCols, C.history, { cell: hcell, keep: "clr-his", cls: "bd" })}`;
+    }
+    return `<div class="card clr"><div class="ctabs">${ptabs(st, "clr", C.tabs, "tabs")}<span class="sp"></span>${tools}</div><div class="card-bd" data-keep="clr-bd">${body}</div></div>`;
+  };
+
+  /* ================= TRS · 交易下单页 + 下单弹窗 ================= */
+  U.pageRenderers["系统设置/交易下单"] = function (st) {
+    const H = D.trsTrade.host;
+    const cur = st.tab.trs || "pos";
+    const acct = `<div class="card"><div class="trs-acct"><div class="sf-i" style="width:340px"><label>账户</label><div class="ipt"><span class="val">${H.account} · ${H.user}</span>${ic("down")}</div></div><span class="sp"></span>${btn(ic("refresh") + "刷新")}${btn("买入", "pri", "ovopen", 'data-o="trs" data-dir="买入"')}${btn("卖出", "", "ovopen", 'data-o="trs" data-dir="卖出"')}</div>
+      <div class="card-bd" style="padding-top:0">${kv(H.accountCols.map((c, i) => [c, H.accountRows[0][i], "num"]), "bd3", 3)}</div></div>`;
+    const tabs = [["pos", `持仓<span class="cnt">${H.posRows.length}</span>`], ["ord", `当日委托<span class="cnt">${H.orderRows.length}</span>`]];
+    const tbl = cur === "pos"
+      ? tableHTML(H.posCols, H.posRows, { cell: (r, i) => (i === 2 ? [r[i], "trade_channel"] : i === 10 || i === 11 ? [r[i], r[i].startsWith("+") ? "up" : "dn"] : r[i]), keep: "trs-pos" })
+      : tableHTML([["操作", 72]].concat(H.orderCols), H.orderRows, { cell: (r, i) => (i === 0 ? (["已报", "部成"].includes(r[10]) ? `<span class="btn txt dng" data-act="ovopen" data-o="confirm" data-kind="cancel">撤单</span>` : "--") : i === 4 ? [r[i - 1], "trade_channel"] : i === 6 ? [r[i - 1], "direction"] : i === 11 ? [r[i - 1], "trs_order_status"] : r[i - 1]), keep: "trs-ord" });
+    return `<div class="pg-list">${acct}<div class="card tb-card"><div class="ctabs">${ptabs(st, "trs", tabs, "tabs")}<span class="sp"></span>${btn(ic("refresh") + "刷新", "txt")}</div>${tbl}</div></div>`;
+  };
+
+  U.overlayRenderers.trs = function (ov) {
+    const G = D.trsTrade.dialog;
+    const H = D.trsTrade.host;
+    const buy = ov.dir === "买入";
+    const seg = (k, opts) => `<div class="segc">${opts.map((o) => `<span class="${o === ov[k] ? "on" : ""}" data-act="ovset" data-k="${k}" data-v="${o}">${o}</span>`).join("")}</div>`;
+    const chips = `<div class="qchips">${G.quick.map(([l, v]) => `<span class="qchip ${v === ov.price ? "on" : ""}" data-act="ovset" data-k="price" data-v="${v}">${l}<b class="num">${v}</b></span>`).join("")}</div>`;
+    const QTY = buy ? { "1/4": "25,200", "1/3": "33,600", "1/2": "50,400", "全部可买": "100,900" } : { "1/4": "3,000", "1/3": "4,000", "1/2": "6,000", "全部可卖": "12,000" };
+    const qtyChips = `<div class="qchips">${Object.entries(QTY).map(([l, v]) => `<span class="qchip ${v === ov.qty ? "on" : ""}" data-act="ovset" data-k="qty" data-v="${v}">${l}</span>`).join("")}</div>`;
+    const amount = parseFloat(ov.price) * parseFloat(ov.qty.replace(/,/g, ""));
+    const fmt = (n) => n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+    const tiles = buy ? G.strip : [G.strip[0], ["可卖股数", H.posRows[0][4], "股"]];
+    const rows = [
+      { label: "交易渠道", req: true, body: seg("channel", G.channels) },
+      { label: "股票代码", req: true, body: `<div class="ipt"><span class="val num" style="flex:none">${G.stock.code}</span><span class="val" style="flex:none">${G.stock.name}</span><span class="chipv">${G.stock.mkt}</span><span class="sp"></span>${ic("down")}</div>` },
+      { label: "委托价格", req: true, body: stepper(ov.price, G.priceCcy), extra: chips },
+      { label: "委托数量", req: true, body: stepper(ov.qty, "股"), extra: qtyChips + `<div class="fld-tip">${buy ? G.qtyHelp : "卖出不超过可卖股数"}</div>` },
+    ];
+    const results = [[`${ov.dir}金额(交易币种)`, fmt(amount), "CNY"], [`${ov.dir}金额(结算币种)`, fmt(amount * 1.0944), "HKD"]];
+    const Q = G.quote;
+    const prev = parseFloat(Q.prev);
+    const maxVol = Math.max(...[...Q.asks, ...Q.bids].map((r) => r[2]));
+    const lvl = (rowsQ, cls) => rowsQ.map(([l, pr, v]) => `<div class="ql ${cls} ${pr === ov.price ? "on" : ""}" data-act="ovset" data-k="price" data-v="${pr}"><span>${l}</span><b class="num ${parseFloat(pr) > prev ? "buy" : parseFloat(pr) < prev ? "sell" : ""}">${pr}</b><span class="vol"><em style="width:${Math.round((v / maxVol) * 100)}%"></em><i class="num">${v}</i></span></div>`).join("");
+    const aside = `<aside class="qpanel"><div class="qname"><b>${G.stock.name}</b><span class="code num">${G.stock.code}.${G.stock.mkt}</span><span class="live">${Q.source}</span></div>
+      <div class="qprice"><b class="num last buy">${Q.last}</b><span class="num chg buy">↑${Q.chg} ${Q.pct}</span></div>
+      <div class="qsub">昨收 <b class="num">${Q.prev}</b> · ${Q.hint}</div>
+      ${lvl(Q.asks, "ask")}<div class="qmid"><b class="num buy">${Q.last} ↑</b><span class="num buy">${Q.chg} ${Q.pct}</span></div>${lvl(Q.bids, "bid")}</aside>`;
+    const okBtn = `<span class="btn ${buy ? "dng-pri" : "ok-pri"}" data-act="ovtoast" data-msg="示例:不真正提交">确认${ov.dir} ${ov.qty} 股</span>`;
+    return sfDialog({ w: 880, lw: 72, cls: "dg-trs", keep: "trs-dg", title: ov.dir, sub: `${G.account} · ${G.user}`, tiles, rows, results, aside,
+      ft: `<span class="hint">${ic("info")}${G.footHint}</span><span class="sp"></span>${btnCancel}${okBtn}` });
+  };
+
+  /* ================= 票据详情抽屉(页签 + 描述表) ================= */
+  U.overlayRenderers.note = function (ov) {
+    const N = P.noteDetail;
+    const tags = `<span class="tags">${N.tags.map(([v, dk]) => tag(dk, v)).join("")}</span>`;
+    const rf = `<span class="btn txt" data-act="ovrf" style="color:var(--t2)">${ic("refresh", "rf")}刷新数据</span>`;
+    const x = `<span class="icb" data-act="ovclose" title="关闭">${ic("x")}</span>`;
+    const hb = N.buttons.map(([t, ty]) => btn(t, ty, "ovtoast", 'data-msg="示例:各操作弹窗按弹窗 J 的卡片分区实现"')).join("");
+    const tabs = `<div class="dw-tabs">${N.tabs.map(([k, l]) => `<span class="tab ${ov.tab === k ? "on" : ""}" data-act="ovset" data-k="tab" data-v="${k}">${l}</span>`).join("")}</div>`;
+    const subHd = (t, extra = "") => `<div class="sec-hd sub"><b>${t}</b><span class="sp"></span>${extra}</div>`;
+    let body;
+    if (ov.tab === "main") {
+      const sub = N.subA;
+      const H = N.hedge;
+      const posCell = (r, i) => (i === 0 ? (r[0] === "合计" ? `<b>合计</b>` : [r[0], "hedge_system"]) : r[i]);
+      const trCell = (r, i) => (i === 0 ? [r[i], "hedge_system"] : i === 6 ? `<span class="tg tg-${r[i][1]}">${r[i][0]}</span>` : i === 7 ? [r[i], "direction"] : r[i]);
+      const dlyCols = [["操作", 100]].concat(H.tradeCols);
+      const dlyCell = (r, i) => {
+        if (i === 0) return r[0].length ? `<span class="btns" style="gap:12px">${r[0].map(([t, ty]) => `<span class="btn txt ${ty === "danger" ? "dng" : ""}" data-act="ovopen" data-o="confirm" data-kind="${t === "删除" ? "remove" : t === "撤单" ? "cancel" : "cancel"}">${t}</span>`).join("")}</span>` : "--";
+        return trCell(r.slice(1), i - 1);
+      };
+      const dailyBtns = H.dailyButtons.map(([t, ty]) => btn(t === "刷新" ? ic("refresh") + t : t, ty === "txt" ? "txt" : "sm", t === "补录" ? "ovopen" : "ovtoast", t === "补录" ? 'data-o="fill"' : 'data-msg="示例:日常买卖的下单弹窗按「TRS 下单弹窗」的专用布局实现"')).join("");
+      body = drCard(N.master.title, "", kv(N.master.fields, "bd3", 3)) +
+        drCard(sub.title, "", kv(sub.fields, "bd3", 3) + subHd(sub.init.title) + kv(sub.init.fields, "bd2", 2) + subHd(sub.current.title) + kv(sub.current.fields, "bd2", 2)) +
+        drCard(H.title, `<span class="chip">${H.ratioLabel}<b>${H.ratio}</b></span>`,
+          subHd(H.posTitle) + tableHTML(H.posCols, H.posRows, { cell: posCell, keep: "nt-pos" }) +
+          subHd(H.tradeTitle) + tableHTML(H.tradeCols, H.tradeRows, { cell: trCell, keep: "nt-tr" }) +
+          subHd(H.dailyTitle, dailyBtns) + tableHTML(dlyCols, H.dailyRows, { cell: dlyCell, keep: "nt-dly" }));
+    } else {
+      body = N.changes.sections.map((s) => drCard(s.title, `<small>${s.rows.length} 条</small>`, tableHTML(s.cols, s.rows, { empty: s.empty, emptyH: 120, keep: "nt-" + s.title }))).join("");
+    }
+    return `<div class="dw dw-e dw-note" style="width:1040px" data-act="stop"><div class="dw-hd"><div class="idc"><div class="idr"><span class="no">${N.noteNo}</span>${tags}</div><div class="facts">${N.facts.map(([l, v]) => `<span>${l}<b>${v}</b></span>`).join("")}</div></div><span class="sp"></span><div class="hbtns col"><div class="r">${rf}${x}</div><div class="r">${hb}</div></div></div>${tabs}<div class="dw-bd gray" data-keep="dwbd-${ov.tab}">${body}</div></div>`;
+  };
+
+  /* ================= 确认框、修改密码弹窗 ================= */
+  const CONFIRMS = Object.assign({}, P.feedback.confirms, {
+    force: { title: "强平卖出", msg: "确定对该票据执行强平卖出吗?卖出后票据状态变为「已卖出」,不能恢复。", kind: "danger", ok: "确定强平", kv: [["票据编号", "NT001202608141A"], ["投资者", "陈思雨 · 安衡证券"], ["证券", "601318 中国平安 · 28,700 股"]] },
+    redo: { title: "重新清算", msg: "将回拨到该批次的交易日并重新执行全部步骤,期间不能下单。确定重新清算吗?", kind: "warning", ok: "重新清算", kv: [["交易日", "2026-09-25"], ["批次", "CLR20260925001"]] },
+    stop: { title: "停止当前任务", msg: "停止后本批次标记为「已停止」,已完成的步骤不会回滚。确定停止吗?", kind: "danger", ok: "停止", kv: [["批次", "CLR20260925001"], ["当前步骤", "4 · 交易交收处理"]] },
+  });
+  U.overlayRenderers.confirm = function (ov) {
+    const c = CONFIRMS[ov.kind] || CONFIRMS.cancel;
+    const icn = c.kind === "danger" ? "error" : c.kind === "warning" ? "warn" : "info";
+    const okCls = c.kind === "danger" ? "dng-pri" : "pri";
+    return `<div class="cfm" data-act="stop"><div class="cfm-bd"><div class="cfm-hd"><span class="cfm-ic ${c.kind}">${ic(icn)}</span><b>${esc(c.title)}</b></div><p>${esc(c.msg)}</p>${c.kv.length ? `<dl class="cfm-kv">${c.kv.map(([l, v]) => `<div><dt>${l}</dt><dd class="num">${esc(v)}</dd></div>`).join("")}</dl>` : ""}</div><div class="cfm-ft"><span class="btn" data-act="ovclose">${c.cancel || "取消"}</span><span class="btn ${okCls}" data-act="ovclose">${c.ok}</span></div></div>`;
+  };
+  U.overlayRenderers.pwd = function () {
+    const rows = P.profile.pwdForm.map((f, i) => ({ label: f.label, req: f.req, body: U.ctl({ ...f, ph: i === 1 ? "请输入新密码" : f.ph }, {}), extra: i === 1 ? `<div class="fld-tip">6~20 位,至少两类字符</div>` : "" }));
+    return sfDialog({ w: 480, lw: 72, title: "修改密码", sub: `${P.profile.username} · ${P.profile.role}`, rows, ft: `<span class="hint">${ic("info")}修改后需要重新登录</span><span class="sp"></span>${btnCancel}<span class="btn pri" data-act="ovtoast" data-msg="示例:不真正提交">确定</span>` });
+  };
+
+  /* ================= 个人中心 ================= */
+  U.pageRenderers["个人中心"] = function () {
+    const p = P.profile;
+    const left = `<div class="card prof-id"><div class="prof-ava" data-act="toast" data-msg="上传头像(代码里是隐藏的文件选择)"><span class="ava big">A</span><span class="prof-cam">${ic("camera")}</span></div><div class="prof-nm"><b>${p.nickname}</b><span class="icb sm" data-act="toast" data-msg="修改昵称、性别的弹窗按弹窗 J 实现" title="修改资料">${ic("edit")}</span></div><div class="prof-role">${p.role}</div></div>`;
+    const bind = (v, what) => (v ? `<span class="num">${v}</span>` : `<span class="t4">未绑定</span><span class="btn txt" data-act="toast" data-msg="绑定${what}的弹窗按弹窗 J 实现" style="margin-left:12px">绑定</span>`);
+    const info = `<div class="card"><div class="card-hd">账号信息</div><div class="card-bd"><dl class="kvg bd1"><div><dt>用户名</dt><dd>${p.username}${ic("male", "gender")}</dd></div><div><dt>手机号码</dt><dd>${bind(p.mobile, "手机")}</dd></div><div><dt>邮箱</dt><dd>${bind(p.email, "邮箱")}</dd></div><div><dt>部门</dt><dd>${p.dept}<span class="t3" style="font-size:12px;margin-left:8px">示例</span></dd></div><div><dt>创建时间</dt><dd class="num">${p.createTime}</dd></div></dl></div></div>`;
+    const sec = `<div class="card"><div class="card-hd">安全设置</div><div class="prof-sec">${p.security.map(([t, d, a], i) => `<div class="prof-row"><div><b>${t}</b><small>${d}</small></div><span class="btn txt" data-act="${i === 0 ? "ovopen" : "toast"}" data-o="pwd" data-msg="绑定弹窗按弹窗 J 实现">${a}</span></div>`).join("")}</div></div>`;
+    return `<div class="prof">${left}<div class="prof-r">${info}${sec}</div></div>`;
+  };
+
+  /* ================= 404 / 401 ================= */
+  function errHTML(code, st) {
+    const is404 = code === "404";
+    const art = `<svg class="err-art" viewBox="0 0 420 160"><text x="210" y="126" text-anchor="middle" font-size="128" font-weight="600" style="font-family:var(--font-num);fill:var(--c-primary-bg);stroke:var(--c-primary);stroke-width:2">${code}</text></svg>`;
+    const home = ENDS[st.end].home;
+    return `<div class="errp">${art}<div class="err-t"><b>${is404 ? "页面不存在" : "没有访问权限"}</b><p>${is404 ? "请确认输入的网址是否正确,或从左侧菜单重新进入。" : "当前账号没有「共赢票据清算」的访问权限,请联系系统管理员开通后再试。"}</p><div class="btns" style="justify-content:center">${btn("返回首页", "pri", "leaf", 'data-k="首页"')}${btn("返回上一页", "", "leaf", `data-k="${home}"`)}</div></div></div>`;
+  }
+  U.pageRenderers["404"] = (st) => errHTML("404", st);
+  U.pageRenderers["401"] = (st) => errHTML("401", st);
+
+  /* ================= 登录页:分项选型登录页 C「右侧浮卡·全幅底图」(2026-10-07 选定) =================
+   * 整页是视觉 F 的渐变底色叠一层淡线条装饰;左侧一列:系统名、本系统业务模块、三端入口;右侧 440px 浮卡放表单。
+   * 画布规格 sp 上记临时状态:cap(验证码种子)、showPw、remember。
+   */
+  const CAPCH = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+  // 验证码图片示意:按种子生成 4 个字符和两条干扰线,点一下换一张
+  function capSVG(seed, w = 120, h = 40) {
+    let s = (seed * 7919 + 104729) % 233280;
+    const rnd = () => (s = (s * 9301 + 49297) % 233280) / 233280;
+    const chars = Array.from({ length: 4 }, () => CAPCH[Math.floor(rnd() * CAPCH.length)]);
+    const fs = Math.round(h * 0.58);
+    const txt = chars.map((ch, i) => { const x = Math.round(w * (0.13 + i * 0.22)), y = Math.round(h * 0.7), r = Math.round((rnd() - 0.5) * 28); return `<text x="${x}" y="${y}" font-size="${fs}" font-weight="600" transform="rotate(${r} ${x} ${y})" style="fill:var(${i % 2 ? "--c-primary" : "--c-link"})">${ch}</text>`; }).join("");
+    const wave = (c) => `<path d="M0 ${(h * (0.3 + rnd() * 0.4)).toFixed(1)} C ${(w * 0.25).toFixed(0)} ${(h * rnd()).toFixed(1)}, ${(w * 0.5).toFixed(0)} ${(h * rnd()).toFixed(1)}, ${(w * 0.75).toFixed(0)} ${(h * rnd()).toFixed(1)} S ${w} ${(h * rnd()).toFixed(1)}, ${w} ${(h * (0.3 + rnd() * 0.4)).toFixed(1)}" style="stroke:var(${c});fill:none;stroke-width:1.2"/>`;
+    return `<svg viewBox="0 0 ${w} ${h}" aria-label="验证码 ${chars.join("")}"><rect width="${w}" height="${h}" style="fill:var(--fill-2)"/>${wave("--c-primary-border")}${wave("--bd-ctl")}${txt}</svg>`;
+  }
+  // 底图:淡网格、几条主色曲线、右上角两个圆(内联 SVG,不用图片文件)
+  const LG_DECO = `<svg class="lg-deco" viewBox="0 0 1440 900" preserveAspectRatio="none"><defs><pattern id="lgGrid" width="48" height="48" patternUnits="userSpaceOnUse"><path d="M48 0H0v48" fill="none" stroke="currentColor" stroke-width="1"/></pattern></defs><rect width="1440" height="900" fill="url(#lgGrid)" opacity=".05"/>
+    <path d="M-40 640C260 560 420 760 760 660S1200 540 1480 620" opacity=".45"/><path d="M-40 700C300 640 480 820 820 700S1260 600 1480 700" opacity=".3"/><path d="M-40 560C200 500 380 640 700 580S1160 440 1480 520" opacity=".2"/><circle cx="1180" cy="150" r="220" opacity=".22"/><circle cx="1180" cy="150" r="300" opacity=".12"/></svg>`;
+  U.canvasRenderers.login = function (sp) {
+    const end = sp.end || "issue";
+    const e = ENDS[end];
+    const L = P.login;
+    const c = L.copy;
+    const v = sp.v || "login";
+    const groups = D.menus[end].map((g) => g[0]).filter((n) => !L.opsGroups.includes(n));
+    const mods = `<div class="lg-mods"><div class="lg-mods-t">本系统业务模块<span>${groups.length}</span></div><div class="lg-chips">${groups.map((n) => `<span class="lg-chip">${esc(n)}</span>`).join("")}</div></div>`;
+    // 三端入口:当前系统标「当前」,另外两个点一下跳到对应系统的登录页(本稿里是切到那个系统)
+    const ents = `<div class="lg-ents"><div class="lg-ents-t">三端系统<span>点击前往对应系统的登录页</span></div>${Object.entries(ENDS).map(([x, ex]) => `<div class="lg-ent ${x === end ? "on" : ""}" data-act="sys" data-end="${x}"${x === end ? "" : ` title="跳到${esc(ex.sys)}的登录地址"`}><span class="lg-logo sm">${ex.logo}</span><span class="lg-ent-n"><b>${esc(ex.sys)}</b><small>${L.nodes[x][1]}</small></span><span class="lg-ent-go">${x === end ? "当前系统" : `前往登录${ic("right")}`}</span></div>`).join("")}</div>`;
+    const left = `<div class="lg-c-left"><div><b class="lg-c-sys">${esc(e.sys)}</b><div class="lg-c-ver">${L.version}</div></div>${mods}${ents}</div>`;
+    const field = (label, inner) => `<div class="lg-fi"><label>${label}</label>${inner}</div>`;
+    const pw = sp.showPw ? "123456" : "••••••";
+    let form;
+    if (v === "login") {
+      form = `<div class="lg-ttl">登录</div>
+        ${field(c.username, `<div class="ipt lg-ipt"><span class="val">admin</span></div>`)}
+        ${field(c.password, `<div class="ipt lg-ipt"><span class="val num">${pw}</span><span class="lg-eye" data-act="eye" title="${sp.showPw ? "隐藏密码" : "显示密码"}">${ic(sp.showPw ? "eye" : "eyeoff")}</span></div>`)}
+        ${field(c.captcha, `<div class="lg-cap-row"><div class="ipt lg-ipt" style="flex:1"><span class="ph">${c.captcha}</span></div><div class="lg-cap" title="点击换一张" data-act="cap">${capSVG((sp.cap || 0) + 3)}</div></div>`)}
+        <div class="lg-row"><span class="chk-l" data-act="rem"><span class="ck ${sp.remember === false ? "" : "on"}">${CHECK}</span>${c.remember}</span><span class="lnk" data-act="lv" data-v="reset">${c.forget}</span></div>
+        <span class="btn pri lg-btn" data-act="toast" data-msg="示例:不真正登录">${c.login}</span>`;
+    } else {
+      form = `<div class="lg-ttl">${c.reset}</div><p class="lg-p">输入账号绑定的邮箱,重置链接会发到这个邮箱。</p>
+        ${field(c.email, `<div class="ipt lg-ipt"><span class="ph">${c.enterEmail}</span></div>`)}
+        <span class="btn pri lg-btn" data-act="toast" data-msg="示例:验证码已发送到您的邮箱">${c.sendReset}</span>
+        <div class="lg-row center"><span class="t3">${c.think}</span><span class="lnk" data-act="lv" data-v="login">${c.back}</span></div>`;
+    }
+    return `<div class="lg">${LG_DECO}<div class="lg-actions"><span class="icb" data-act="theme" title="${c.theme}:在浅色和最近看过的深色皮肤之间切换">${ic(T.dark ? "sun" : "moon")}</span><span class="icb" data-act="toast" data-msg="示例:${c.lang}" title="${c.lang}">${ic("globe")}</span></div>
+      ${left}<div class="lg-card"><div class="lg-brand sm"><span class="lg-logo sm">${e.logo}</span><b>${esc(e.sys)}</b></div>${form}</div></div>`;
+  };
+  U.extraActions.cap = (host, sp) => { sp.cap = (sp.cap || 0) + 1; return true; };
+  U.extraActions.eye = (host, sp) => { sp.showPw = !sp.showPw; return true; };
+  U.extraActions.rem = (host, sp) => { sp.remember = sp.remember === false; return true; };
+  U.extraActions.lv = (host, sp, el) => { sp.v = el.dataset.v; return true; };
+
+  /* ================= 组件总览 ================= */
+  U.canvasRenderers.kit = function () {
+    const Tk = T.tokens;
+    const dict = U.dict;
+    const sw = (label, key) => { const val = Tk[key]; const none = val === "transparent"; return `<div class="swc"><i style="background:${none ? "repeating-linear-gradient(45deg,var(--bg-card) 0 4px,var(--fill-3) 4px 8px)" : val}"></i><b>${label}</b><span>${none ? "无" : esc(val)}</span></div>`; };
+    const group = (items) => `<div class="sw-group">${items.map(([l, key]) => sw(l, key)).join("")}</div>`;
+    const colors = `<div class="card"><div class="card-hd">颜色<span class="sub">主色 ${Tk["c-primary"]} 只做填充,文字链接 ${Tk["c-link"]};买入红、卖出绿语义不变</span></div><div class="card-bd">${T.palette.map(([, items]) => group(items)).join("")}</div></div>`;
+    const ty = (spec, html) => `<div class="ty-row"><span class="spec">${spec}</span><div>${html}</div></div>`;
+    const type = `<div class="card s6"><div class="card-hd">文字<span class="sub">中文系统字体,英文和数字 Inter</span></div><div class="card-bd" style="padding-top:8px;padding-bottom:8px">
+      ${ty("20 / 28 · 编号标题", `<span class="num" style="font-size:20px;line-height:28px;font-weight:var(--fw-title);color:var(--t1)">ORD001202609250031</span>`)}
+      ${ty("16 / 24 · 弹窗标题", `<span style="font-size:16px;line-height:24px;font-weight:var(--fw-title);color:var(--t1)">撤销订单</span>`)}
+      ${ty("15 / 22 · 区块标题", `<span style="font-size:15px;font-weight:var(--fw-title);color:var(--t1)">订单信息</span>`)}
+      ${ty("14 / 22 · 正文", `<span style="color:var(--t1)">订单已进入对冲流程,预计交收日 <span class="num">2026-09-29</span>。</span>`)}
+      ${ty("14 / 22 · 次要", `<span style="color:var(--t2)">投资者名称、机构名称等次要信息</span>`)}
+      ${ty("12 / 18 · 辅助", `<span style="font-size:12px;line-height:18px;color:var(--t3)">表头、键名、输入框下方的提示</span>`)}
+      ${ty("数字", `<span class="num" style="color:var(--t1);font-size:16px">3,000,000.00 1,688.00 0.0003 2026-09-25 09:31:07</span>`)}
+    </div></div>`;
+    const buttons = `<div class="card s6"><div class="card-hd">按钮<span class="sub">${T.notes.buttons}</span></div><div class="card-bd">
+      <div class="kit-row"><span class="kit-lbl">常规</span><span class="btn pri">${ic("search")}查找</span><span class="btn">${ic("refresh")}重置</span><span class="btn">${ic("download")}数据导出</span><span class="btn dng">撤单</span><span class="btn is-dis">禁用</span></div>
+      <div class="kit-row"><span class="kit-lbl">悬停</span><span class="btn pri is-hover">${ic("plus")}新增</span><span class="btn is-hover">${ic("refresh")}重置</span><span class="btn icon">${ic("refresh")}</span><span class="btn is-loading"><i class="spin"></i>查找中</span></div>
+      <div class="kit-row"><span class="kit-lbl">表格内</span><span class="btn txt">详情</span><span class="btn txt">编辑</span><span class="btn txt dng">撤单</span><span class="btn sm pri">确认</span><span class="btn sm">补录</span></div>
+      <div class="kit-row"><span class="kit-lbl">方向</span><span class="btn dng-pri">确认买入 1,000 股</span><span class="btn ok-pri">确认卖出 500 股</span><span class="btn">取消</span></div>
+    </div></div>`;
+    const f = (label, html, extra = "") => `<div><div class="f-l">${label}</div>${html}${extra}</div>`;
+    const forms = `<div class="card"><div class="card-hd">输入控件<span class="sub">默认 32px;登录页 40px</span></div><div class="card-bd"><div class="kit-fields">
+      ${f("默认", `<div class="ipt"><span class="ph">主订单编号</span></div>`)}
+      ${f("已填写", `<div class="ipt"><span class="val num">ORD001202609250031</span></div>`)}
+      ${f("聚焦", `<div class="ipt is-focus"><span class="val">贵州茅台</span><span style="width:1px;height:16px;background:var(--t1);margin-left:-5px"></span></div>`)}
+      ${f("下拉选择", `<div class="ipt is-focus"><span class="val">待处理</span>${ic("down")}</div>`, `<div class="pop" style="margin-top:4px"><div class="pop-i">全部</div><div class="pop-i on">待处理<span class="sp" style="flex:1"></span>${ic("check")}</div><div class="pop-i is-hover">执行中</div><div class="pop-i">已完成</div></div>`)}
+      ${f("日期区间", `<div class="ipt">${ic("calendar")}<span class="val num">2026-09-25</span><span class="sep">~</span><span class="val num">2026-09-25</span></div>`, `<div class="f-l" style="margin-top:16px">带单位</div><div class="ipt"><span class="val num">1,000</span><span class="sfx">股</span></div><div class="f-l" style="margin-top:16px">多选</div><div class="ipt"><span class="val chips"><span class="chipv">待生效</span><span class="chipv">存续期</span></span>${ic("down")}</div>`)}
+      ${f("校验失败", `<div class="ipt is-err"><span class="ph">请输入委托数量</span></div>`, `<div class="fld-err">${ic("error")}委托数量必须是 100 的整数倍</div><div class="f-l" style="margin-top:16px">禁用</div><div class="ipt is-dis"><span class="val">CNY</span></div><div class="f-l" style="margin-top:16px">两段切换</div><div class="segc"><span class="on">自定义</span><span>TRS</span></div>`)}
+      ${f("勾选与开关", `<div class="kit-row" style="gap:20px"><span class="chk-l"><span class="ck on">${CHECK}</span>已勾选</span><span class="chk-l"><span class="ck">${CHECK}</span>未勾选</span><span class="chk-l"><span class="ck half"></span>部分</span></div><div class="kit-row" style="gap:20px"><span class="chk-l"><span class="rd on"></span>限价</span><span class="chk-l"><span class="rd"></span>市价</span><span class="chk-l"><span class="sw on"></span>开启</span><span class="chk-l"><span class="sw"></span>关闭</span></div>`)}
+    </div></div></div>`;
+    const tags = (dk, list) => (list || Object.keys(dict[dk])).map((t) => tag(dk, t)).join("");
+    const status = `<div class="card"><div class="card-hd">状态与字典<span class="sub">${T.notes.status}</span></div><div class="card-bd">
+      <div class="kit-row"><span class="kit-lbl">订单状态</span>${tags("note_order_status")}</div>
+      <div class="kit-row"><span class="kit-lbl">委托状态</span>${tags("trs_order_status", ["已报", "部成", "已成", "已撤", "废单", "过期"])}</div>
+      <div class="kit-row"><span class="kit-lbl">票据</span>${tags("note_status")}${tags("note_risk_status")}</div>
+      <div class="kit-row"><span class="kit-lbl">方向</span>${tags("direction")}<span class="t3" style="margin-left:12px">文字形式:</span><span class="buy" style="font-weight:var(--fw-strong)">买入</span><span class="sell" style="font-weight:var(--fw-strong)">卖出</span><span class="t3" style="margin-left:12px">盈亏:</span><span class="num buy">+58,002.47</span><span class="num sell">-31,886.40</span></div>
+      <div class="kit-row"><span class="kit-lbl">操作类型</span>${tags("op_type")}${tags("process_mode")}${tags("hedge_progress")}</div>
+    </div></div>`;
+    const kvs = `<div class="card s6"><div class="card-hd">描述表<span class="sub">${T.notes.kv}</span></div><div class="card-bd">
+      <dl class="kvg bd2 n"><div><dt>主订单编号</dt><dd class="num">ORD001202609250031</dd></div><div><dt>订单状态</dt><dd>${tag("note_order_status", "待处理")}</dd></div><div><dt>订单方向</dt><dd>${tag("direction", "买入")}</dd></div><div><dt>投资者</dt><dd>张明远 · INV001100151</dd></div><div><dt>证券</dt><dd>600519 贵州茅台</dd></div><div><dt>名义本金</dt><dd class="num">3,000,000.00 CNY</dd></div><div><dt>下单时间</dt><dd class="num">2026-09-25 09:31:07</dd></div><div><dt>备注</dt><dd class="t4">--</dd></div></dl>
+      <div class="f-l" style="margin:16px 0 8px">数字卡片(汇总)</div><div class="tiles" style="grid-template-columns:repeat(3,1fr)"><div class="tile"><div class="l">对冲比例</div><div class="v">77.78%</div><div class="bar"><i style="width:77.78%"></i></div></div><div class="tile"><div class="l">累计成交数量</div><div class="v">6,300</div></div><div class="tile"><div class="l">对冲含费成交均价</div><div class="v">245.2112<i>CNY</i></div></div></div>
+      </div></div>`;
+    const rows = D.issueRows.filter((r) => r.sub === 0).slice(0, 5);
+    const table = `<div class="card"><div class="ctabs"><div class="tabs"><span class="tab on">全部<span class="cnt">12</span></span><span class="tab">待处理<span class="cnt">1</span></span><span class="tab">执行中<span class="cnt">3</span></span><span class="tab">已完成<span class="cnt">5</span></span></div><span class="sp"></span><span class="btn txt">${ic("refresh")}刷新</span></div>
+      <div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:48px"><span class="ck half"></span></th><th>主订单编号</th><th>订单方向</th><th>订单状态</th><th>投资者名称</th><th>证券代码/名称</th><th class="r">订单名义本金(交易币种)</th><th>下单时间</th><th>操作</th></tr></thead><tbody>
+      ${rows.map((r, i) => `<tr class="${i === 1 ? "is-sel" : ""} ${i === 3 ? "is-hover" : ""}"><td><span class="ck ${i === 1 ? "on" : ""}">${CHECK}</span></td><td><span class="lnk num">${r.no}</span></td><td>${tag("direction", r.direction)}</td><td>${tag("note_order_status", r.status)}</td><td>${r.invName}</td><td>${r.stock}</td><td class="r num">${r.notional === "--" ? '<span class="t4">--</span>' : r.notional}</td><td class="num">${r.orderTime}</td><td><span class="btns" style="gap:12px"><span class="btn txt">详情</span><span class="btn txt dng">撤单</span></span></td></tr>`).join("")}
+      </tbody></table></div>
+      ${U.pagerHTML(12)}
+      <div style="padding:0 16px 12px;font-size:12px;color:var(--t3)">第 2 行为选中态,第 4 行为悬停态;卡片内页签见「清算管理」。</div></div>`;
+    const feedback = `<div class="card s6"><div class="card-hd">提示与反馈<span class="sub">完整排布见「反馈类总览」</span></div><div class="card-bd">
+      <div style="display:grid;grid-template-columns:1fr;gap:10px">
+        <div class="alt alt-info">${ic("info")}<span>自开始日期(含)起同步交易日,重复同步不会产生重复数据。</span></div>
+        <div class="alt alt-danger">${ic("error")}<span>撤单结果未知:上轮撤单未取得明确结果,可人工再次撤单。</span></div>
+      </div>
+      <div class="kit-row" style="margin-top:16px;gap:16px"><span class="toast">${ic("success", "ok")}<span>保存成功</span></span><span class="toast">${ic("error", "no")}<span>导出失败,请稍后重试</span></span><span class="tip">点击复制编号</span></div>
+    </div></div>`;
+    const dialog = `<div class="card s6"><div class="card-hd">弹窗与弹层<span class="sub">${T.notes.dialog}</span></div><div class="card-bd"><div class="kit-mask">
+      <div class="dlg"><div class="dlg-hd">撤销订单<span class="x">${ic("x")}</span></div><div class="dlg-bd" style="display:flex;flex-direction:column;gap:14px">
+        <div class="alt alt-warning">${ic("warn")}<span>撤销后订单进入「撤单中」,不能恢复。</span></div>
+        <dl class="kv" style="grid-template-columns:1fr;gap:8px"><div><dt>主订单编号</dt><dd class="num">ORD001202609250031</dd></div><div><dt>证券</dt><dd>600519 贵州茅台</dd></div></dl>
+      </div><div class="dlg-ft"><span class="btn">取消</span><span class="btn pri">确认撤销</span></div></div>
+    </div></div></div>`;
+    return `<div class="kit-top"><h1>组件总览 · ${T.title}</h1><span>主色 ${Tk["c-primary"]}|正文 14px · 控件 32px · 表格行高 40px · 圆角 ${Tk["r-ctl"]}/${Tk["r-card"]}|尺寸和圆角四套皮肤相同,换肤只改颜色与质感</span></div>
+      <div class="kit-grid">${colors}${type}${buttons}${forms}${status}${table}${kvs}${feedback}${dialog}</div>`;
+  };
+
+  /* ================= 反馈类总览 ================= */
+  U.canvasRenderers.feedback = function () {
+    const F = P.feedback;
+    const card = (title, sub, body, cls = "") => `<div class="card ${cls}"><div class="card-hd">${title}${sub ? `<span class="sub">${sub}</span>` : ""}</div><div class="card-bd">${body}</div></div>`;
+    const icOf = (t) => ic(t === "success" ? "success" : t === "error" || t === "danger" ? "error" : t === "warning" ? "warn" : "info", t);
+    const msgs = `<div class="fb-msgs">${F.messages.map(([t, m, close]) => `<div class="toast msg-${t}">${icOf(t)}<span>${esc(m)}</span>${close ? `<span class="x">${ic("x")}</span>` : ""}</div>`).join("")}</div><div class="fb-note">顶部居中,距顶 72px;多条时向下堆叠;默认 3 秒消失,带关闭按钮的停留 8 秒。</div>`;
+    const ntf = `<div class="ntf"><div class="ntf-hd">${ic("success", "ok")}<b>${F.notification.title}</b><small>${F.notification.time}</small><span class="x">${ic("x")}</span></div><p>${F.notification.body}</p><span class="lnk">${F.notification.link}${ic("right")}</span></div><div class="fb-note">右上角,距顶 16px、距右 16px;用于后台任务完成这类不打断操作的消息。</div>`;
+    const alerts = `<div class="fb-alts">${F.alerts.map(([t, m]) => `<div class="alt alt-${t}">${icOf(t)}<span>${esc(m)}</span></div>`).join("")}<div class="alt alt-${F.alertTitled[0]} titled">${icOf("warning")}<div><b>${F.alertTitled[1]}</b><p>${F.alertTitled[2]}</p></div><span class="x">${ic("x")}</span></div></div>`;
+    const cfm = U.overlayRenderers.confirm({ kind: "cancel" });
+    const confirm = `<div class="kit-mask" style="min-height:260px">${cfm}</div><div class="fb-note">在页面里的样子见「确认框」;危险操作红色实心按钮并写明动作。</div>`;
+    const empties = `<div class="fb-empties">${F.empties.map(([n, t, d, a]) => `<div class="fb-cell"><div class="fb-lbl">${n}</div><div class="empty box">${EMPTY}<span>${esc(t)}</span>${d ? `<small>${esc(d)}</small>` : ""}${a ? `<span class="btn sm">${esc(a)}</span>` : ""}</div></div>`).join("")}</div>`;
+    const rows = D.issueRows.filter((r) => r.sub === 0).slice(0, 4);
+    const loadTbl = `<div class="fb-load"><div class="tbl-wrap bd" style="position:relative"><table class="tbl"><thead><tr><th>主订单编号</th><th>订单方向</th><th>订单状态</th><th>投资者名称</th><th class="r">订单名义本金</th></tr></thead><tbody>${rows.map((r) => `<tr><td class="num">${r.no}</td><td>${tag("direction", r.direction)}</td><td>${tag("note_order_status", r.status)}</td><td>${r.invName}</td><td class="r num">${r.notional}</td></tr>`).join("")}</tbody></table><div class="loading"><i class="spin big"></i><span>加载中</span></div></div>
+      <div class="fb-load-r"><div class="f-l">按钮</div><div class="btns"><span class="btn pri is-loading"><i class="spin"></i>查找中</span><span class="btn is-loading"><i class="spin"></i>导出中</span></div><div class="f-l" style="margin-top:16px">首屏骨架</div><div class="skel" style="height:auto;gap:8px"><div class="bar" style="width:40%"></div><div class="bar" style="width:92%"></div><div class="bar" style="width:84%"></div><div class="bar" style="width:70%"></div></div></div></div>`;
+    const results = `<div class="fb-results">${F.results.map(([t, ttl, d, bs]) => `<div class="result"><span class="res-ic ${t}">${icOf(t)}</span><b>${ttl}</b><p>${d}</p><div class="btns">${bs.map(([b, ty]) => `<span class="btn ${ty === "primary" ? "pri" : ""}">${b}</span>`).join("")}</div></div>`).join("")}</div>`;
+    const validate = `<div class="fgrid c2" style="max-width:720px"><div class="fi fi-t"><label><i class="req">*</i>E-mail</label><div class="fc"><div class="ipt is-err"><span class="val">wanqing.li@</span></div><div class="fld-err">${ic("error")}邮箱格式不正确</div></div></div><div class="fi fi-t"><label><i class="req">*</i>发生金额(结算币种)</label><div class="fc"><div class="ipt is-err"><span class="ph">请输入</span><span class="sfx">HKD</span></div><div class="fld-err">${ic("error")}请输入发生金额(HKD)</div></div></div></div>
+      <div class="fb-note" style="margin-top:12px">错误在失焦时校验、提交时全部校验;长表单的弹窗底部汇总「还有几项必填未填、几项有误」,第一处错误滚动到可见。</div>`;
+    return `<div class="kit-top"><h1>反馈类总览</h1><span>全局提示、通知、提示条、确认框、空状态、加载态、结果提示、表单校验</span></div>
+      <div class="kit-grid">${card("全局提示", "ElMessage", msgs, "s6")}${card("通知", "ElNotification", ntf, "s6")}${card("提示条", "el-alert:按状态取色;可带标题、可关闭", alerts, "s6")}${card("确认框", "ElMessageBox.confirm", confirm, "s6")}${card("空状态", "el-empty:说明原因,必要时给一个动作", empties)}${card("加载态", "v-loading、按钮 loading、骨架", loadTbl, "s6")}${card("结果提示", "el-result:整块居中", results, "s6")}${card("表单校验", "输入框变红 + 下方原因", validate)}</div>`;
+  };
+})();

+ 42 - 0
src/shot.mjs

@@ -0,0 +1,42 @@
+// 开发用截图:node shot.mjs [名称前缀...]。每个页面用单独模式按原尺寸截图,放 ../shots/(可用 SHOT_DIR 改)。
+import { chromium } from "playwright";
+import { fileURLToPath, pathToFileURL } from "url";
+import path from "path";
+import fs from "fs";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const url = pathToFileURL(path.join(root, "CMS-UI完整设计.html")).href;
+const want = process.argv.slice(2);
+const theme = process.env.SHOT_THEME || ""; // f / da / db / dc;不填就是浅色
+const tsuf = theme && theme !== "f" ? `/t=${theme}` : "";
+const tpre = theme ? `${theme}-` : "";
+const outDir = process.env.SHOT_DIR || path.join(root, "shots");
+fs.mkdirSync(outDir, { recursive: true });
+const jobs = [
+  ["kit", "#solo/kit", true], ["feedback", "#solo/feedback", true],
+  ["login-issue", "#solo/login/issue"], ["login-sales", "#solo/login/sales"], ["login-trs", "#solo/login/trs"], ["login-reset", "#solo/login/issue/reset"],
+  ["home-issue", "#solo/home/issue"], ["home-sales", "#solo/home/sales"], ["home-trs", "#solo/home/trs"],
+  ["list-issue", "#solo/list/issue"], ["list-sales", "#solo/list/sales"], ["list-trs", "#solo/list/trs"],
+  ["drawer", "#solo/drawer"], ["dlg-fill", "#solo/dlg-fill"], ["dlg-investor", "#solo/dlg-investor"], ["dlg-trs", "#solo/dlg-trs"], ["dlg-trs-sell", "#solo/dlg-trs/trs/sell"], ["drawer-note", "#solo/drawer-note"],
+  ["tabs", "#solo/tabs"], ["clearing", "#solo/clearing"], ["profile", "#solo/profile"], ["profile-pwd", "#solo/profile/issue/pwd"],
+  ["err-404", "#solo/err/issue/404"], ["err-401", "#solo/err/issue/401"],
+  ["confirm-cancel", "#solo/confirm"], ["confirm-ratio", "#solo/confirm/issue/ratio"],
+  ["book", "#p/spec", "book"], ["book-home", "#p/home", "book"],
+];
+const browser = await chromium.launch();
+const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: +(process.env.SHOT_DPR || 1) });
+let bad = 0;
+for (const [name, hash, mode] of jobs) {
+  if (want.length && !want.some((w) => name.startsWith(w))) continue;
+  const page = await ctx.newPage();
+  const errs = [];
+  page.on("pageerror", (e) => errs.push(e.message));
+  page.on("console", (m) => m.type() === "error" && errs.push(m.text()));
+  await page.goto(url + hash + tsuf);
+  await page.waitForTimeout(250);
+  await page.screenshot({ path: path.join(outDir, tpre + name + ".png"), fullPage: mode === true || mode === "book" });
+  if (errs.length) { bad++; console.log("✗", name, errs); } else console.log("✓", name);
+  await page.close();
+}
+await browser.close();
+process.exit(bad ? 1 : 0);

+ 56 - 0
src/template.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+  三端 CMS · 完整 UI 设计稿(基于 2026-10-08 第一批分项选型结果:基础视觉 F 晴空渐变、框架 A 经典三段、弹窗 J 卡片分区、抽屉 E 卡片+描述表;登录页按 10-07 追加的分项选型登录页 C 右侧浮卡·全幅底图)
+  换肤:浅色 F + 深色 B 石墨灰(10-08 从三版候选里选定;顶栏「皮肤」切换;地址 #p/<页面>/<端>/<变体>/t=db)
+  本文件由 src/build.py 生成,单文件自包含,离线双击即可打开。请改 src/ 下的源文件后重新生成。
+-->
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>三端 CMS · 完整 UI 设计稿</title>
+<style>
+/*{{fonts.css}}*/
+/*{{book.css}}*/
+/*{{kit.css}}*/
+/*{{frame.css}}*/
+/*{{overlay.css}}*/
+/*{{pages.css}}*/
+/*{{theme.css}}*/
+</style>
+</head>
+<body>
+<header class="bk-hd"><b>三端 CMS · 完整 UI 设计稿</b><div class="bk-picks"><span>基础视觉 F 晴空渐变</span><span>框架 A 经典三段</span><span>长表单 J 卡片分区</span><span>抽屉 E 卡片+描述表</span><span>登录页 C 右侧浮卡</span><span>深色皮肤 B 石墨灰</span></div><span class="sp"></span><div class="bk-theme" id="bkTheme"></div><span class="bk-date">按 2026-10-08 第一批评审结果绘制 · 1440×900 · 三端一致</span></header>
+<div class="bk-body">
+  <nav class="bk-nav" id="bkNav"></nav>
+  <main class="bk-main">
+    <div class="bk-bar" id="bkBar"></div>
+    <div class="bk-host" id="bkStage"></div>
+    <div id="bkSpec" hidden></div>
+    <section class="bk-notes" id="bkNotes"></section>
+  </main>
+</div>
+<div class="bk-full" id="bkFull" hidden>
+  <div class="bk-full-bar" id="bkFullBar"></div>
+  <div class="bk-host" id="bkFullStage"></div>
+</div>
+<script>
+/*{{data.js}}*/
+</script>
+<script>
+/*{{data2.js}}*/
+</script>
+<script>
+/*{{tokens.js}}*/
+</script>
+<script>
+/*{{app.js}}*/
+</script>
+<script>
+/*{{pages.js}}*/
+</script>
+<script>
+/*{{book.js}}*/
+</script>
+</body>
+</html>

+ 329 - 0
src/test.mjs

@@ -0,0 +1,329 @@
+// 在最终交付的 HTML 里实际点击每一类交互并断言结果。用法:node test.mjs。任何一项失败退出码为 1,pack.py 不出包。
+import { chromium } from "playwright";
+import { fileURLToPath, pathToFileURL } from "url";
+import path from "path";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const url = pathToFileURL(path.join(root, "CMS-UI完整设计.html")).href;
+let fails = 0, passes = 0;
+const ok = (cond, msg) => { if (cond) passes++; else { fails++; console.log("  ✗", msg); } };
+const section = (t) => console.log("•", t);
+
+const browser = await chromium.launch();
+const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
+const page = await ctx.newPage();
+const errs = [];
+page.on("pageerror", (e) => errs.push(e.message));
+page.on("console", (m) => m.type() === "error" && errs.push(m.text()));
+await page.goto(url);
+const S = () => page.evaluate(() => JSON.parse(JSON.stringify({ page: window.__book.S.page, end: window.__book.S.end, variant: window.__book.S.variant })));
+const stage = "#bkStage";
+const cv = `${stage} .cv`;
+const count = (sel) => page.$$eval(sel, (els) => els.length);
+const visible = (sel) => page.isVisible(sel);
+const text = (sel) => page.$eval(sel, (el) => el.textContent);
+const has = (sel) => page.$(sel).then((h) => !!h);
+const goPage = async (key) => { await page.click(`.bk-item[href="#p/${key}"]`); await page.waitForTimeout(50); };
+const st = () => page.evaluate(() => { const s = document.querySelector("#bkStage")._spec.st; return { active: s.active, tags: s.tags, col: s.col, ovls: s.ovls, pop: s.pop, sel: [...s.sel], sfOpen: s.sfOpen, tab: s.tab, open: [...s.open] }; });
+
+/* ---------- 设计稿外壳 ---------- */
+section("外壳:目录、路由、端切换、变体、全屏");
+ok((await S()).page === "spec" && (await visible("#bkSpec")), "默认打开规范摘要");
+ok((await count(".bk-item")) === 16, "目录 16 个页面");
+await goPage("home");
+ok((await S()).page === "home" && (await page.evaluate(() => location.hash)) === "#p/home/issue", "点目录到首页,hash 带端");
+ok((await visible(`${cv}.shell`)) && (await visible(`${cv} .nb .crumb`)) && (await visible(`${cv} .nb .td`)), "首页是外壳画布:顶栏有面包屑和交易日");
+await page.click('.bk-seg[data-seg="end"] button[data-v="trs"]');
+ok((await text(`${cv} .brand .nm`)).includes("TRS"), "切到 TRS 端后系统名变化");
+ok((await text(`${cv} .hm-st .card-hd`)).includes("委托"), "TRS 首页看的是委托");
+await page.click('.bk-seg[data-seg="end"] button[data-v="issue"]');
+await page.keyboard.press("ArrowRight");
+ok((await S()).page === "list", "按 → 到下一页(列表页)");
+await page.keyboard.press("ArrowLeft");
+ok((await S()).page === "home", "按 ← 回到首页");
+await page.click("[data-full]");
+ok(await visible("#bkFull"), "全屏打开");
+ok(await visible("#bkFullStage .cv.shell"), "全屏里有外壳画布");
+await page.keyboard.press("Escape");
+ok(!(await visible("#bkFull")), "Esc 退出全屏");
+await page.click(".sp-go, .bk-item[href='#p/spec']").catch(() => {});
+await goPage("spec");
+await page.click('.sp-go[href="#p/login"]');
+ok((await S()).page === "login", "规范摘要里的链接跳到登录页");
+
+/* ---------- 登录页 ---------- */
+section("登录页:C 右侧浮卡·全幅底图,变体与画面里的交互");
+ok((await visible(`${cv}.login .lg-card`)) && (await visible(`${cv} .lg-deco`)) && (await count(`${cv} .lg-ent`)) === 3, "登录页画布:右侧浮卡、底图、三张三端入口");
+ok((await text(`${cv} .lg-ent.on`)).includes("发行管理系统") && (await text(`${cv} .lg-c-sys`)).includes("发行管理系统") && (await count(`${cv} .lg-chip`)) === 6, "发行端:入口高亮发行端,左侧系统名和 6 个业务模块");
+ok(await page.$eval(`${cv} .lg-card`, (el) => { const r = el.getBoundingClientRect(), c = el.closest(".cv").getBoundingClientRect(), z = c.width / 1440; return Math.round((c.right - r.right) / z) === 96 && Math.round(r.width / z) === 440; }), "浮卡 440px、离右边 96px");
+ok((await page.$eval(`${cv} .lg-ipt`, (el) => el.offsetHeight)) === 40, "登录页输入框 40px");
+await page.click(`${cv} [data-act="lv"][data-v="reset"]`);
+ok((await text(`${cv} .lg-ttl`)).includes("重置密码"), "点「忘记密码?」切到重置密码");
+ok((await page.$eval('.bk-seg[data-seg="variant"] button.on', (b) => b.dataset.v)) === "reset", "变体按钮同步");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="login"]');
+ok((await text(`${cv} .lg-ttl`)).includes("登录"), "变体按钮切回登录");
+const cap0 = await page.$eval(`${cv} .lg-cap`, (el) => el.innerHTML);
+await page.click(`${cv} [data-act="cap"]`);
+ok((await page.$eval(`${cv} .lg-cap`, (el) => el.innerHTML)) !== cap0, "点验证码换一张");
+await page.click(`${cv} [data-act="eye"]`);
+ok((await text(`${cv} .lg-card`)).includes("123456"), "点眼睛显示密码");
+await page.click(`${cv} [data-act="eye"]`);
+ok(!(await text(`${cv} .lg-card`)).includes("123456"), "再点隐藏密码");
+await page.click(`${cv} [data-act="rem"]`);
+ok((await count(`${cv} [data-act="rem"] .ck.on`)) === 0, "取消「记住我」");
+await page.click(`${cv} [data-act="rem"]`);
+ok((await count(`${cv} [data-act="rem"] .ck.on`)) === 1, "再点勾上「记住我」");
+await page.click(`${cv} .lg-ent[data-end="sales"]`);
+ok((await page.$eval('.bk-seg[data-seg="end"] button.on', (b) => b.dataset.v)) === "sales" && (await text(`${cv} .lg-brand`)).includes("销售") && (await text(`${cv} .lg-ent.on`)).includes("销售管理系统"), "点三端入口「前往登录」切到销售端,上方「看哪一端」同步");
+ok((await count(`${cv} .lg-chip`)) === 7 && (await page.evaluate(() => location.hash)) === "#p/login/sales/login", "销售端 7 个业务模块,地址栏同步");
+await page.click('.bk-seg[data-seg="end"] button[data-v="trs"]');
+ok((await text(`${cv} .lg-c-sys`)).includes("TRS") && (await count(`${cv} .lg-chip`)) === 8, "切到 TRS 端:左侧系统名和 8 个业务模块");
+await page.click(`${cv} .lg-btn`);
+ok(await visible(`${cv} > .toast`), "点登录出现提示");
+await page.click('.bk-seg[data-seg="end"] button[data-v="issue"]');
+
+/* ---------- 首页 + 外壳交互 ---------- */
+section("首页与外壳:菜单、标签页、收起、搜索、通知、用户菜单");
+await goPage("home");
+ok((await st()).active === "首页", "首页画布 active=首页");
+await page.click(`${cv} .hm-tile`);
+ok((await st()).active === "共赢票据管理/订单管理", "点状态数字进入订单管理");
+ok(await visible(`${cv} .pg-list .sf`), "订单管理显示搜索区");
+await page.click(`${cv} .tvi[data-k="首页"]`);
+ok((await st()).active === "首页", "点标签页回首页");
+await page.click(`${cv} .hm-sc >> nth=3`);
+ok((await st()).active === "风控管理/共赢票据风险监控", "快捷入口打开风险监控");
+ok(await visible(`${cv} .ptabs`), "风险监控页有页面级页签");
+await page.click(`${cv} .ptabs .tab[data-v="status-log"]`);
+ok((await st()).tab.risk === "status-log" && (await text(`${cv} .tbl th`)).includes("票据编号"), "切换到风险状态变更记录");
+await page.click(`${cv} .ptabs .tab[data-v="monitor"]`);
+await page.click(`${cv} .tbl td .btn.dng`);
+ok((await st()).ovls.includes("confirm") && (await text(`${cv} .cfm-hd b`)).includes("强平卖出"), "危险行的「强平卖出」弹出确认框");
+await page.click(`${cv} .cfm-ft .btn >> nth=0`);
+ok((await st()).ovls.length === 0, "取消后确认框关闭");
+await page.click(`${cv} .tbl td .btn.txt >> nth=0`);
+ok((await st()).ovls.includes("note") && (await visible(`${cv} .dw-note`)), "风险监控「详情」打开票据详情抽屉");
+await page.click(`${cv} .dw-tabs .tab[data-v="changes"]`);
+ok((await text(`${cv} .dw-bd`)).includes("票据变动记录") && (await count(`${cv} .dw-bd .empty`)) >= 3, "变动记录页签:空表显示空状态");
+await page.click(`${cv} .dw-tabs .tab[data-v="main"]`);
+ok((await text(`${cv} .dw-bd`)).includes("主票据信息"), "切回主页");
+await page.click(`${cv} .dw-note .sec-hd.sub .btn[data-o="fill"]`);
+ok((await st()).ovls.join() === "note,fill", "票据详情里点「补录」叠开补录弹窗");
+await page.click(`${cv} .ovl.lv2 [data-act="ovclose"] >> nth=0`);
+ok((await st()).ovls.join() === "note", "关闭补录后回到抽屉");
+await page.click(`${cv} .dw-hd .icb[data-act="ovclose"]`);
+ok((await st()).ovls.length === 0, "关闭抽屉");
+await page.click(`${cv} .mg-t[data-g="清算管理"]`);
+ok((await st()).open.includes("清算管理"), "展开菜单分组");
+await page.click(`${cv} .mi[data-k="清算管理/共赢票据清算"]`);
+ok(await visible(`${cv} .clr .ctabs`), "清算页:卡片内页签");
+await page.click(`${cv} .ctabs .tab[data-v="logs"]`);
+ok((await text(`${cv} .clr .card-bd`)).includes("清算记录") && (await has(`${cv} .ctabs .btn.dng`)), "清算日志页签:有清算记录和停止按钮");
+await page.click(`${cv} .ctabs .btn.dng`);
+ok((await text(`${cv} .cfm-hd b`)).includes("停止当前任务"), "停止任务弹确认框");
+await page.click(`${cv} .cfm-ft .btn >> nth=1`);
+await page.click(`${cv} .ctabs .tab[data-v="clearing"]`);
+await page.click(`${cv} .ctabs .btn[data-kind="redo"]`);
+ok((await text(`${cv} .cfm-hd b`)).includes("重新清算"), "重新清算弹确认框");
+await page.click(`${cv} .cfm-ft .btn >> nth=0`);
+await page.click(`${cv} .tvi[data-k="清算管理/共赢票据清算"] [data-act="tagx"]`);
+ok(!(await st()).tags.includes("清算管理/共赢票据清算"), "关闭标签页");
+await page.click(`${cv} .tvi[data-k="共赢票据管理/票据管理"]`, { button: "right" });
+ok(await visible(`${cv} .ctx`), "标签页右键菜单");
+await page.click(`${cv} .ctx [data-do="others"]`);
+ok((await st()).tags.length === 2, "关闭其它后只剩首页和当前页");
+await page.click(`${cv} [data-act="col"]`);
+ok((await st()).col && (await page.$eval(`${cv} .sd`, (el) => el.offsetWidth)) === 64, "收起侧栏到 64px");
+await page.click(`${cv} [data-act="col"]`);
+await page.click(`${cv} .icb[data-pop="ms"]`);
+ok(await visible(`${cv} .ms-pop`), "菜单搜索弹出");
+await page.fill(`${cv} [data-ms]`, "清算");
+ok((await count(`${cv} .ms-list .pop-i`)) >= 3 && (await text(`${cv} .ms-list`)).includes("清算"), "搜索结果包含「清算」");
+await page.keyboard.press("Enter");
+ok((await st()).active.includes("清算") && !(await st()).pop, "回车打开第一个结果");
+await page.click(`${cv} [data-pop="bell"]`);
+ok((await count(`${cv} .bell-i`)) === 2, "通知弹层 2 条");
+await page.click(`${cv} .user`);
+ok(await visible(`${cv} .pop-i[data-k="个人中心"]`), "用户菜单");
+await page.click(`${cv} .pop-i[data-k="个人中心"]`);
+ok((await st()).active === "个人中心" && (await visible(`${cv} .prof`)), "用户菜单进入个人中心");
+await page.click(`${cv} .prof-row .btn[data-o="pwd"]`);
+ok((await st()).ovls.includes("pwd") && (await text(`${cv} .dg-hd`)).includes("修改密码"), "安全设置「修改」打开修改密码弹窗");
+await page.click(`${cv} .dg-ft .btn >> nth=0`);
+await page.click(`${cv} .user`);
+await page.click(`${cv} .pop-i[data-kind="logout"]`);
+ok((await text(`${cv} .cfm-hd b`)).includes("退出登录"), "退出登录弹确认框");
+await page.click(`${cv} .cfm-ft .btn >> nth=0`);
+
+/* ---------- 列表页 → 抽屉 → 弹窗 ---------- */
+section("列表页:搜索展开、勾选、详情抽屉、补录弹窗");
+await goPage("list");
+ok((await st()).active === "共赢票据管理/订单管理" && (await count(`${cv} .sf .sf-i`)) === 5, "订单管理:搜索收起态 5 项");
+await page.click(`${cv} [data-act="sf"]`);
+ok((await count(`${cv} .sf .sf-i`)) === 11, "展开后 11 项");
+await page.click(`${cv} [data-act="rowsel"] >> nth=0`);
+ok((await st()).sel.length === 1 && (await visible(`${cv} .selbar`)), "勾选一行出现选择条");
+await page.click(`${cv} [data-act="allsel"]`);
+ok((await st()).sel.length === 12, "全选");
+await page.click(`${cv} [data-act="clearsel"]`);
+ok((await st()).sel.length === 0, "清空");
+await page.click(`${cv} .tbl td .btn[data-o="order"] >> nth=0`);
+ok((await st()).ovls.join() === "order" && (await visible(`${cv} .dw-e`)), "「详情」打开订单详情抽屉");
+await page.click(`${cv} .dw-e .btn[data-o="fill"]`);
+ok((await st()).ovls.join() === "order,fill" && (await visible(`${cv} .ovl.lv2 .dg-s`)), "抽屉里「补录」叠开弹窗");
+await page.click(`${cv} .ovl.lv2 [data-act="hsysset"][data-v="TRS"]`);
+ok((await text(`${cv} .ovl.lv2 .dg-bd`)).includes("对冲账号") && (await count(`${cv} .ovl.lv2 .sf-rt`)) === 0, "对冲系统切到 TRS 后字段变化、结果块消失");
+await page.click(`${cv} .ovl.lv2 [data-act="hsysset"][data-v="自定义"]`);
+ok((await count(`${cv} .ovl.lv2 .sf-rt`)) === 2 && (await count(`${cv} .ovl.lv2 .stp`)) === 2, "切回自定义:两个结果块、两个加减数字框");
+await page.click(`${cv} .ovl.lv2 .dg-ft .btn >> nth=0`);
+ok((await st()).ovls.join() === "order", "取消补录后回到抽屉");
+await page.click(`${cv} .dw-e .tbl .btn.dng >> nth=0`);
+ok((await st()).ovls.join() === "order,confirm" && (await text(`${cv} .cfm-hd b`)).includes("撤单"), "对冲记录「撤单」弹确认框");
+await page.click(`${cv} .cfm-ft .btn >> nth=1`);
+await page.click(`${cv} .dw-hd [data-act="ovclose"]`);
+ok((await st()).ovls.length === 0, "关闭抽屉");
+await page.click('.bk-seg[data-seg="end"] button[data-v="sales"]');
+ok(!(await has(`${cv} [data-act="allsel"]`)) && (await count(`${cv} .sf .sf-i`)) === 5, "销售端订单管理:没有勾选列");
+await page.click('.bk-seg[data-seg="end"] button[data-v="trs"]');
+ok(!(await has(`${cv} .tbl td .btn.txt`)) && (await text(`${cv} .tbl th`)).includes("用户ID"), "TRS 委托查询:没有操作列");
+await page.click('.bk-seg[data-seg="end"] button[data-v="issue"]');
+
+/* ---------- 直接打开的弹层页面 ---------- */
+section("弹层页面:订单详情、补录、新建投资者、TRS 下单、票据详情、确认框");
+await goPage("drawer");
+ok((await st()).ovls.join() === "order" && (await count(`${cv} .kvg.bd3`)) === 2, "订单详情抽屉:两张描述表");
+await goPage("dlg-fill");
+ok((await st()).ovls.join() === "order,fill" && (await count(`${cv} .ovl.lv2 .sf-tile`)) === 2 && (await count(`${cv} .ovl.lv2 .sf-row`)) >= 5 && !(await has(`${cv} .ovl.lv2 .dg-s.lt`)) && (await page.$eval(`${cv} .ovl.lv2 .sf-row > label`, (el) => getComputedStyle(el).textAlign)) === "right", "补录弹窗:顶部两个信息块、标签在左,叠在抽屉上");
+await goPage("dlg-investor");
+ok((await st()).ovls.join() === "investor" && (await count(`${cv} .dgc`)) === 4 && (await text(`${cv} .brand`)).includes("销售"), "新建投资者:四张卡片,在销售端");
+const lackBefore = await text(`${cv} .dg-ft .hint`);
+await page.click(`${cv} [data-act="chk"][data-i="0"]`);
+ok(!(await has(`${cv} .dgc .alt-warning`)) && (await text(`${cv} .dg-ft .hint`)) !== lackBefore, "勾选 A 类后警告消失、必填计数变化");
+await page.click(`${cv} [data-act="chk"][data-i="0"]`);
+await goPage("dlg-trs");
+ok((await st()).ovls.join() === "trs" && (await visible(`${cv} .dg-trs .qpanel`)), "TRS 下单弹窗:有五档行情");
+ok((await text(`${cv} .dg-ft .btn.dng-pri`)).includes("确认买入") && (await text(`${cv} .dg-hd .ttl`)) === "买入" && (await count(`${cv} .sf-tile`)) === 2, "默认买入:标题是方向,红色确认按钮,两个信息块");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="sell"]');
+ok((await text(`${cv} .dg-ft .btn.ok-pri`)).includes("确认卖出") && (await text(`${cv} .sf-res`)).includes("卖出金额") && (await text(`${cv} .sf-tiles`)).includes("可卖股数"), "切到卖出:绿色按钮、金额文案和信息块变化");
+await page.click(`${cv} .ql.bid >> nth=0`);
+ok((await text(`${cv} .stp .val >> nth=0`)) === "11.64", "点买一档位填入委托价 11.64");
+await page.click(`${cv} .qchip[data-k="qty"][data-v="6,000"]`);
+ok((await text(`${cv} .dg-ft .btn.ok-pri`)).includes("6,000"), "点 1/2 后数量变成 6,000");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="buy"]');
+await page.click(`${cv} .qchip[data-k="price"][data-v="11.64"]`);
+ok((await text(`${cv} .stp .val >> nth=0`)) === "11.64" && (await has(`${cv} .ql.bid.on`)), "点「最新 11.64」后档位同步高亮");
+await page.click(`${cv} .dg-hd [data-act="ovclose"]`);
+ok((await st()).ovls.length === 0 && (await visible(`${cv} .trs-acct`)), "关闭后是交易下单页");
+await page.click(`${cv} .ctabs .tab[data-v="ord"]`);
+ok((await text(`${cv} .tb-card .tbl thead`)).includes("委托编号"), "交易下单页切到当日委托");
+await page.click(`${cv} .trs-acct .btn[data-dir="卖出"]`);
+ok((await st()).ovls.join() === "trs" && (await text(`${cv} .dg-hd .ttl`)) === "卖出", "页面「卖出」按钮打开卖出弹窗");
+await goPage("drawer-note");
+ok((await st()).ovls.join() === "note" && (await count(`${cv} .dw-note .card`)) === 3, "票据详情抽屉:三张卡片");
+await goPage("confirm");
+ok((await text(`${cv} .cfm-hd b`)).includes("撤单"), "确认框页默认撤单");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="ratio"]');
+ok((await text(`${cv} .cfm-hd b`)).includes("确认调整") && (await has(`${cv} .cfm-ic.warning`)), "切到比例确认:警告图标");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="cancel"]');
+
+/* ---------- 其他页 ---------- */
+section("其他页:页签、清算、个人中心、404/401");
+await goPage("tabs");
+ok((await count(`${cv} .ptabs .tab`)) === 3, "风险监控三个页签");
+await goPage("clearing");
+ok((await count(`${cv} .tbl tbody tr`)) === 6 && (await has(`${cv} .btn.pri.is-dis`)), "清算:6 个步骤,一键清算禁用");
+await goPage("profile");
+ok(await visible(`${cv} .prof-id`), "个人中心");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="pwd"]');
+ok((await st()).ovls.join() === "pwd", "变体切到修改密码弹窗");
+await goPage("err");
+ok((await text(`${cv} .err-t b`)).includes("页面不存在"), "404");
+await page.click('.bk-seg[data-seg="variant"] button[data-v="401"]');
+ok((await text(`${cv} .err-t b`)).includes("没有访问权限"), "401");
+await page.click(`${cv} .errp .btn.pri`);
+ok((await st()).active === "首页", "401 页「返回首页」");
+await goPage("kit");
+ok(await visible(`${cv}.kit .kit-grid`), "组件总览");
+await goPage("feedback");
+ok((await count(`${cv} .empty.box`)) === 5 && (await count(`${cv} .result`)) === 2, "反馈类总览:5 个空状态、2 个结果提示");
+
+/* ---------- 换肤 ---------- */
+section("换肤:顶栏皮肤切换、地址、画面里的入口、规范摘要对照");
+const cvVar = (name) => page.$eval(cv, (el, n) => getComputedStyle(el).getPropertyValue(n).trim(), name);
+const cvTheme = () => page.$eval(cv, (el) => el.dataset.v);
+ok((await count("#bkTheme .bk-seg button")) === 2 && (await page.$eval("#bkTheme .bk-seg button.on", (b) => b.dataset.v)) === "f", "顶栏皮肤 2 项(浅色 F、深色 B),默认浅色");
+await goPage("home");
+const light = { bg: await cvVar("--bg-page"), sd: await cvVar("--sd-bg"), pri: await cvVar("--pri-bg") };
+ok((await cvTheme()) === "f" && !(await has(`${cv}[data-dark]`)) && (await cvVar("--grad")).includes("gradient"), "浅色画布 data-v=f,无 data-dark,主色渐变令牌是渐变");
+if (!(await st()).open.includes("清算管理")) await page.click(`${cv} .mg-t[data-g="清算管理"]`); // 首页画布状态是缓存的,前面可能已展开
+await page.click(`${cv} .mi[data-k="清算管理/共赢票据清算"]`);
+await page.click('#bkTheme button[data-v="db"]');
+ok((await cvTheme()) === "db" && (await has(`${cv}[data-dark]`)) && (await cvVar("--bg-page")) === "#141518" && (await cvVar("--grad")) === "#4A8CF7", "切到深色 B:data-v=db、有 data-dark、底色石墨灰、主色令牌实色");
+ok((await page.evaluate(() => location.hash)) === "#p/home/issue/t=db", "地址带 /t=db");
+ok((await st()).active === "清算管理/共赢票据清算" && (await visible(`${cv} .clr .ctabs`)), "换肤后画布状态不丢:仍在清算页");
+const noShadow = (v) => v === "none" || /^rgba\(0, 0, 0, 0\) 0px 0px 0px 0px$/.test(v); // 令牌里用「0 0 0 0 transparent」代替 none
+const cs = (sel, prop) => page.$eval(sel, (el, pr) => getComputedStyle(el)[pr], prop);
+ok(!(await cs(`${cv} .btn.pri`, "backgroundImage")).includes("gradient") && noShadow(await cs(`${cv} .btn.pri`, "boxShadow")), "深色 B 的主按钮实心无渐变、无投影");
+ok((await cs(`${cv} .tg`, "backgroundColor")) === "rgba(0, 0, 0, 0)" && (await cs(`${cv} .tg`, "borderTopColor")) !== "rgba(0, 0, 0, 0)", "深色 B 的状态标签只描边不铺底");
+ok(parseFloat(await cs(`${cv} .card`, "borderTopWidth")) > 0 && noShadow(await cs(`${cv} .card`, "boxShadow")), "深色 B 的卡片有描边、无投影(画布有 zoom,描边宽度按比例算)");
+await page.click('.bk-seg[data-seg="end"] button[data-v="trs"]');
+ok((await cvTheme()) === "db" && (await page.evaluate(() => location.hash)) === "#p/home/trs/t=db", "切端后皮肤保持,地址同步");
+await page.click('.bk-seg[data-seg="end"] button[data-v="issue"]');
+await page.click("[data-full]");
+ok((await count("#bkFullBar .bk-seg[data-seg=theme] button")) === 2 && (await page.$eval("#bkFullStage .cv", (el) => el.dataset.v)) === "db", "全屏条也有皮肤切换,全屏画布是深色 B");
+await page.click('#bkFullBar .bk-seg[data-seg=theme] button[data-v="f"]');
+ok((await page.$eval("#bkFullStage .cv", (el) => el.dataset.v)) === "f" && (await page.$eval("#bkTheme .bk-seg button.on", (b) => b.dataset.v)) === "f", "全屏里切回浅色,顶栏同步");
+await page.click('#bkFullBar .bk-seg[data-seg=theme] button[data-v="db"]');
+await page.keyboard.press("Escape");
+await page.click(`${cv} .user`);
+await page.click(`${cv} .pop-i[data-act="theme"]`);
+ok((await cvTheme()) === "f" && !(await st()).pop, "用户菜单「界面设置」切回浅色,弹层关闭");
+await page.click(`${cv} .user`);
+await page.click(`${cv} .pop-i[data-act="theme"]`);
+ok((await cvTheme()) === "db", "再点回到深色 B");
+await goPage("login");
+ok((await cvTheme()) === "db" && (await has(`${cv} .lg-actions [data-act="theme"] .ic`)), "登录页沿用深色 B,右上角是主题按钮");
+await page.click(`${cv} .lg-actions [data-act="theme"]`);
+ok((await cvTheme()) === "f" && (await page.$eval("#bkTheme .bk-seg button.on", (b) => b.dataset.v)) === "f" && (await page.evaluate(() => location.hash)) === "#p/login/issue/login", "登录页太阳按钮切回浅色,顶栏和地址同步(浅色不带 /t=)");
+await page.click(`${cv} .lg-actions [data-act="theme"]`);
+ok((await cvTheme()) === "db", "再点回深色 B");
+await goPage("kit");
+ok((await text(`${cv} .kit-top h1`)).includes("深色皮肤 B 石墨灰") && (await text(`${cv} .kit-top`)).includes("#4A8CF7"), "组件总览标题跟随皮肤,主色是 B 的 #4A8CF7");
+await goPage("spec");
+ok((await count(".sp-theme")) === 2 && (await text(".sp-theme.on")).includes("石墨灰") && (await count(".sp-tbl.cmp tbody tr")) === 9 && (await count(".sp-pick")) === 6, "规范摘要「换肤」两张卡片、当前是 B、对照表 9 行;选型结果 6 项含深色皮肤 B");
+ok((await text("#bkSpec h2 >> nth=3")).includes("石墨灰"), "规范摘要「颜色」标题写着当前皮肤");
+await page.click('.sp-theme[data-theme="f"] [data-theme-go]');
+ok((await S()).page === "home" && (await cvTheme()) === "f", "「用这套皮肤看首页」切到浅色并跳到首页");
+await goPage("spec");
+await page.click('.sp-theme[data-theme="db"] [data-theme-go]');
+ok((await S()).page === "home" && (await cvTheme()) === "db", "再从规范摘要切到深色 B");
+await goPage("spec");
+await page.click('.sp-go[href="#p/login"]');
+ok((await cvTheme()) === "db" && (await page.evaluate(() => location.hash)) === "#p/login/issue/login/t=db", "规范摘要里不带皮肤的链接也沿用当前皮肤");
+await page.click('#bkTheme button[data-v="f"]');
+ok((await cvTheme()) === "f" && (await cvVar("--sd-bg")) === light.sd && (await cvVar("--pri-bg")) === light.pri, "切回浅色 F,令牌和最初一致");
+
+/* ---------- 单独模式 ---------- */
+section("单独模式");
+for (const h of ["#solo/login/trs/reset", "#solo/home/sales", "#solo/dlg-fill", "#solo/err/issue/401", "#solo/feedback", "#solo/home/issue/t=db", "#solo/dlg-trs/trs/sell/t=db", "#solo/kit/t=db"]) {
+  const p2 = await ctx.newPage();
+  p2.on("pageerror", (e) => errs.push(e.message));
+  await p2.goto(url + h);
+  await p2.waitForTimeout(100);
+  ok(await p2.isVisible("#solo .cv"), `单独模式 ${h} 有画布`);
+  const want = (h.match(/\/t=([a-z]+)/) || [])[1] || "f";
+  ok((await p2.$eval("#solo .cv", (el) => el.dataset.v)) === want, `单独模式 ${h} 皮肤是 ${want}`);
+  if (h.startsWith("#solo/home/issue/t=db")) {
+    await p2.click("#solo .user");
+    await p2.click('#solo .pop-i[data-act="theme"]');
+    ok((await p2.$eval("#solo .cv", (el) => el.dataset.v)) === "f", "单独模式里「界面设置」切到浅色");
+    await p2.click("#solo .user");
+    await p2.click('#solo .pop-i[data-act="theme"]');
+    ok((await p2.$eval("#solo .cv", (el) => el.dataset.v)) === "db", "单独模式里再点回深色 B");
+  }
+  await p2.close();
+}
+ok(errs.length === 0, "没有脚本错误:" + errs.join(" | "));
+await browser.close();
+console.log(`\n通过 ${passes},失败 ${fails}`);
+process.exit(fails ? 1 : 0);

+ 14 - 0
src/theme.css

@@ -0,0 +1,14 @@
+/* ============ 皮肤之外的结构差异(取自分项选型 themes.css 的 F 段,改成读令牌,四套皮肤共用)。放在最后 ============ */
+.cv .tg { padding: 0 9px; }
+.cv .ck.on, .cv .ck.half { background: var(--grad); border-color: transparent; }
+.cv .tab.on::after { height: 3px; border-radius: 3px 3px 0 0; background: var(--grad); }
+.cv .alt { border: 0; border-radius: 10px; }
+.cv .tile .bar i { background: var(--grad); }
+.cv .dlg-hd, .cv .dlg-ft { border-color: var(--bd-light); }
+.cv .hm-tile b { font-weight: 600; }
+.cv .lg { background: var(--canvas, var(--bg-page)); }
+/* 深色皮肤:单选选中后圆点用白色,深色底上才看得见;标签文字用「亮色字」令牌 */
+.cv[data-dark] .rd.on { background: #fff; }
+.cv[data-dark] .kit-mask::before { background: repeating-linear-gradient(0deg, rgba(255,255,255,.03) 0 40px, transparent 40px 41px); }
+/* 深色 B 的提示条、登录页模块标签只描边:描边令牌是半透明彩色,底色透明 */
+.cv[data-v="db"] .alt { border: 1px solid var(--x-bd); background: var(--fill-1); }

+ 120 - 0
src/tokens.js

@@ -0,0 +1,120 @@
+/* 设计令牌:浅色 F「晴空渐变」(第一批选型最终结果)+ 深色 B「石墨灰」(2026-10-08 三版深色候选里用户选定;未选中的 A 夜空蓝、C 极光玻璃归档在 归档/tokens-深色候选AC.js)。
+ * 每套皮肤是同一份令牌表的不同取值;画布根元素 .cv[data-v=皮肤] 上生成为 CSS 变量,所有页面只读变量,不写死颜色。
+ * 换肤只改颜色与质感(底色、卡片、侧栏、按钮、表格、标签、投影/描边、装饰),不改尺寸、圆角、字体、排布。
+ * 锁定口径:控件 32px、表格行高 40px、正文 14px;1440×900 基准;三端一致;买入红、卖出绿。
+ */
+window.TOKENS = (function () {
+  const SYS = '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif';
+  const INTER = '"Inter", ' + SYS;
+  const NOSH = "0 0 0 0 transparent"; // 代替 none,这样才能和聚焦光晕拼成合法的 box-shadow
+  const SIZE = { "r-ctl": "8px", "r-card": "14px", "r-dlg": "16px", "r-tag": "6px", "r-pop": "10px", "ck-r": "5px", "card-hd-h": "52px", "tag-h": "22px", "tag-fs": "12px", "tag-fw": "500", "th-fw": "500", "th-fs": "13px", "fw-title": "600", "fw-strong": "600", "btn-fw": "500", font: SYS, "font-num": INTER };
+
+  /* ---------------- 浅色 F 晴空渐变(2026-10-08 侧栏改成分项选型框架 E 的浅色侧栏配色:白底微透、深灰字;其余不动) ---------------- */
+  const F_GRAD = "linear-gradient(135deg, #5AA9FF 0%, #2F7FF5 100%)";
+  const f = {
+    key: "f", letter: "F", name: "晴空渐变", label: "浅色", dark: false, title: "基础视觉 F 晴空渐变",
+    tagline: "已选定的浅色主题(默认):晴空蓝主色、渐变主按钮、蓝白渐变底色、白卡片淡蓝投影、浅色侧栏。",
+    points: ["主色 #3C8CFF,文字链接 #1668DC;主按钮左上浅、右下深的渐变加淡蓝投影。", "页面底色蓝白渐变、右上角一团浅蓝光晕;卡片白底 14px 圆角淡蓝投影。", "侧栏是白底微透(透出底色的蓝白渐变)、深灰字,选中项淡蓝渐变块+蓝字,标志是晴空蓝渐变;表头很淡的蓝底;状态是无描边的浅色圆角标签。"],
+    cmp: { 底色: "蓝白渐变+右上浅蓝光晕", 卡片: "白底、淡蓝投影、无描边", 侧栏: "白底微透、深灰字,选中项淡蓝块+蓝字", 主按钮: "蓝色渐变+淡蓝投影", 次按钮与输入框: "白底描边,悬停描边变浅蓝", 表头与行: "淡蓝表头,白行,悬停极淡蓝", 状态标签: "浅色底、彩色字、无描边", 投影与描边: "靠投影分层,几乎不用描边", 装饰: "渐变、光晕" },
+    notes: { buttons: "32px;主按钮渐变加淡蓝投影,次按钮白底描边", status: "颜色按字典 tag_type 取;无描边的浅色圆角标签", dialog: "淡蓝投影,圆角 16px;排布见「补录弹窗」", kv: "详情展示统一用带边框的描述表,灰底标签格" },
+    tokens: {
+      ...SIZE,
+      "c-primary": "#3C8CFF", "c-primary-hover": "#63A5FF", "c-primary-active": "#2B76E6", "c-primary-bg": "#E9F3FF", "c-primary-border": "#BBD9FF", "c-link": "#1668DC",
+      grad: F_GRAD, "grad-h": "linear-gradient(135deg, #74B8FF 0%, #4A92FA 100%)",
+      "pri-bg": "var(--grad)", "pri-bg-h": "var(--grad-h)", "pri-bd": "transparent", "pri-bd-h": "transparent", "pri-t": "#FFFFFF",
+      "logo-bg": "var(--grad)", "logo-t": "#FFFFFF",
+      buy: "#CB2634", sell: "#008026",
+      t1: "#15233B", t2: "#44546C", t3: "#8492A6", t4: "#BAC4D2",
+      "bd-ctl": "#D5E0EE", bd: "#E4ECF5", "bd-light": "#EEF3F9",
+      "fill-1": "#F5F9FE", "fill-2": "#EDF3FB", "fill-3": "#E0E9F5",
+      "bg-page": "#F1F6FD", "bg-card": "#FFFFFF", "frame-outer": "#F7FAFF",
+      canvas: "radial-gradient(900px 460px at 88% -8%, rgba(110,180,255,.34), transparent 62%), radial-gradient(760px 420px at 4% 0%, rgba(150,170,255,.20), transparent 60%), linear-gradient(180deg, #EAF3FF 0%, #F3F8FE 38%, #F6F9FD 100%)",
+      "pa-bg": "transparent",
+      "bg-pop": "#FFFFFF", "bg-ctl": "#FFFFFF", "bg-dlg": "var(--bg-card)", "td-bg": "var(--bg-card)",
+      "tip-bg": "rgba(17,19,24,.88)", "tip-t": "#FFFFFF", "hover-veil": "rgba(0,0,0,.04)", veil: "rgba(255,255,255,.72)",
+      "card-bd": "0 solid transparent", "card-sh": "0 1px 2px rgba(30,90,180,.05), 0 8px 24px rgba(30,90,180,.07)", "card-hd-bg": "transparent", "card-hd-bd": "#EEF3F9",
+      "sh-pop": "0 0 0 1px rgba(30,90,180,.06), 0 10px 28px rgba(30,90,180,.14)", "sh-dlg": "0 24px 64px rgba(20,60,130,.22)", "pop-bd": "0 solid transparent", mask: "rgba(21,45,90,.38)",
+      "card-bf": "none", "pop-bf": "none", "dlg-bf": "none", "sd-bf": "none",
+      "btn-bg": "#FFFFFF", "btn-bd": "#D5E0EE", "btn-t": "#15233B", "btn-bg-h": "#F5F9FE", "btn-bd-h": "#9CC5FF", "btn-t-h": "#1668DC",
+      "btn-sh": "0 1px 2px rgba(30,90,180,.05)", "btn-sh-h": "0 2px 6px rgba(30,90,180,.10)", "btn-pri-sh": "0 4px 12px rgba(47,127,245,.30)",
+      "ipt-bg": "#FFFFFF", "ipt-bd": "#D5E0EE", "ipt-sh": NOSH, "ipt-bg-h": "#FFFFFF", "ipt-bd-h": "#9CC5FF",
+      focus: "0 0 0 3px rgba(60,140,255,.18)", "focus-bd": "#3C8CFF",
+      "ck-bd": "#C3D1E3", "sw-off": "#BAC4D2", "sw-ring": "rgba(0,0,0,.06)", "fx-sh": "6px 0 6px -6px rgba(0,0,0,.14)",
+      "th-bg": "#F2F7FE", "th-t": "#5A6B84",
+      "row-bd": "#EDF2F8", "row-hover": "#F5F9FF", "row-sel": "#E6F1FF", "row-stripe": "#FFFFFF", "td-t": "#15233B",
+      succ: "#17B26A", "succ-t": "#0B8A4D", "succ-bg": "#E3F8EC", "succ-bd": "transparent",
+      warn: "#F79009", "warn-t": "#B95E00", "warn-bg": "#FFF3DF", "warn-bd": "transparent",
+      dang: "#F04461", "dang-t": "#CB2634", "dang-bg": "#FFE9EC", "dang-bd": "transparent",
+      info: "#8492A6", "info-t": "#44546C", "info-bg": "#EDF2F8", "info-bd": "transparent",
+      "prim-bg": "#E3F0FF", "prim-t": "#1668DC", "prim-bd": "transparent",
+      "pg-bg": "transparent", "pg-bd": "transparent", "pg-on-bg": "var(--grad)", "pg-on-t": "#FFFFFF", "pg-on-bd": "transparent",
+      // 侧栏:分项选型框架 E 的浅色侧栏(白底 78% 透出底色渐变、深灰字、选中项淡蓝渐变块+蓝字、右侧 1px 淡蓝线、标志晴空蓝渐变)
+      "sd-bg": "rgba(255,255,255,.78)", "sd-sub": "transparent", "sd-t": "#44546C", "sd-t-dim": "#8492A6", "sd-hi": "var(--t1)",
+      "sd-act-bg": "linear-gradient(90deg, #DCEBFF, #EDF5FF)", "sd-act-t": "#1668DC", "sd-hover": "rgba(60,140,255,.07)", "sd-bd": "1px solid #E4ECF5", "sd-fly": "var(--bg-pop)", "sd-logo-bg": "var(--grad)", "sd-logo-t": "#FFFFFF",
+    },
+  };
+
+  /* ---------------- 深色 B 石墨灰:不带色相的暗灰,扁平、靠描边分区、不用投影和渐变;状态标签只描边不铺底 ---------------- */
+  const db = {
+    key: "db", letter: "B", name: "石墨灰", label: "深色 B", dark: true, title: "深色皮肤 B 石墨灰(2026-10-08 选定)",
+    tagline: "已选定的深色主题:中性暗灰,不带蓝调;扁平,卡片靠 1px 描边分区,没有渐变、光晕和投影;主按钮是一块实心蓝,状态标签只描边不铺底,整张表最安静。",
+    points: ["底色 #141518、卡片 #1C1E23,中性灰,不偏蓝;没有渐变和光晕,顶栏、卡片靠 1px 描边分区。", "主色 #4A8CF7 实心(主按钮、勾选、分页当前页都不用渐变);文字链接 #8DBBFF。", "侧栏近黑 #111215,选中项是一块深蓝底+亮蓝字。", "状态标签和提示条改成「描边+彩色字」,不铺底色;买入红 #F47781、卖出绿 #4CC38A。"],
+    cmp: { 底色: "中性暗灰,纯色", 卡片: "比底色亮一级,1px 灰描边,无投影", 侧栏: "近黑,选中项深蓝块+亮蓝字", 主按钮: "实心蓝,无投影", 次按钮与输入框: "深灰底、灰描边,悬停描边变亮", 表头与行: "灰表头,灰行线,悬停略亮", 状态标签: "只描边+彩色字,不铺底", 投影与描边: "全靠描边,弹层才有投影", 装饰: "无" },
+    notes: { buttons: "32px;主按钮实心蓝、无投影,次按钮深灰底描边", status: "颜色按字典 tag_type 取;只描边+彩色字,不铺底色", dialog: "比底色亮一级、1px 描边,弹层才有投影;圆角 16px", kv: "带边框的描述表,标签格比内容格暗一级" },
+    tokens: {
+      ...SIZE,
+      "c-primary": "#4A8CF7", "c-primary-hover": "#6DA3FF", "c-primary-active": "#3B78DD", "c-primary-bg": "#243247", "c-primary-border": "#3A5480", "c-link": "#8DBBFF",
+      grad: "#4A8CF7", "grad-h": "#6DA3FF",
+      "pri-bg": "#4A8CF7", "pri-bg-h": "#6DA3FF", "pri-bd": "transparent", "pri-bd-h": "transparent", "pri-t": "#FFFFFF",
+      "logo-bg": "#4A8CF7", "logo-t": "#FFFFFF",
+      buy: "#F47781", sell: "#4CC38A",
+      t1: "#E6E8EC", t2: "#B3B8C2", t3: "#848B98", t4: "#5C6370",
+      "bd-ctl": "#3B404A", bd: "#2E323A", "bd-light": "#292D34",
+      "fill-1": "#22252B", "fill-2": "#292D34", "fill-3": "#343941",
+      "bg-page": "#141518", "bg-card": "#1C1E23", "frame-outer": "#111215",
+      canvas: "#141518", "pa-bg": "transparent",
+      "bg-pop": "#23262C", "bg-ctl": "#141518", "bg-dlg": "#1C1E23", "td-bg": "var(--bg-card)",
+      "tip-bg": "#E6E8EC", "tip-t": "#141518", "hover-veil": "rgba(255,255,255,.05)", veil: "rgba(20,21,24,.72)",
+      "card-bd": "1px solid #2E323A", "card-sh": NOSH, "card-hd-bg": "transparent", "card-hd-bd": "#2E323A",
+      "sh-pop": "0 8px 24px rgba(0,0,0,.50)", "sh-dlg": "0 20px 60px rgba(0,0,0,.60)", "pop-bd": "1px solid #3B404A", mask: "rgba(0,0,0,.60)",
+      "card-bf": "none", "pop-bf": "none", "dlg-bf": "none", "sd-bf": "none",
+      "btn-bg": "#23262C", "btn-bd": "#3B404A", "btn-t": "#E6E8EC", "btn-bg-h": "#2A2E35", "btn-bd-h": "#555C68", "btn-t-h": "#FFFFFF",
+      "btn-sh": NOSH, "btn-sh-h": NOSH, "btn-pri-sh": NOSH,
+      "ipt-bg": "#141518", "ipt-bd": "#3B404A", "ipt-sh": NOSH, "ipt-bg-h": "#141518", "ipt-bd-h": "#555C68",
+      focus: "0 0 0 2px rgba(74,140,247,.32)", "focus-bd": "#4A8CF7",
+      "ck-bd": "#555C68", "sw-off": "#3B404A", "sw-ring": "rgba(255,255,255,.10)", "fx-sh": "6px 0 8px -6px rgba(0,0,0,.6)",
+      "th-bg": "#202329", "th-t": "#A0A7B3",
+      "row-bd": "#2A2E35", "row-hover": "#22252B", "row-sel": "#243247", "row-stripe": "var(--bg-card)", "td-t": "#DADDE3",
+      succ: "#4CC38A", "succ-t": "#6FD6A3", "succ-bg": "transparent", "succ-bd": "rgba(76,195,138,.50)",
+      warn: "#E5A84B", "warn-t": "#F0BD6C", "warn-bg": "transparent", "warn-bd": "rgba(229,168,75,.50)",
+      dang: "#EC6A73", "dang-t": "#F58B93", "dang-bg": "transparent", "dang-bd": "rgba(236,106,115,.50)",
+      info: "#848B98", "info-t": "#B3B8C2", "info-bg": "transparent", "info-bd": "#3B404A",
+      "prim-bg": "transparent", "prim-t": "#8DBBFF", "prim-bd": "rgba(74,140,247,.55)",
+      "pg-bg": "transparent", "pg-bd": "transparent", "pg-on-bg": "#4A8CF7", "pg-on-t": "#FFFFFF", "pg-on-bd": "transparent",
+      "sd-bg": "#111215", "sd-sub": "#0C0D10", "sd-t": "#A7AEBA", "sd-t-dim": "#6B7280", "sd-hi": "#FFFFFF",
+      "sd-act-bg": "#243247", "sd-act-t": "#8DBBFF", "sd-hover": "rgba(255,255,255,.05)", "sd-bd": "0 solid transparent", "sd-fly": "var(--sd-bg)", "sd-logo-bg": "#4A8CF7", "sd-logo-t": "#FFFFFF",
+    },
+  };
+
+  const palette = [
+    ["主色与语义", [["主色", "c-primary"], ["主色悬停", "c-primary-hover"], ["主色按下", "c-primary-active"], ["主色浅底", "c-primary-bg"], ["主色描边", "c-primary-border"], ["链接文字", "c-link"], ["买入/上涨", "buy"], ["卖出/下跌", "sell"]]],
+    ["文字与线条", [["文字 1 标题正文", "t1"], ["文字 2 次要", "t2"], ["文字 3 辅助", "t3"], ["文字 4 占位禁用", "t4"], ["控件描边", "bd-ctl"], ["分割线", "bd"], ["浅分割线", "bd-light"], ["表格行线", "row-bd"]]],
+    ["底色", [["页面底色", "bg-page"], ["卡片", "bg-card"], ["填充 1", "fill-1"], ["填充 2", "fill-2"], ["填充 3", "fill-3"], ["表头底", "th-bg"], ["行悬停", "row-hover"], ["行选中", "row-sel"]]],
+    ["状态", [["成功", "succ"], ["成功文字", "succ-t"], ["成功浅底", "succ-bg"], ["警告", "warn"], ["警告文字", "warn-t"], ["警告浅底", "warn-bg"], ["危险", "dang"], ["危险文字", "dang-t"], ["危险浅底", "dang-bg"]]],
+  ];
+  // 规范摘要「换肤」里每套皮肤的小色条
+  const strip = [["底色", "bg-page"], ["卡片", "bg-card"], ["侧栏", "sd-bg"], ["主按钮", "pri-bg"], ["文字", "t1"], ["成功", "succ"], ["买入", "buy"], ["卖出", "sell"]];
+
+  const all = { f, db };
+  const order = ["f", "db"];
+  // 当前皮肤:对外仍是 window.TOKENS(key / name / tokens…),use(key) 切换时原地改,持有引用的模块不用动
+  const cur = { all, order, palette, strip, SYS, INTER, frame: "a", frameName: "经典三段" };
+  cur.use = function (key) {
+    const t = all[key];
+    if (!t || t.key === cur.key) return false;
+    Object.assign(cur, { key: t.key, letter: t.letter, name: t.name, label: t.label, dark: t.dark, title: t.title, tagline: t.tagline, points: t.points, cmp: t.cmp, notes: t.notes, tokens: t.tokens });
+    return true;
+  };
+  cur.use("f");
+  return cur;
+})();

+ 34 - 0
src/使用说明.txt

@@ -0,0 +1,34 @@
+三端 CMS · 完整 UI 设计稿 · 使用说明
+(2026-10-08,按第一批分项选型的评审结果绘制:基础视觉 F 晴空渐变、框架 A 经典三段、长表单弹窗 J 卡片分区、抽屉 E 卡片+描述表;短表单弹窗按发起人给的下单弹窗布局;登录页按 10 月 7 日追加的分项选型类别选定的 C 右侧浮卡·全幅底图)
+
+一、怎么打开
+  1. 先把压缩包整个解压出来(不要在压缩包里直接双击)。
+  2. 双击「CMS-UI完整设计.html」,用 Chrome 或 Edge 打开。不需要联网,不需要安装任何东西。
+  3. 屏幕宽一点看得更清楚;笔记本上可以点「全屏查看」,←/→ 切换页面,Esc 退出。
+
+二、看什么
+  左侧目录 16 项,分五组:
+    总览  规范摘要(选型结果、尺寸、颜色、12 类对应关系、待确认清单)、组件总览
+    全局  登录页(右侧浮卡 + 左侧三端入口)、整体框架·首页、列表页(搜索区 + 表格)
+    弹层  订单详情抽屉、补录弹窗、新建投资者弹窗、TRS 下单弹窗、票据详情抽屉
+    页面  页内标签页·风险监控、清算管理、个人中心、404 / 401
+    反馈  确认框(在页面里)、反馈类总览
+  原计划分三批挑选的 12 类,这次不再分批出方案,直接按已选的四项画成一套;
+  每一类在「规范摘要」里都标了对应页面。
+
+    上方「皮肤」切换浅色 F/深色 B,全部页面都能用两套皮肤看。
+
+三、怎么看
+  - 每个页面是 1440×900 的真实尺寸,缩放显示;画面可以直接点:菜单、标签页(含右键)、收起侧栏、
+    菜单搜索、通知、用户菜单、搜索展开、勾选表格行、点「详情」打开抽屉、在抽屉里点「补录」叠开弹窗、
+    切换页签、切换买卖方向和价格。登录页里可以点三端入口切到对应系统、换一张验证码、显示密码、记住我、忘记密码。
+  - 上方可以切换看发行/销售/TRS 哪一端,以及页面的变体(登录/重置密码、404/401、几种确认框)。
+  - 每个页面下方有「来源」「设计要点」「待确认」。待确认的问题汇总在「规范摘要」最后一节,请逐条回复。
+
+四、需要知道的
+  - 菜单、字段、列名取自三端真实代码;表里的数值是示例;首页的数字需要后端提供汇总接口。
+  - 控件 32px、表格行高 40px、正文 14px;三端完全一致;不放页面标题;只有单一左侧栏。
+  - 换肤:两套皮肤——浅色 F 晴空渐变(默认)、深色 B 石墨灰(10-08 从三版候选里选定)。
+    顶栏右侧「皮肤」可以切着看每个页面;登录页右上角的月亮/太阳按钮、用户菜单里的「界面设置」在画面里也能切。
+    「规范摘要 → 换肤」有两套的对照表。待确认:默认浅色还是跟随系统;切换后的选择记在账号还是本机。
+  - 「界面尺寸」切换已去掉(密度已统一),「界面设置」收进用户菜单。

+ 91 - 0
src/归档/tokens-深色候选AC.js

@@ -0,0 +1,91 @@
+/* 2026-10-08 三版深色候选里没选中的两版:A 夜空蓝、C 极光玻璃(用户选了 B 石墨灰)。
+ * 不参与构建。要恢复时把下面两段放回 tokens.js 的 B 前后,并在 all / order 里加回 da / dc,再 python3 pack.py。
+ * 这两段里引用的 SIZE / F_GRAD / NOSH / SYS / INTER 都在 tokens.js 里。
+ * 2026-10-08 侧栏令牌改成只读 sd-*(新增 sd-hi / sd-bd / sd-fly,去掉 sl-*),下面两段已按新结构同步。
+ */
+  /* ---------------- 深色 A 夜空蓝:F 的夜间版。带蓝调的深海军蓝,卡片比底色亮一级,右上角蓝色微光;主按钮沿用 F 的渐变 ---------------- */
+  const da = {
+    key: "da", letter: "A", name: "夜空蓝", label: "深色 A", dark: true, title: "深色皮肤 A 夜空蓝",
+    tagline: "F 的夜间版:内容区换成带蓝调的深海军蓝,侧栏仍是 F 的蓝色渐变(调深一档),深浅来回切时品牌感不变;主按钮和勾选沿用 F 的晴空蓝渐变。",
+    points: ["底色 #0E1628、卡片 #151F35,都带蓝调,不是纯黑;右上角很淡的蓝色光晕,和 F 的位置一样。", "主色提亮到 #4D9BFF,文字链接 #8CC0FF;主按钮仍是 F 的渐变,下面是蓝色辉光。", "侧栏保留 F 的蓝色渐变、只调深一档(#1B3F8F → #2457B8),白字,选中项是白色半透明块;顶栏和卡片同色。", "状态标签是深色彩底+亮色字;买入红、卖出绿提亮一档(#FF6F7A/#35D07F)。"],
+    cmp: { 底色: "深海军蓝+右上蓝色微光", 卡片: "比底色亮一级,1px 极淡描边,深投影", 侧栏: "F 的蓝色渐变调深一档,白字,选中项白色半透明块", 主按钮: "F 的蓝色渐变+蓝色辉光", 次按钮与输入框: "深蓝底、蓝灰描边,悬停描边变亮", 表头与行: "深蓝表头,行线更暗,悬停略亮", 状态标签: "深色彩底、亮色字、无描边", 投影与描边: "投影为主,加一道极淡描边", 装饰: "蓝色微光、渐变" },
+    notes: { buttons: "32px;主按钮沿用 F 的渐变,下面是蓝色辉光;次按钮深蓝底描边", status: "颜色按字典 tag_type 取;深色彩底+亮色字", dialog: "比底色亮一级,深投影;圆角 16px", kv: "带边框的描述表,标签格比内容格暗一级" },
+    tokens: {
+      ...SIZE,
+      "c-primary": "#4D9BFF", "c-primary-hover": "#74B3FF", "c-primary-active": "#3A86EC", "c-primary-bg": "#172A4D", "c-primary-border": "#2C4C84", "c-link": "#8CC0FF",
+      grad: F_GRAD, "grad-h": "linear-gradient(135deg, #74B8FF 0%, #4A92FA 100%)",
+      "pri-bg": "var(--grad)", "pri-bg-h": "var(--grad-h)", "pri-bd": "transparent", "pri-bd-h": "transparent", "pri-t": "#FFFFFF",
+      "logo-bg": "var(--grad)", "logo-t": "#FFFFFF",
+      buy: "#FF6F7A", sell: "#35D07F",
+      t1: "#E8EEF9", t2: "#B4C0D6", t3: "#7F8FA9", t4: "#55627A",
+      "bd-ctl": "#2F3D58", bd: "#25314A", "bd-light": "#1F2A40",
+      "fill-1": "#1A2439", "fill-2": "#202D45", "fill-3": "#2A3A55",
+      "bg-page": "#0E1628", "bg-card": "#151F35", "frame-outer": "#0B1222",
+      canvas: "radial-gradient(900px 460px at 88% -8%, rgba(77,155,255,.20), transparent 62%), radial-gradient(760px 420px at 4% 0%, rgba(120,110,255,.10), transparent 60%), linear-gradient(180deg, #0C1426 0%, #0E1628 40%, #0F172A 100%)",
+      "pa-bg": "transparent",
+      "bg-pop": "#1B2740", "bg-ctl": "#0F1829", "bg-dlg": "#151F35", "td-bg": "var(--bg-card)",
+      "tip-bg": "#E8EEF9", "tip-t": "#0E1628", "hover-veil": "rgba(255,255,255,.05)", veil: "rgba(14,22,40,.72)",
+      "card-bd": "1px solid rgba(140,170,230,.10)", "card-sh": "0 1px 2px rgba(0,0,0,.20), 0 8px 24px rgba(0,0,0,.28)", "card-hd-bg": "transparent", "card-hd-bd": "#223050",
+      "sh-pop": "0 0 0 1px rgba(140,170,230,.12), 0 12px 32px rgba(0,0,0,.55)", "sh-dlg": "0 0 0 1px rgba(140,170,230,.10), 0 28px 72px rgba(0,0,0,.60)", "pop-bd": "0 solid transparent", mask: "rgba(3,8,20,.66)",
+      "card-bf": "none", "pop-bf": "none", "dlg-bf": "none", "sd-bf": "none",
+      "btn-bg": "#1B2740", "btn-bd": "#2F3D58", "btn-t": "#DCE4F2", "btn-bg-h": "#223050", "btn-bd-h": "#4D7BC4", "btn-t-h": "#8CC0FF",
+      "btn-sh": NOSH, "btn-sh-h": NOSH, "btn-pri-sh": "0 4px 14px rgba(47,127,245,.38)",
+      "ipt-bg": "#0F1829", "ipt-bd": "#2F3D58", "ipt-sh": NOSH, "ipt-bg-h": "#0F1829", "ipt-bd-h": "#4D7BC4",
+      focus: "0 0 0 3px rgba(77,155,255,.28)", "focus-bd": "#4D9BFF",
+      "ck-bd": "#41527A", "sw-off": "#3A4860", "sw-ring": "rgba(255,255,255,.10)", "fx-sh": "6px 0 8px -6px rgba(0,0,0,.6)",
+      "th-bg": "#182238", "th-t": "#93A3BF",
+      "row-bd": "#1F2A40", "row-hover": "#1A2541", "row-sel": "#1B2E58", "row-stripe": "var(--bg-card)", "td-t": "#DDE5F2",
+      succ: "#34C98A", "succ-t": "#5EDBA6", "succ-bg": "#123327", "succ-bd": "transparent",
+      warn: "#F5A742", "warn-t": "#FFC470", "warn-bg": "#3A2B14", "warn-bd": "transparent",
+      dang: "#F2636B", "dang-t": "#FF8E95", "dang-bg": "#3B1C24", "dang-bd": "transparent",
+      info: "#7F8FA9", "info-t": "#B4C0D6", "info-bg": "#223050", "info-bd": "transparent",
+      "prim-bg": "#172A4D", "prim-t": "#8CC0FF", "prim-bd": "transparent",
+      "pg-bg": "transparent", "pg-bd": "transparent", "pg-on-bg": "var(--grad)", "pg-on-t": "#FFFFFF", "pg-on-bd": "transparent",
+      "sd-bg": "linear-gradient(172deg, #1B3F8F 0%, #2457B8 100%)", "sd-sub": "rgba(8,28,90,.22)", "sd-t": "rgba(255,255,255,.86)", "sd-t-dim": "rgba(255,255,255,.56)",
+      "sd-act-bg": "rgba(255,255,255,.18)", "sd-act-t": "#FFFFFF", "sd-hover": "rgba(255,255,255,.10)", "sd-hi": "#FFFFFF", "sd-bd": "0 solid transparent", "sd-fly": "var(--sd-bg)", "sd-logo-bg": "#FFFFFF", "sd-logo-t": "#1B3F8F",
+    },
+  };
+
+  /* ---------------- 深色 C 极光玻璃:深蓝到靛紫的渐变底叠极光光晕,卡片、侧栏、弹窗是半透明毛玻璃;主按钮蓝→靛渐变带辉光 ---------------- */
+  const dc = {
+    key: "dc", letter: "C", name: "极光玻璃", label: "深色 C", dark: true, title: "深色皮肤 C 极光玻璃",
+    tagline: "最有装饰感的一版:深蓝到靛紫的渐变底,右上蓝、左下紫两团极光;卡片、侧栏、顶栏、弹窗都是半透明毛玻璃加一圈亮边;主按钮蓝→靛渐变带辉光。",
+    points: ["底色是深蓝→靛紫的渐变(#0E1333 → #111744),右上一团蓝光、左下一团紫光;表格区仍是实色,保证固定列不透底。", "卡片、侧栏、顶栏、弹层都是半透明(约 60%)+背景模糊,极光透过来;边缘一圈 1px 亮边,分层靠「玻璃+亮边」而不是投影。", "主色 #5B8CFF,主按钮是蓝→靛紫渐变(#6EA8FF → #5B6EF5)带辉光;文字链接 #9DBBFF。", "状态标签是半透明彩底+亮色字;买入红 #FF6B7A、卖出绿 #3FD39A。"],
+    cmp: { 底色: "深蓝→靛紫渐变+蓝、紫两团极光", 卡片: "半透明毛玻璃,1px 亮边,柔投影", 侧栏: "半透明毛玻璃,选中项蓝→靛光带", 主按钮: "蓝→靛紫渐变+辉光", 次按钮与输入框: "半透明底、亮边描边,悬停亮边变蓝", 表头与行: "实色深蓝表头与行,悬停略亮", 状态标签: "半透明彩底、亮色字", 投影与描边: "玻璃+亮边为主,柔投影为辅", 装饰: "渐变、极光、毛玻璃、辉光" },
+    notes: { buttons: "32px;主按钮蓝→靛渐变带辉光,次按钮半透明底亮边", status: "颜色按字典 tag_type 取;半透明彩底+亮色字", dialog: "半透明毛玻璃加亮边;圆角 16px", kv: "带边框的描述表,标签格是半透明亮底" },
+    tokens: {
+      ...SIZE,
+      "c-primary": "#5B8CFF", "c-primary-hover": "#7FA6FF", "c-primary-active": "#4A77E8", "c-primary-bg": "rgba(91,140,255,.16)", "c-primary-border": "rgba(91,140,255,.42)", "c-link": "#9DBBFF",
+      grad: "linear-gradient(135deg, #6EA8FF 0%, #5B6EF5 100%)", "grad-h": "linear-gradient(135deg, #86B8FF 0%, #7284FF 100%)",
+      "pri-bg": "var(--grad)", "pri-bg-h": "var(--grad-h)", "pri-bd": "transparent", "pri-bd-h": "transparent", "pri-t": "#FFFFFF",
+      "logo-bg": "var(--grad)", "logo-t": "#FFFFFF",
+      buy: "#FF6B7A", sell: "#3FD39A",
+      t1: "#EEF2FF", t2: "#B9C3E0", t3: "#8490B4", t4: "#5A6486",
+      "bd-ctl": "rgba(160,180,240,.24)", bd: "rgba(160,180,240,.14)", "bd-light": "rgba(160,180,240,.09)",
+      "fill-1": "rgba(160,180,240,.07)", "fill-2": "rgba(160,180,240,.11)", "fill-3": "rgba(160,180,240,.17)",
+      "bg-page": "#0F1430", "bg-card": "rgba(26,32,78,.60)", "frame-outer": "#0D1230",
+      canvas: "radial-gradient(1000px 560px at 88% -12%, rgba(91,140,255,.44), transparent 60%), radial-gradient(820px 520px at 2% 106%, rgba(150,90,255,.38), transparent 60%), radial-gradient(560px 360px at 60% 116%, rgba(40,200,220,.18), transparent 60%), linear-gradient(160deg, #0D1136 0%, #13184A 50%, #0E1234 100%)",
+      "pa-bg": "transparent",
+      "bg-pop": "rgba(30,36,78,.92)", "bg-ctl": "rgba(8,12,38,.50)", "bg-dlg": "rgba(22,28,66,.90)", "td-bg": "#161C40",
+      "tip-bg": "#EEF2FF", "tip-t": "#0F1430", "hover-veil": "rgba(255,255,255,.06)", veil: "rgba(15,20,48,.70)",
+      "card-bd": "1px solid rgba(170,190,255,.14)", "card-sh": "inset 0 1px 0 rgba(255,255,255,.06), 0 8px 32px rgba(0,0,0,.35)", "card-hd-bg": "transparent", "card-hd-bd": "rgba(160,180,240,.12)",
+      "sh-pop": "0 0 0 1px rgba(170,190,255,.16), 0 16px 40px rgba(0,0,0,.55)", "sh-dlg": "0 0 0 1px rgba(170,190,255,.14), 0 32px 80px rgba(0,0,0,.60)", "pop-bd": "0 solid transparent", mask: "rgba(6,8,30,.60)",
+      "card-bf": "blur(18px)", "pop-bf": "blur(20px)", "dlg-bf": "blur(24px)", "sd-bf": "blur(20px)",
+      "btn-bg": "rgba(160,180,240,.08)", "btn-bd": "rgba(160,180,240,.24)", "btn-t": "#E4E9FF", "btn-bg-h": "rgba(160,180,240,.14)", "btn-bd-h": "rgba(130,160,255,.62)", "btn-t-h": "#FFFFFF",
+      "btn-sh": NOSH, "btn-sh-h": NOSH, "btn-pri-sh": "0 6px 18px rgba(91,110,245,.45)",
+      "ipt-bg": "rgba(8,12,38,.45)", "ipt-bd": "rgba(160,180,240,.24)", "ipt-sh": NOSH, "ipt-bg-h": "rgba(8,12,38,.45)", "ipt-bd-h": "rgba(130,160,255,.62)",
+      focus: "0 0 0 3px rgba(91,140,255,.30)", "focus-bd": "#5B8CFF",
+      "ck-bd": "rgba(160,180,240,.42)", "sw-off": "rgba(160,180,240,.28)", "sw-ring": "rgba(255,255,255,.12)", "fx-sh": "6px 0 8px -6px rgba(0,0,0,.6)",
+      "th-bg": "#1B2249", "th-t": "#9AA6CC",
+      "row-bd": "#232A54", "row-hover": "#1B2250", "row-sel": "#22306A", "row-stripe": "var(--td-bg)", "td-t": "#E4E9FF",
+      succ: "#3FD39A", "succ-t": "#74E3B8", "succ-bg": "rgba(63,211,154,.16)", "succ-bd": "transparent",
+      warn: "#FFB454", "warn-t": "#FFC97A", "warn-bg": "rgba(255,180,84,.16)", "warn-bd": "transparent",
+      dang: "#FF6B7A", "dang-t": "#FF98A3", "dang-bg": "rgba(255,107,122,.16)", "dang-bd": "transparent",
+      info: "#8490B4", "info-t": "#B9C3E0", "info-bg": "rgba(160,180,240,.12)", "info-bd": "transparent",
+      "prim-bg": "rgba(91,140,255,.18)", "prim-t": "#9DBBFF", "prim-bd": "transparent",
+      "pg-bg": "transparent", "pg-bd": "transparent", "pg-on-bg": "var(--grad)", "pg-on-t": "#FFFFFF", "pg-on-bd": "transparent",
+      "sd-bg": "linear-gradient(180deg, rgba(14,18,56,.60) 0%, rgba(12,16,50,.76) 100%)", "sd-sub": "rgba(0,0,0,.22)", "sd-t": "rgba(230,236,255,.78)", "sd-t-dim": "rgba(230,236,255,.42)",
+      "sd-act-bg": "linear-gradient(90deg, rgba(110,168,255,.42), rgba(91,110,245,.16))", "sd-act-t": "#FFFFFF", "sd-hover": "rgba(255,255,255,.06)", "sd-hi": "#FFFFFF", "sd-bd": "0 solid transparent", "sd-fly": "var(--sd-bg)", "sd-logo-bg": "var(--grad)", "sd-logo-t": "#FFFFFF",
+    },
+  };
+

Some files were not shown because too many files changed in this diff