docs(portal): record 2026-06-03 frontend visual upgrade + design system runbook
- EXECUTION_BOARD.md: new 2026-06-03 entry with full evidence trail
(asset tests, browser smoke, screenshot list, conclusion=已闭环)
- 2026-06-03-FRONTEND-DESIGN-SYSTEM-RUNBOOK.md (new, 10KB):
* file structure + design token quick reference
* standard page skeleton + component API (stat-card, statusbar,
Portal.icons, Portal.toast, Portal.copyText, Portal.theme,
Portal.renderModernAdminNav)
* test-contract string rules (70+ strings must remain in HTML)
* common pitfalls (duplicate <!doctype>, duplicate const AdminCommon,
stat-card ID drift, accidental script removal)
* submission workflow + screenshot evidence commands
2026-06-03 09:11:18 +08:00
|
|
|
|
# 前端设计系统 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.css`(shim)+ `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. 标准页面骨架
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
|
<!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">`
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
|
<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 图标
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
Portal.toast("保存成功", { tone: "success", duration: 3000 });
|
|
|
|
|
|
Portal.toast("拉取失败", { tone: "danger" });
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.5 Copy
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
Portal.copyText(value, { success: "已复制", failure: "复制失败" });
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.6 Theme
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
Portal.theme.set("dark" | "light" | "auto"); // 写入 localStorage + html[data-theme]
|
|
|
|
|
|
Portal.theme.get(); // 当前生效 theme
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.7 Modern Nav
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
|
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. 测试门禁
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# 启动本地 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**(颜色 / 间距 / 字号)。示例:
|
|
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
|
/* 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` 单独一个 commit,message `style(portal): <token/组件>`
|
|
|
|
|
|
- 页面改造:每个 page 一个 commit,message `refactor(portal/<page>): <change>`
|
|
|
|
|
|
- 文档同步:`docs/EXECUTION_BOARD.md` + 本 runbook 一个 docs commit,与代码 commit 分开
|
|
|
|
|
|
- 一次 push 前跑:`test_tksea_portal_assets` + `verify_frontend_smoke` + 8 张截图
|
2026-06-04 20:00:03 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 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 全部如此):
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
|
<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 量出来的硬证据
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// 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 `3e158e78`,2026-06-03)
|
|
|
|
|
|
|
|
|
|
|
|
只改 `portal.css`,影响所有 8 个页面:
|
|
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
|
.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。
|
|
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
|
.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.sh`(scp 到
|
|
|
|
|
|
`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 设计直接套用。
|