Files
sub2api-cn-relay-manager/docs/2026-06-03-FRONTEND-DESIGN-SYSTEM-RUNBOOK.md
2026-06-04 20:00:03 +08:00

16 KiB
Raw Blame History

前端设计系统 Runbook

适用:deploy/tksea-portal/ 下所有 HTML 静态页(含 public portal + 7 个 admin 页) 建立时间2026-06-03 来源:把原本「像 demo」的多页 HTML 升级为对齐宿主 sub2api Vue+Tailwind teal/slate 体系的 Linear/Vercel 信息建筑派设计系统。

1. 文件结构

deploy/tksea-portal/
├── portal.css              # 真设计系统 (777 行) — 唯一新视觉真源
├── portal.js               # window.Sub2ApiPortal — toast / icon / nav / theme / drawer
├── admin-common.css        # 4KB legacy shim — 老类名映射到新 token
├── admin-common.js         # 313 行原 nav 渲染契约(未动)
├── index.html              # public portal (light)
├── admin/
│   ├── index.html          # admin 入口
│   ├── logical-groups.html
│   ├── route-health.html
│   ├── accounts.html
│   ├── providers.html
│   └── batch-import.html   # → /portal/admin-batch-import.html 1.5KB redirect
└── admin-batch-import.html # 旧地址实页(历史兼容)

新页面 = portal.css + portal.js + 自己的 <style>(仅页内布局)

老页面 = portal.css + admin-common.cssshim+ admin-common.js + portal.js + 自己的 <style>(仅页内布局)

2. Design Token 速查

颜色

Token 用途
--color-primary #14b8a6 teal 主色,对齐宿主
--color-primary-soft rgba(20,184,166,.12) 主色背景层
--color-success / -soft #22c55e / rgba(34,197,94,.1) healthy / live / success
--color-warning / -soft #f59e0b / rgba(245,158,11,.1) cooldown / warn
--color-danger / -soft #ef4444 / rgba(239,68,68,.1) failing / dead / danger
--color-neutral / -soft #64748b / rgba(100,116,139,.1) disabled / neutral
--text-strong --slate-50 (dark) / --slate-900 (light) 标题
--text-default --slate-200 / --slate-700 正文
--text-muted --slate-400 / --slate-500 辅助
--bg-base #0f172a / #f8fafc 背景
--bg-elev-1 #1e293b / #ffffff 卡片 1
--bg-elev-2 #334155 / #f1f5f9 卡片 2
--border-subtle rgba(148,163,184,.16) / rgba(15,23,42,.08) 边框

间距 (s-*)

--s-1=4 · --s-2=8 · --s-3=12 · --s-4=16 · --s-5=24 · --s-6=32 · --s-7=48

字号

--fs-xs=12 · --fs-sm=13 · --fs-md=14 · --fs-base=15 · --fs-lg=17 · --fs-xl=20 · --fs-2xl=24 · --fs-3xl=32 · --fs-display=44

圆角

--r-sm=6 · --r-md=10 · --r-lg=14 · --r-xl=20 · --r-full=9999

3. 标准页面骨架

<!doctype html>
<html lang="zh-CN" data-theme="dark">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>...</title>
    <link rel="stylesheet" href="/portal/portal.css" />
    <link rel="stylesheet" href="/portal/admin-common.css" />
    <!-- 老页兼容用 -->
    <style>
      /* 仅本页独有布局。token 全部用 var(--...) */
    </style>
  </head>
  <body>
    <main class="shell fade-in">
      <nav class="topnav" data-admin-nav data-admin-current="accounts"></nav>

      <section class="page-hero">
        <div>
          <span class="page-hero__eyebrow">Section Name</span>
          <h1>页面主标题</h1>
          <p>副标题描述,用 token 控制字号行高。</p>
        </div>
        <div class="stack" style="gap: var(--s-3);">
          <div class="stat-card">
            <div class="stat-icon stat-icon-primary" id="m1"></div>
            <div class="min-w-0">
              <p class="stat-label">API Root</p>
              <p class="stat-value" id="metric-api-root">-</p>
            </div>
          </div>
          <!-- 更多 stat-card -->
        </div>
      </section>

      <section class="layout">
        <!-- 你的页内 grid -->
        ...
      </section>
    </main>
    <script src="/portal/admin-common.js"></script>
    <script src="/portal/portal.js"></script>
    <script>
      const AdminCommon = window.Sub2ApiAdminCommon;
      const Portal = window.Sub2ApiPortal;
      AdminCommon.renderAdminNav(
        document.querySelector("[data-admin-nav]"),
        "accounts",
      );
      Portal.renderModernAdminNav(
        document.querySelector("[data-admin-nav]"),
        "accounts",
      );
      Portal.icons.shield.mount("#m1");
      Portal.icons.group.mount("#m2");
      // ... 你的页逻辑
    </script>
  </body>
