Files
sub2api-cn-relay-manager/deploy/tksea-portal/admin/index.html
phamnazage-jpg 122d6282e1
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
fix(portal): unify all input/select/textarea/label/button/table styles via global fallback
Root cause: only admin/index.html had explicit .input / .select / .label classes.
100+ inputs across logical-groups / route-health / accounts / providers /
admin-batch-import + public portal had no class → browser default styling →
页面看起来「未统一」。

Fix:
- portal.css: add global rules that auto-apply design system styling to
  any input/select/textarea/label/button/table that doesn't opt out
  via .raw-input / .field-label. The existing .input / .select /
  .label / .btn classes still win (same styles, just explicit).
- portal.js: detectInitialTheme() now respects HTML's data-theme
  attribute first (page author intent), then localStorage, then OS
  preference. This makes admin pages' explicit data-theme="dark"
  actually stick instead of being overridden.
- admin/index.html: h3 标题 8 个 article 统一用 class="card-title"
  (前 4 个 inline 15px / 后 3 个 inline 16px 已统一)
- 6 admin pages: 修复 critical HTML 结构 bug — 之前 batch 处理的
  残留让 <link> 和 <style> 嵌套在 <style>:root{} 块内,浏览器
  解析时直接忽略,导致所有 stylesheet 不加载、整个页面无样式

Verification:
- bash scripts/test/test_tksea_portal_assets.sh → PASS
- bash scripts/test/verify_frontend_smoke.sh → PASS
- 8 张 screenshot v4 在 /tmp/portal-screenshots/ (各 600KB-1.2MB)
- 浏览器实测:3 stylesheets 加载,103 个 input 全部 38px/12px 圆角输入框
  35 个 label 全部 12px uppercase slate-400
  6 个 select 全部 38px + 自定义箭头
2026-06-03 11:05:10 +08:00

