refactor(ui): restyle dashboard as animal island

This commit is contained in:
chick
2026-07-07 22:32:20 +08:00
parent 143ab369c9
commit 182bd19c95
4 changed files with 100 additions and 91 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
## 功能 ## 功能
- 只需要在 `config.json` 配置多个 SimAdmin 地址。 - 只需要在 `config.json` 配置多个 SimAdmin 地址。
- 深色专业 Dashboard:实例舰队列表、状态筛选、搜索、当前设备摘要 - Animal Island / 动森风格 Dashboard:暖米色背景、棕色文字、Ribbon 飘带标题、NookPhone 应用格、胶囊按钮与波点卡片
- 实例舰队列表、状态筛选、搜索、当前设备摘要。
- 支持无密码实例状态读取;有密码实例可通过本地服务代登录并保持会话。 - 支持无密码实例状态读取;有密码实例可通过本地服务代登录并保持会话。
- 统一代理 SimAdmin API,内置 API 工作台,便于读取/调试设备、SIM、网络、短信、eSIM、OTA 等接口。 - 统一代理 SimAdmin API,内置 API 工作台,便于读取/调试设备、SIM、网络、短信、eSIM、OTA 等接口。
- 保留原始 SimAdmin 页面 iframe 嵌入;若目标站禁止 iframe,可一键在原站打开。 - 保留原始 SimAdmin 页面 iframe 嵌入;若目标站禁止 iframe,可一键在原站打开。
+4 -4
View File
@@ -227,7 +227,7 @@ function renderEndpointList() {
} }
function switchView(view) { function switchView(view) {
$$('.rail-btn[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)) $$('.phone-app[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view))
$$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`)) $$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`))
localStorage.setItem('multi-simadmin-view', view) localStorage.setItem('multi-simadmin-view', view)
} }
@@ -347,10 +347,10 @@ function bindEvents() {
document.body.classList.toggle('compact') document.body.classList.toggle('compact')
localStorage.setItem('multi-simadmin-density', document.body.classList.contains('compact') ? 'compact' : 'normal') localStorage.setItem('multi-simadmin-density', document.body.classList.contains('compact') ? 'compact' : 'normal')
}) })
$$('.rail-btn[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view))) $$('.phone-app[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view)))
$$('#statusFilters .chip').forEach(btn => btn.addEventListener('click', () => { $$('#statusFilters .tab-leaf').forEach(btn => btn.addEventListener('click', () => {
activeFilter = btn.dataset.filter activeFilter = btn.dataset.filter
$$('#statusFilters .chip').forEach(x => x.classList.toggle('active', x === btn)) $$('#statusFilters .tab-leaf').forEach(x => x.classList.toggle('active', x === btn))
renderInstances() renderInstances()
})) }))
$('loginForm').addEventListener('submit', async (event) => { $('loginForm').addEventListener('submit', async (event) => {
+78 -76
View File
@@ -3,106 +3,107 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Multi SimAdmin Control</title> <title>Multi SimAdmin Island</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<div id="appShell"> <div class="island-bg" aria-hidden="true">
<aside class="rail" aria-label="主导航"> <span class="cloud cloud-a"></span>
<div class="brand-mark"><span>MS</span></div> <span class="cloud cloud-b"></span>
<nav class="rail-nav"> <span class="leaf leaf-a"></span>
<button class="rail-btn active" data-view="dashboard" title="控制台"></button> <span class="leaf leaf-b"></span>
<button class="rail-btn" data-view="api" title="API 工作台">{}</button>
<button class="rail-btn" data-view="rawpage" title="原页面"></button>
</nav>
<button id="densityBtn" class="rail-btn bottom" title="切换紧凑密度"></button>
</aside>
<aside class="sidebar">
<header class="sidebar-head">
<div>
<p class="eyebrow">SIM FLEET</p>
<h1>Multi SimAdmin</h1>
</div> </div>
<button id="refreshBtn" class="icon-action" type="button" title="刷新全部"></button>
<div id="appShell" class="island-shell">
<aside class="nook-sidebar" aria-label="设备岛屿列表">
<header class="island-card sidebar-card pattern-default">
<div class="ribbon compact-ribbon" aria-label="Multi SimAdmin">
<span class="ribbon-back ribbon-back-left"></span>
<span class="ribbon-back ribbon-back-right"></span>
<span class="ribbon-fold ribbon-fold-left"></span>
<span class="ribbon-fold ribbon-fold-right"></span>
<span class="ribbon-front"></span>
<span class="ribbon-text">SimAdmin 岛屿</span>
</div>
<p class="sidebar-subtitle">像 NookPhone 一样管理每个 CPE 小岛</p>
<button id="refreshBtn" class="animal-btn primary-btn" type="button">刷新状态</button>
</header> </header>
<div class="fleet-stats" id="fleetStats"></div>
<div class="search-wrap"> <section class="wallet-row" id="fleetStats"></section>
<label class="search-pill" aria-label="搜索设备">
<span></span> <span></span>
<input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址…" autocomplete="off" /> <input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址…" autocomplete="off" />
</div> </label>
<div class="filter-row" id="statusFilters">
<button class="chip active" data-filter="all">全部</button> <nav class="island-tabs" id="statusFilters" aria-label="状态筛选">
<button class="chip" data-filter="online">在线</button> <button class="tab-leaf active" data-filter="all">全部</button>
<button class="chip" data-filter="auth">需登录</button> <button class="tab-leaf" data-filter="online">在线</button>
<button class="chip" data-filter="offline">离线</button> <button class="tab-leaf" data-filter="auth">需登录</button>
</div> <button class="tab-leaf" data-filter="offline">离线</button>
<div id="instances" class="instance-list"></div> </nav>
<footer class="sidebar-foot">
<span>配置</span> <div id="instances" class="island-list"></div>
<footer class="config-note">
<span>本地配置</span>
<code id="configPath">config.json</code> <code id="configPath">config.json</code>
</footer> </footer>
</aside> </aside>
<main class="workspace"> <main class="nook-workspace">
<header class="workspace-head"> <header class="workspace-hero island-card pattern-app-teal">
<div class="title-block"> <div>
<p id="activeKicker" class="eyebrow">NO DEVICE SELECTED</p> <p id="activeKicker" class="eyebrow">NO ISLAND SELECTED</p>
<h2 id="activeName">选择一个 SimAdmin 实例</h2> <h1 id="activeName">选择一个 SimAdmin 小岛</h1>
<p id="activeMeta">统一查看设备、SIM、网络、短信、eSIM、OTA 与原始管理页面。</p> <p id="activeMeta">查看设备、SIM、网络、短信、eSIM、OTA 与原始管理页面。</p>
</div> </div>
<div class="head-actions"> <div class="hero-actions">
<button id="loginBtn" type="button" class="ghost-btn" disabled>登录/刷新会话</button> <button id="loginBtn" type="button" class="animal-btn default-btn" disabled>登录/刷新会话</button>
<a id="openExternal" href="#" target="_blank" rel="noreferrer" class="ghost-btn disabled">原站打开</a> <a id="openExternal" href="#" target="_blank" rel="noreferrer" class="animal-btn default-btn disabled">原站打开</a>
</div> </div>
</header> </header>
<section id="dashboardView" class="view active"> <nav class="phone-dock" aria-label="主视图">
<section id="heroPanel" class="hero-panel"> <button class="phone-app active app-teal" data-view="dashboard"><span>🏝️</span><b>总览</b></button>
<div class="hero-copy"> <button class="phone-app app-yellow" data-view="api"><span>{}</span><b>接口</b></button>
<p class="eyebrow accent">CONTROL SURFACE</p> <button class="phone-app app-blue" data-view="rawpage"><span></span><b>原页</b></button>
<h3>统一运维所有 SimAdmin 节点</h3> <button id="densityBtn" class="phone-app app-green" type="button"><span></span><b>密度</b></button>
<p>无密码实例直接读取;有密码实例由本地服务保存会话。所有敏感凭据只留在本地配置。</p> </nav>
</div>
<div id="signalPanel" class="signal-panel"></div> <section id="dashboardView" class="view active dashboard-view">
<section class="signal-board island-card pattern-app-yellow">
<div class="section-ribbon color-app-yellow">小岛告示牌</div>
<div id="signalPanel" class="signal-grid"></div>
</section> </section>
<section id="overview" class="metric-grid"></section> <section id="overview" class="nook-metrics"></section>
<section class="section-block"> <section class="module-board island-card pattern-default">
<div class="section-title"> <div class="board-head">
<div> <div class="section-ribbon color-app-green">功能应用</div>
<p class="eyebrow">MODULES</p>
<h3>集成功能模块</h3>
</div>
<div id="moduleTabs" class="module-tabs"></div> <div id="moduleTabs" class="module-tabs"></div>
</div> </div>
<div id="featureGrid" class="feature-grid"></div> <div id="featureGrid" class="app-grid"></div>
</section> </section>
</section> </section>
<section id="apiView" class="view api-layout"> <section id="apiView" class="view api-layout">
<aside class="api-catalog"> <aside class="api-catalog island-card pattern-purple">
<div class="section-title small"> <div class="section-ribbon color-purple">API 地图</div>
<div>
<p class="eyebrow">API MAP</p>
<h3>接口目录</h3>
</div>
</div>
<div id="endpointList" class="endpoint-list"></div> <div id="endpointList" class="endpoint-list"></div>
</aside> </aside>
<section class="api-console"> <section class="api-console island-card pattern-default">
<div class="console-toolbar"> <div class="console-toolbar">
<select id="methodSelect"> <select id="methodSelect" aria-label="请求方法">
<option>GET</option> <option>GET</option>
<option>POST</option> <option>POST</option>
<option>DELETE</option> <option>DELETE</option>
</select> </select>
<input id="endpointInput" value="/api/device" spellcheck="false" /> <input id="endpointInput" value="/api/device" spellcheck="false" aria-label="接口路径" />
<button id="runEndpoint" class="primary-btn" type="button">发送</button> <button id="runEndpoint" class="animal-btn primary-btn" type="button">发送</button>
</div> </div>
<textarea id="requestBody" class="request-body" placeholder='POST/DELETE 请求体 JSON,例如 {"enabled":true}'></textarea> <textarea id="requestBody" class="request-body" placeholder='POST/DELETE 请求体 JSON,例如 {"enabled":true}'></textarea>
<pre id="rawOutput" class="raw-output">选择设备后可调用统一代理接口。</pre> <pre id="rawOutput" class="raw-output">选择设备后可调用统一代理接口。</pre>
@@ -110,12 +111,13 @@
</section> </section>
<section id="rawpageView" class="view rawpage-view"> <section id="rawpageView" class="view rawpage-view">
<div class="frame-toolbar"> <div class="frame-toolbar island-card pattern-app-blue">
<span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span> <span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span>
</div> </div>
<section id="emptyState" class="empty-state"> <section id="emptyState" class="empty-state island-card pattern-default">
<div class="section-ribbon color-app-pink">等待登岛</div>
<h3>还没有选择设备</h3> <h3>还没有选择设备</h3>
<p>在左侧选择一个实例后,这里会加载原 SimAdmin 页面。</p> <p>在左侧选择一个小岛后,这里会加载原 SimAdmin 页面。</p>
</section> </section>
<iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe> <iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe>
</section> </section>
@@ -123,14 +125,14 @@
</div> </div>
<dialog id="loginDialog"> <dialog id="loginDialog">
<form method="dialog" id="loginForm" class="login-card"> <form method="dialog" id="loginForm" class="login-card island-card pattern-default">
<p class="eyebrow accent">SESSION</p> <div class="section-ribbon color-app-teal">会话通行证</div>
<h3>登录 SimAdmin</h3> <h3>登录 SimAdmin</h3>
<p>输入该实例管理员密码;只发送到本地聚合服务,不写入浏览器存储。</p> <p>输入该实例管理员密码;只发送到本地聚合服务,不写入浏览器存储。</p>
<input id="passwordInput" type="password" placeholder="管理员密码" autocomplete="current-password" /> <input id="passwordInput" type="password" placeholder="管理员密码" autocomplete="current-password" />
<menu> <menu>
<button value="cancel" class="ghost-btn">取消</button> <button value="cancel" class="animal-btn default-btn">取消</button>
<button id="submitLogin" value="default" class="primary-btn">登录</button> <button id="submitLogin" value="default" class="animal-btn primary-btn">登录</button>
</menu> </menu>
</form> </form>
</dialog> </dialog>
+17 -11
View File
File diff suppressed because one or more lines are too long