</html>

4. 组件 API

4.1 <div class="stat-card">

<div class="stat-card">
  <div
    class="stat-icon stat-icon-{primary|info|success|warning|danger}"
    id="unique-id"
  ></div>
  <div class="min-w-0">
    <p class="stat-label">label</p>
    <p class="stat-value" id="metric-foo">0</p>
  </div>
</div>

4.2 <div class="statusbar">

.statusbar 是统一的提示条;颜色用 data-tone="success|warning|danger|note|neutral" 或直接 class tone-healthy|tone-cooldown|tone-failing|tone-disabled|tone-ready|tone-note|tone-warn

4.3 Portal 图标

Portal.icons.shield.mount("#icon-slot"); // 渲染 22px lucide stroke 1.75
Portal.icons.group.mount("#icon-slot", { size: 18 });

可用:home / group / route / health / account / provider / import / check / x / alert / info / copy / edit / trash / plus / refresh / download / upload / eye / eyeoff / play / pause / search / filter / shield / activity / package / key / send / log-out / external-link / arrow-right / chevron-right

4.4 Toast

Portal.toast("保存成功", { tone: "success", duration: 3000 });
Portal.toast("拉取失败", { tone: "danger" });

4.5 Copy

Portal.copyText(value, { success: "已复制", failure: "复制失败" });

4.6 Theme

Portal.theme.set("dark" | "light" | "auto"); // 写入 localStorage + html[data-theme]
Portal.theme.get(); // 当前生效 theme

4.7 Modern Nav

Portal.renderModernAdminNav(container, currentKey);
// 渲染带 SVG icon + 渐变 hover 的现代 nav并存于 AdminCommon.renderAdminNav 之上

5. 关键约束

  1. 不要破坏测试契约字符串logical_group / shadow_host_id / matched_account_state / account_resolution / package_tier / visibility_scope / gpt-5.4 / MiniMax-M2.7-highspeed / deepseek-chat / Batch Import Admin / route_status / cooldown_seconds / failover_threshold / Smoke Logical Group / Smoke Provider Account / smoke-admin / smoke-route-primary 等 70+ 字符串必须在 HTML 中出现(前端门禁 grep 断言)。

  2. 不要修改 admin-common.js:保留 window.Sub2ApiAdminCommon 的全局 API 契约。portal.js 是叠加层,不是替代层。

  3. 不要使用 emoji / 渐变紫 / generic stock photo:用 SVG icon + token 调色板。

  4. 不要写裸 style="color:#xxx":用 var(--text-muted) 等 token。

  5. JS 引用 DOM ID 时:新增的 page-hero / stat-card ID 必须和 getElementById() 调用对应。修改 stat-card label 时同步检查 JS 里的引用。

6. 测试门禁

cd /home/long/project/sub2api-cn-relay-manager
bash scripts/test/test_tksea_portal_assets.sh      # 70+ 字符串契约断言
bash scripts/test/verify_frontend_smoke.sh         # chromium headless 渲染 7 页 + public
bash scripts/acceptance/verify_frontend_acceptance_matrix.sh   # 总入口