342 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>Admin Portal · Sub2API 中继管理</title>
<link rel="stylesheet" href="/portal/portal.css" />
<link rel="stylesheet" href="/portal/admin-common.css" />
</head>
<body>
<main class="shell fade-in">
<!-- 顶部导航(由 admin-common.js 渲染data-admin-nav 是契约) -->
<nav class="topnav" aria-label="Admin Navigation" data-admin-nav data-admin-current="home"></nav>
<!-- 页面 hero -->
<section class="page-hero">
<div>
<span class="page-hero__eyebrow">Admin Portal</span>
<h1>把新增模型、导入帐号与 Route 收进同一套入口</h1>
<p>
当前版本统一从 <code>/portal/admin/</code> 进入:一边看 pack/provider 目录、做 preview/import
一边继续保留 item 级 <code>reused / reactivated / replaced</code> 的 batch-import 结果面板。
所有写操作都走 CRM <code>/portal-admin-api</code>,浏览器不直连 Git。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/providers.html" data-cta="providers">
<span id="providers-cta-icon"></span> 进入供应商目录
</a>
<a class="btn" href="/portal/" target="_blank" rel="noreferrer">
<span id="portal-cta-icon"></span> 打开用户 Portal
</a>
<a class="btn btn-ghost" href="https://github.com" target="_blank" rel="noreferrer">
<span id="docs-cta-icon"></span> 文档
</a>
</div>
</div>
<!-- 右侧 metric 卡片组host DashboardView 风格) -->
<div class="stack">
<div class="stat-card">
<div class="stat-icon stat-icon-primary" id="m1-icon"></div>
<div class="min-w-0">
<p class="stat-label">统一入口</p>
<p class="stat-value">/portal/admin/</p>
<p class="stat-sub">默认同域走 <code>/portal-admin-api/</code></p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-icon-info" id="m2-icon"></div>
<div class="min-w-0">
<p class="stat-label">Logical Group</p>
<p class="stat-value">/logical-groups</p>
<p class="stat-sub">logical_group · public_model · route</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon stat-icon-success" id="m3-icon"></div>
<div class="min-w-0">
<p class="stat-label">Provider 目录</p>
<p class="stat-value">/providers</p>
<p class="stat-sub">preview-import · manifest 草稿</p>
</div>
</div>
</div>
</section>
<!-- 4 个核心模块入口(用 stat-card 一致风格) -->
<section class="section-head mt-6">
<div>
<h2>核心模块</h2>
<p>每个模块都对应一个 admin 页面 + 同一套 API base<code>/portal-admin-api</code>)。</p>
</div>
</section>
<div class="grid grid-2">
<!-- 逻辑分组 / 路由 -->
<article class="card card-hover">
<div class="card-body">
<div class="row-between">
<div class="row" style="gap:10px;">
<div class="stat-icon stat-icon-info" id="lg-icon"></div>
<div>
<h3 class="card-title">逻辑分组 / 路由</h3>
<p class="text-muted" style="margin:2px 0 0;font-size:12px;">logical_group · public_model · route · shadow_*</p>
</div>
</div>
<span class="pill pill-primary">运行中</span>
</div>
<p class="text-muted mt-3" style="font-size:13px;line-height:1.6;">
给插件前置路由使用,维护 <code>logical_group</code><code>public_model</code><code>route</code>
<code>shadow_host_id / shadow_group_id</code> 的关系。当前首版已经能直接调
<code>/api/logical-groups</code> 系列接口,适合先把 canonical shadow route 收进统一管理面。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/logical-groups.html">打开逻辑分组页</a>
<span class="text-subtle" style="font-size:12px;">首版页面只覆盖新增与查看</span>
</div>
</div>
</article>
<!-- 新增模型 / 供应商目录 -->
<article class="card card-hover">
<div class="card-body">
<div class="row-between">
<div class="row" style="gap:10px;">
<div class="stat-icon stat-icon-success" id="pr-icon"></div>
<div>
<h3 class="card-title">新增模型 / 供应商目录</h3>
<p class="text-muted" style="margin:2px 0 0;font-size:12px;">pack · provider · preview · manifest draft</p>
</div>
</div>
<span class="pill pill-success">推荐入口</span>
</div>
<p class="text-muted mt-3" style="font-size:13px;line-height:1.6;">
这页负责浏览已安装 pack、选择 provider、调用 <code>preview-import</code> / <code>import</code>
同时提供 provider manifest 草稿生成与发布。当前版本已经支持先保存草稿,再经由 CRM 服务端写入
pack/provider 文件并自动提交到仓库。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/providers.html">打开供应商页</a>
<a class="btn btn-ghost" href="/portal/admin/providers.html#manifest-draft">跳到 manifest 草稿</a>
</div>
</div>
</article>
<!-- Route 健康视图 -->
<article class="card card-hover">
<div class="card-body">
<div class="row-between">
<div class="row" style="gap:10px;">
<div class="stat-icon stat-icon-warning" id="rh-icon"></div>
<div>
<h3 class="card-title">Route 健康视图</h3>
<p class="text-muted" style="margin:2px 0 0;font-size:12px;">healthy · cooldown · failing · disabled</p>
</div>
</div>
<span class="pill pill-warning">只读</span>
</div>
<p class="text-muted mt-3" style="font-size:13px;line-height:1.6;">
这页专门给运营看 route 当前运行状态,聚合 <code>routefail</code><code>routecool</code>
最近一次选路与 failover 事件。首版只做只读健康视图,不在这里直接改 route。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/route-health.html">打开健康页</a>
<a class="btn btn-ghost" href="/portal/admin/route-health.html#matrix">看 matrix</a>
</div>
</div>
</article>
<!-- 帐号资产 -->
<article class="card card-hover">
<div class="card-body">
<div class="row-between">
<div class="row" style="gap:10px;">
<div class="stat-icon stat-icon-primary" id="ac-icon"></div>
<div>
<h3 class="card-title">帐号资产</h3>
<p class="text-muted" style="margin:2px 0 0;font-size:12px;">logical_group · route · shadow_group · shadow_host</p>
</div>
</div>
<span class="pill">库存</span>
</div>
<p class="text-muted mt-3" style="font-size:13px;line-height:1.6;">
把导入结果收成插件侧 <code>provider_accounts</code> 库存,直接展示帐号属于哪个
<code>logical_group / route / shadow_group / shadow_host</code>,并提供人工
<code>enable / disable / retire</code> 动作。启停动作当前只修改插件库存状态,不直接改宿主 account 记录。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/accounts.html">打开帐号资产页</a>
<a class="btn btn-ghost" href="/portal/admin/accounts.html#binding">显式整理归属</a>
</div>
</div>
</article>
</div>
<!-- 批量导入 + 旧地址兼容 -->
<article class="card mt-6">
<div class="card-body">
<div class="row-between">
<div class="row" style="gap:10px;">
<div class="stat-icon stat-icon-info" id="bi-icon"></div>
<div>
<h3 class="card-title">导入供应商帐号</h3>
<p class="text-muted" style="margin:2px 0 0;font-size:12px;">reused · created · reactivated · replaced</p>
</div>
</div>
<span class="pill pill-info">实时</span>
</div>
<p class="text-muted mt-3" style="font-size:13px;line-height:1.6;">
这页继续负责 live batch-import创建 run、拉取 run summary、查看 item 级别的
<code>matched_account_state</code><code>account_resolution</code>。批量导入第三方 key验证
<code>reused / created / reactivated / replaced</code> 状态语义。
</p>
<div class="row mt-4">
<a class="btn btn-primary" href="/portal/admin/batch-import.html">打开导入页</a>
<a class="btn btn-ghost" href="/portal/admin-batch-import.html">旧地址兼容入口</a>
</div>
</div>
</article>
<!-- 边界 + 状态 -->
<section class="section-head mt-6">
<div>
<h2>当前边界与安全前提</h2>
<p>明确告诉你"哪里能写、哪里只读、谁有权限",避免误操作。</p>
</div>
</section>
<div class="grid grid-3">
<article class="card">
<div class="card-body">
<div class="row" style="gap:10px;">
<span class="dot dot-success"></span>
<strong style="font-size:14px;">可立即使用</strong>
</div>
<h3 class="card-title">逻辑分组 + Provider 导入</h3>
<p class="text-muted" style="font-size:13px;line-height:1.6;">
依赖现有 <code>/api/logical-groups</code><code>/api/packs</code>
<code>/api/providers/*</code><code>/api/batch-import/*</code> 即可完成。
</p>
</div>
</article>
<article class="card">
<div class="card-body">
<div class="row" style="gap:10px;">
<span class="dot dot-info"></span>
<strong style="font-size:14px;">当前边界</strong>
</div>
<h3 class="card-title">浏览器提交到 CRM再由 CRM 写仓库</h3>
<p class="text-muted" style="font-size:13px;line-height:1.6;">
页面不会直接拼 Git 命令;所有写 pack/provider 与提交仓库的动作,都统一走 CRM 服务端的发布接口。
</p>
</div>
</article>
<article class="card">
<div class="card-body">
<div class="row" style="gap:10px;">
<span class="dot dot-warning"></span>
<strong style="font-size:14px;">安全前提</strong>
</div>
<h3 class="card-title">仍需 Admin Token</h3>
<p class="text-muted" style="font-size:13px;line-height:1.6;">
CRM 的 API 权限仍由 Bearer token 控制,同域反代只解决浏览器可达性,不降低鉴权门槛。
</p>
</div>
</article>
</div>
<!-- 管理员会话小工具条 -->
<section class="card mt-6">
<div class="card-body">
<div class="section-head" style="margin-bottom:12px;">
<div>
<h2 style="font-size:16px;">管理员会话</h2>
<p>可在此处建立 session 检查当前鉴权状态,或留作跨页签到。</p>
</div>
<div class="actions">
<button class="btn btn-sm" id="check-session-btn" type="button">检查会话</button>
</div>
</div>
<div class="grid grid-2">
<div class="field">
<label class="label" for="api-base-input">API Base</label>
<input id="api-base-input" class="input" type="text" placeholder="https://sub.tksea.top/portal-admin-api" />
<span class="field-help">同域走 <code>/portal-admin-api</code>,调试时可改成完整 URL。</span>
</div>
<div class="field">
<label class="label" for="admin-token-input">Bearer Token可选</label>
<input id="admin-token-input" class="input" type="password" placeholder="不落盘,仅当前会话" />
<span class="field-help">已登录管理员 session 时可不填。</span>
</div>
</div>
<div class="statusbar mt-4" id="admin-session-status" data-tone="info">
点击「检查会话」可拉取 <code>/api/admin/session</code>
</div>
</div>
</section>
</main>
<!-- 共享层admin-common.js保持兼容契约+ 新 portal.jstoast/icons/theme -->
<script src="/portal/admin-common.js"></script>
<script src="/portal/portal.js"></script>
<script>
(function initAdminHome() {
const runtime = window.Sub2ApiAdminCommon.createAdminPageRuntime({
apiBaseInput: document.getElementById("api-base-input"),
adminTokenInput: document.getElementById("admin-token-input"),
adminSessionStatus: document.getElementById("admin-session-status"),
});
window.Sub2ApiPortal.renderModernAdminNav(document.querySelector("[data-admin-nav]"), "home");
// 注入图标
const I = (id, name) => { const el = document.getElementById(id); if (el) el.innerHTML = window.Sub2ApiPortal.svg(name, 22); };
I("m1-icon", "shield");
I("m2-icon", "group");
I("m3-icon", "package");
I("lg-icon", "group");
I("pr-icon", "package");
I("rh-icon", "activity");
I("ac-icon", "account");
I("bi-icon", "import");
I("providers-cta-icon", "package");
I("portal-cta-icon", "external");
I("docs-cta-icon", "file");
// 会话检查
const stored = runtime.readStoredConfig("tksea-admin-home");
if (stored.apiBase) document.getElementById("api-base-input").value = stored.apiBase;
const checkBtn = document.getElementById("check-session-btn");
checkBtn.addEventListener("click", async () => {
checkBtn.disabled = true;
const orig = checkBtn.textContent;
checkBtn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;"></span> 检查中…';
try {
await runtime.refreshAdminSession();
window.Sub2ApiPortal.toast("会话状态已刷新", "success", 1800);
} catch (e) {
window.Sub2ApiPortal.toast(`检查失败:${e.message || e}`, "danger");
} finally {
checkBtn.disabled = false;
checkBtn.textContent = orig;
}
});
["api-base-input"].forEach((id) => {
const el = document.getElementById(id);
el.addEventListener("change", () => {
runtime.writeStoredConfig("tksea-admin-home", { apiBase: el.value });
});
});
})();
</script>
</body>
</html>