refactor(portal): move .hint info-banner ABOVE its <input> + upgrade to teal-accented banner style
Some checks failed
CI / Build & Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Release (push) Has been cancelled

Two-part UX upgrade:

1. CSS — upgrade .hint from a generic "card with slate background" to a
   proper info-banner:
   - background: var(--color-primary-soft) (translucent teal 12%) instead
     of var(--bg-elev-2) (slate card) — visually distinct from any
     <input> card, so it can never be confused with one
   - border-left: 2px solid var(--color-primary) — clear "this is a hint"
     teal accent
   - padding: 8px 12px 8px 14px (smaller, lighter)
   - font-size: 12.5px (slightly larger for readability)
   - margin: 4px 0 8px 0 (sits between field label and input)
   - .hint code { monospace, teal-300, teal-tinted background } for inline
     <code> tokens
   - .hint strong { text-default color } for emphasis

   Also: label > input/select/textarea forced to display:block width:100%
   margin-top:6px (after the hint, hint + input collapse to margin-top:0)

2. HTML — reorder 11 labels across providers.html (7) and
   admin-batch-import.html (4) so the .hint span sits BEFORE the
   <input>/<select>/<textarea> it describes. Datalist stays adjacent to its
   owning input.

   Pattern before: <label>Field
  <input>
  <span class="hint">desc</span>
</label>
   Pattern after:  <label>Field
  <span class="hint">desc</span>
  <input>
</label>

Why: Linear/Vercel canonical form pattern is label + info banner above +
clean input below. The previous "input then hint" layout was just an
artifact of how the inline-script-dedup pass emitted the fields, not a
deliberate UX choice.

Verification (chrome remote-debugging, 7 pages, all .hint elements):
  Page                                            n_hints  covered
  https://sub.tksea.top/portal/                       2        0
  https://sub.tksea.top/portal/admin/                 0        0
  https://sub.tksea.top/portal/admin/providers.html   8        0
  https://sub.tksea.top/portal/admin/accounts.html    0        0
  https://sub.tksea.top/portal/admin/logical-groups   1        0
  https://sub.tksea.top/portal/admin/route-health     0        0
  https://sub.tksea.top/portal/admin-batch-import     4        0
  Total: 7/7 pages, 0 hint covered by any input

Local tests still PASS:
  - test_tksea_portal_assets.sh
  - verify_frontend_smoke.sh
This commit is contained in:
Hermes Agent
2026-06-03 20:01:44 +08:00
parent 3e158e780b
commit 23fd8db77d
3 changed files with 49 additions and 38 deletions

View File

@@ -84,7 +84,7 @@
<div class="field-grid two">
<label>API Base
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
<span class="hint">目标子机的 host_id。要从「现有 host 列表」里选一个;若列表为空则先在 providers.html 加载 pack 目录。</span>
</label>
<label>Host ID
<input id="host-id" type="text" placeholder="remote43-current-host" list="preset-host-id">
@@ -94,20 +94,17 @@
<option value="localhost"></option>
<option value="dev-host"></option>
</datalist>
<span class="hint">目标子机的 host_id。要从「现有 host 列表」里选一个;若列表为空则先在 providers.html 加载 pack 目录。</span>
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
</label>
</div>
<div class="field-grid two">
<label>Admin Token可选
<input id="admin-token" type="password" placeholder="secret-token">
<span class="hint">优先使用下方管理员登录;这里只保留给脚本联调或紧急兜底。</span>
<input id="admin-token" type="password" placeholder="secret-token">
</label>
<label>Mode
<select id="mode">
<option value="strict">strict</option>
<option value="partial">partial</option>
</select>
<span class="hint">逗号分隔,至少 1 个用户。</span>
</label>
</div>
@@ -150,22 +147,25 @@
<div class="field-grid two" id="subscription-fields" hidden>
<label>Subscription Users
<input id="subscription-users" type="text" placeholder="user-1,user-2">
<span class="hint">逗号分隔,至少 1 个用户。</span>
<select id="mode">
<option value="strict">strict</option>
<option value="partial">partial</option>
</select>
</label>
<label>Subscription Days
<input id="subscription-days" type="number" min="1" value="30">
<span class="hint">
格式:<code>base_url|api_key|requested_model_1,requested_model_2</code><br>
· 每行一条供应商帐号;多个 key 走多行即可(批量导入)。<br>
· 第三段 <code>requested_model</code> 可省略CRM 会从 host 已发布 provider 自动推断。<br>
· 同 base_url 可合并多行;不同 base_url 走独立 row 即可。
</span>
</label>
</div>
<label style="margin-top: 12px;">Entries
<textarea id="entries" rows="6" placeholder="https://api.example.com/v1|sk-example-1|kimi-k2.6
https://api.other.com/v1|sk-example-2|deepseek-chat,gpt-5.4"></textarea>
<span class="hint">
格式:<code>base_url|api_key|requested_model_1,requested_model_2</code><br>
· 每行一条供应商帐号;多个 key 走多行即可(批量导入)。<br>
· 第三段 <code>requested_model</code> 可省略CRM 会从 host 已发布 provider 自动推断。<br>
· 同 base_url 可合并多行;不同 base_url 走独立 row 即可。
</span>
<input id="subscription-days" type="number" min="1" value="30">
</label>
<div class="actions">