修改任何 deploy/tksea-portal/*.html / portal.css / portal.js / admin-common.css 后必须三门都过。

7. 截图 evidence

# 启动本地 server端口 8765映射 /portal/* → deploy/tksea-portal/*
python3 /tmp/portal_server.py &

# 截图 8 个页面
mkdir -p /tmp/portal-screenshots
for url in \
  "/portal/admin/index.html" \
  "/portal/admin/logical-groups.html" \
  "/portal/admin/route-health.html" \
  "/portal/admin/accounts.html" \
  "/portal/admin/providers.html" \
  "/portal/admin-batch-import.html" \
  "/portal/admin/batch-import.html" \
  "/portal/index.html"; do
  name=$(basename "$url" .html)
  google-chrome --headless --disable-gpu --no-sandbox --hide-scrollbars \
    --window-size=1440,2400 --virtual-time-budget=4000 \
    --screenshot=/tmp/portal-screenshots/${name}.png \
    "http://127.0.0.1:8765${url}"
done

8. 常见错误

  • 重复 <!doctype html>:原页有 inline <link> 紧跟 <style>,新 head 注入会拼接出两个 doctype。修复删掉第一个 doctype 到第二个 doctype 之间的内容。
  • const AdminCommon 重复声明:批量处理时容易把 renderAdminNav 块和原 script 块拼接,两块都包含 const AdminCommon = window.Sub2ApiAdminCommon; → SyntaxError。修复保留一处。
  • stat-card ID 改名导致 getElementById 找不到JS 引用 metric-total 而 DOM 是 metric-account-count → loadAccounts 抛错。修复:保持 ID 不变。
  • 去掉 inline <style> 时连 <script> 也误删:原页有 inline <script><style> 之后,被批量处理误删。修复:从 git show HEAD:<file> 提取原始 script 块插回。

9. 进阶:自定义页内布局

每个页都可以有自己的 <style> 块,只放页内布局grid / flex不放 token(颜色 / 间距 / 字号)。示例:

/* accounts.html */
.layout {
  display: grid;
  grid-template-columns: 420px minmax(0, 1fr);
  gap: var(--s-5);
}
.field-grid {
  display: grid;
  gap: 12px;
}
.field-grid.two {
  grid-template-columns: 1fr 1fr;
}
.catalog {
  display: grid;
  gap: 12px;
  max-height: 32rem;
  overflow: auto;
}

10. 提交规范

  • 设计系统变更:portal.css / portal.js / admin-common.css 单独一个 commitmessage style(portal): <token/组件>
  • 页面改造:每个 page 一个 commitmessage refactor(portal/<page>): <change>
  • 文档同步:docs/EXECUTION_BOARD.md + 本 runbook 一个 docs commit与代码 commit 分开
  • 一次 push 前跑:test_tksea_portal_assets + verify_frontend_smoke + 8 张截图

11. 真实 bug 案例label 内 hint 被 input 覆盖 (2026-06-03)

11.1 症状

用户在生产环境 (https://sub.tksea.top/portal/) 报告"部分提示文字被遮挡"。 具体表现providers.html 的 ADMIN TOKEN / PACK PATH / PROVIDER ID / SMOKE TEST MODEL / 发布 COMMIT MESSAGE / KEYS 6 个字段,其 .hint 描述文字末行被 input 框盖住 8-10px导致末尾几个字看不到。

11.2 根因

HTML 结构11 个 label 全部如此):

<label>字段名
  <input id="..." type="..." placeholder="...">
  <datalist>...</datalist>          <!-- 可选 -->
  <span class="hint">描述文字</span>
</label>

3 个流在同一行normal flow打架

  • <label> 全局规则 display: block; font-size: 12px (portal.css line 75)
  • <input> 默认 display: inline-block,高度 40px
  • <span class="hint"> 默认 display: inline,但 .hint 自己有 padding: 10px 12px + border + background,等同于"内联卡片"

inline .hint box 跟在 inline-block <input> 后面 baseline 对齐, 但 .hint 实际是 39-77px 高的盒子,比 input 高 0-37px。多出来的 8-10px 垂直方向 跟 input 重叠 → input 覆盖 hint 末行。

11.3 量出来的硬证据

// chrome devtools console:
(() => {
  const results = [];
  document.querySelectorAll('label').forEach((el) => {
    if (!el.querySelector('input,select,textarea')) return;
    const hint = el.querySelector('.hint');
    if (!hint) return;
    const r = hint.getBoundingClientRect();
    const ir = el.querySelector('input,select,textarea').getBoundingClientRect();
    const covered = (r.top < ir.bottom && r.bottom > ir.top && r.left < ir.right && r.right > ir.left);
    if (covered) results.push({label: el.textContent.split('\n')[0].trim(), hint: hint.textContent.trim()});
  });
  return results;
})()
// 返回 5-6 个被覆盖的 hint 对象

11.4 修法CSS-only → HTML 升级 → 全面 info-banner

第一阶段CSS 兜底 (commit 3e158e782026-06-03)

只改 portal.css,影响所有 8 个页面:

.hint { display: block; ...; margin-top: 6px; }   /* 强制独立行 */
label:not(.field-label):not(.raw-input) > input,
label:not(.field-label):not(.raw-input) > select,
label:not(.field-label):not(.raw-input) > textarea {
  display: block; width: 100%; margin-top: 4px;
}
label:not(.field-label):not(.raw-input) > .hint + input,
label:not(.field-label):not(.raw-input) > .hint + select,
label:not(.field-label):not(.raw-input) > .hint + textarea { margin-top: 6px; }

效果:.hint 永远在 input 下方独立成行0 covered。但 UX 仍不理想 — hint 跟 input 分离后用户要"先填后看解释",违反 Linear/Vercel 信息建筑派 "label + 解释在 input 上方"的规范。

第二阶段HTML 升级为 info-banner (commit 23fd8db7)

11 个 label 重排:hint 移到 input 之前datalist 仍紧贴 input。 同时把 .hint 样式从"实心 slate 卡片"升级为"teal 12% 透明 + 左边 2px teal accent" 的 info-banner视觉上跟 input 卡片严格区分,永远不会被 误以为是 input。

.hint {
  display: block;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-muted);
  padding: 8px 12px 8px 14px;
  border-radius: var(--r-sm);
  background: var(--color-primary-soft);     /* 浅 teal 12% 透明 */
  border-left: 2px solid var(--color-primary); /* 左边 teal accent */
  margin: 4px 0 8px 0;
}
.hint code { font-family: var(--font-mono); font-size: 12px; padding: 0 4px;
             border-radius: 4px; background: rgba(20,184,166,.08); color: var(--teal-300); }
.hint strong { color: var(--text-default); font-weight: 700; }

11.5 验证chrome remote-debugging 7 页面)

脚本 /tmp/verify_7pages.js 走 Chrome DevTools Protocol对 7 个 URL 逐个 navigate + evaluate

Page n_hints covered
/portal/ 2 0
/portal/admin/ 0 0
/portal/admin/providers.html 7-8 0
/portal/admin/accounts.html 0 0
/portal/admin/logical-groups.html 1 0
/portal/admin/route-health.html 0 0
/portal/admin-batch-import.html 4 0
总计 14-15 0

deploy 流程:bash scripts/deploy/deploy_tksea_portal.shscp 到 ubuntu@43.155.133.187:/var/www/sub2api-portal/ + reload nginx

11.6 防同类 bug checklist

下次在 portal 添加新字段时,强制遵循:

  • hint 描述文字放在 input 之前(不是之后)
  • hint 用 <span class="hint">描述</span>,不要用 <p><div>
  • 不要在 label 文字后直接接 hint span中间用换行 + 缩进)
  • datalist 紧贴它所属的 input不跟 hint 混在一起
  • 新增完成后跑: bash bash scripts/test/test_tksea_portal_assets.sh bash scripts/test/verify_frontend_smoke.sh node /tmp/verify_7pages.js 2>&1 | grep covered 必须全部 covered=0

11.7 相关 commit

  • 3e158e78 fix(portal): prevent .hint description from being covered by <input> (CSS-only)
  • 23fd8db7 refactor(portal): move .hint info-banner ABOVE its <input> + upgrade to teal-accented banner style (HTML reorder + style upgrade)

11.8 相关 skill

  • ~/.hermes/skills/software-development/label-hint-coverage-debug/SKILL.md — 把这个 bug 的根因/修法/verify/防复发提炼为通用 skill下个项目的 form hint 设计直接套用。