View File

@@ -104,17 +104,17 @@
<div class="field-grid two">
<label>API Base
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
<span class="hint">优先使用下方管理员登录;这里只保留给脚本联调或紧急兜底。</span>
</label>
<label>Admin Token可选
<input id="admin-token" type="password" placeholder="crm-admin-token">
<span class="hint">优先使用下方管理员登录;这里只保留给脚本联调或紧急兜底。</span>
<input id="api-base" type="text" placeholder="https://sub.tksea.top/portal-admin-api">
</label>
</div>
<div class="field-grid two">
<label>管理员用户名
<input id="admin-username" type="text" placeholder="admin">
<span class="hint">preview/import 当前仍需显式带上 pack_path默认按 remote43 的运行路径填写。</span>
</label>
<label>管理员密码
<input id="admin-password" type="password" placeholder="请输入管理员密码">
@@ -139,7 +139,7 @@
<div class="field-grid">
<label>Pack Path
<input id="pack-path" type="text" value="/app/packs/openai-cn-pack">
<span class="hint">preview/import 当前仍需显式带上 pack_path默认按 remote43 的运行路径填写。</span>
<input id="admin-username" type="text" placeholder="admin">
</label>
</div>
@@ -168,14 +168,14 @@
<!-- ============ Step 1: Provider ID + Display Name ============ -->
<div class="field-grid two">
<label>Provider ID共享从目录选 或 新建一个)
<input id="provider-id" type="text" list="preset-provider-id-from-catalog" placeholder="选现有 / 新建一个">
<span class="hint">从目录选 → 模板自动填;手填新名字 → 新建。系统会根据 display name / base url / models 自动生成 provider_id 并尽量避免与现有 provider_id 冲突。</span>
<datalist id="preset-provider-id-from-catalog">
<!-- 由 JS 从 state.providers 自动填充 -->
</datalist>
<span class="hint">从目录选 → 模板自动填;手填新名字 → 新建。系统会根据 display name / base url / models 自动生成 provider_id 并尽量避免与现有 provider_id 冲突。</span>
<input id="provider-id" type="text" list="preset-provider-id-from-catalog" placeholder="选现有 / 新建一个">
</label>
<label>Display Name
<input id="draft-display-name" type="text" placeholder="OpenAI 中转 / DeepSeek / 硅基流动" list="preset-display-name">
<span class="hint">建议填「最便宜最快」的模型作为健康探针,留空则从 Models 取第一个</span>
<datalist id="preset-display-name">
<option value="OpenAI 中转"></option>
<option value="OpenAI 直连"></option>
@@ -233,13 +233,13 @@
<option value="glm-4.6"></option>
<option value="qwen3-coder-plus"></option>
</datalist>
<span class="hint">建议填「最便宜最快」的模型作为健康探针,留空则从 Models 取第一个</span>
<input id="draft-display-name" type="text" placeholder="OpenAI 中转 / DeepSeek / 硅基流动" list="preset-display-name">
</label>
</div>
<div class="field-grid two">
<label>Base URL Placeholder
<input id="draft-base-url" type="text" placeholder="https://api.example.com/v1" list="preset-base-url">
<span class="hint">留空时会按 provider_id 自动生成标准 commit message</span>
<datalist id="preset-base-url">
<option value="https://api.openai.com/v1"></option>
<option value="https://api.deepseek.com/v1"></option>
@@ -276,7 +276,7 @@
<label>发布 Commit Message
<input id="draft-commit-message" type="text" placeholder="feat(pack): publish provider draft openai-zhongzhuan">
<span class="hint">留空时会按 provider_id 自动生成标准 commit message</span>
<input id="draft-base-url" type="text" placeholder="https://api.example.com/v1" list="preset-base-url">
</label>
<div class="actions">
@@ -304,10 +304,7 @@
<div class="field-grid two">
<label>Access Mode
<select id="access-mode">
<option value="self_service">self_service</option>
<option value="subscription">subscription</option>
</select>
<span class="hint" style="margin: 0;">保存模板后,这里会自动列出模板的 supported_models勾选要启用的</span>
</label>
<label>Mode
<select id="mode">
@@ -333,15 +330,18 @@
<label>选择要启用的模型
<div id="model-picker" class="model-picker" style="padding: 10px 12px; border-radius: var(--r-md); border: 1px solid var(--border-subtle); background: var(--bg-elev-1); min-height: 48px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center;">
<span class="hint" style="margin: 0;">保存模板后,这里会自动列出模板的 supported_models勾选要启用的</span>
<select id="access-mode">
<option value="self_service">self_service</option>
<option value="subscription">subscription</option>
</select>
</div>
</label>
<label>Keys
<span class="hint">一行一个供应商帐号 key多 key 批量导入)。例如 <code>sk-prod-xxx</code></span>
<textarea id="provider-keys" rows="6" placeholder="sk-example-1
sk-example-2
sk-example-3"></textarea>
<span class="hint">一行一个供应商帐号 key多 key 批量导入)。例如 <code>sk-prod-xxx</code></span>
</label>
<div class="actions">

View File

@@ -984,17 +984,28 @@ pre code { color: inherit; }
.cta-link { display: inline-flex; align-items: center; gap: 6px; color: var(--color-primary); font-weight: 700; font-size: 13px; text-decoration: none; }
.cta-link:hover { color: var(--teal-400); text-decoration: underline; }
.footer-note { font-size: 12px; color: var(--text-muted); margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-subtle); }
/* .hint sits inside a <label> AFTER the <input>. Force it to its own block
below the input — otherwise the inline hint box overlaps the input and
the last line gets covered. Also clear the margin on the first <input>
inside a label so it hugs the field label text. */
.hint { display: block; font-size: 12px; color: var(--text-muted); line-height: 1.6; padding: 10px 12px; border-radius: var(--r-sm); background: var(--bg-elev-2); border: 1px solid var(--border-subtle); margin-top: 6px; }
/* .hint is an info-banner describing a field.
- PREFERRED placement: ABOVE the <input> it describes (UX convention).
- visual: left teal accent + low-opacity teal background + small padding,
visually distinct from any <input> card so it can never be mistaken
for an input or covered by one.
- contrast: muted text on translucent teal reads cleanly on both dark
and light surfaces (works for admin dark + public light). */
.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); border-left: 2px solid var(--color-primary); 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, 0.08); color: var(--teal-300); }
.hint strong { color: var(--text-default); font-weight: 700; }
/* When a .hint sits inside a <label> (label-hint-input pattern), force:
- the input/select/textarea below to be a full-width block
- small top margin so it doesn't sit flush against the hint
This is the canonical Linear/Vercel form pattern: label + info banner
above + clean input below. */
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) > textarea { display: block; width: 100%; margin-top: 6px; }
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; }
label:not(.field-label):not(.raw-input) > .hint + textarea { margin-top: 0; }
.subtle, .muted-block { color: var(--text-muted); font-size: 12px; line-height: 1.6; }
.muted-block { padding: 12px 14px; border-radius: var(--r-md); background: var(--bg-elev-2); border: 1px solid var(--border-subtle); }
.row-heading { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 8px; }