From 4f80e951d441dc31cd0242a9983ad3296c572622 Mon Sep 17 00:00:00 2001 From: chick Date: Wed, 8 Jul 2026 00:17:22 +0800 Subject: [PATCH] Refocus UI on device card details --- public/app.js | 203 +++++++++++++++++++++------------------ public/index.html | 174 ++++++++++++++++----------------- public/styles.css | 20 ++-- test/ui-contract.test.js | 23 +++-- 4 files changed, 224 insertions(+), 196 deletions(-) diff --git a/public/app.js b/public/app.js index e2341e3..6c00ae5 100644 --- a/public/app.js +++ b/public/app.js @@ -42,20 +42,6 @@ function instancePayloadFromForm() { auth: { password: $('instancePasswordInput').value }, } } -function setActiveHome() { - activeId = null - localStorage.removeItem('multi-simadmin-active') - $('loginBtn').disabled = true - $('openExternal').href = '#' - $('openExternal').classList.add('disabled') - $('emptyState').hidden = false - $('frame').hidden = true - $('frame').removeAttribute('src') - switchView('dashboard') - renderInstances() - renderOverview() - renderFeatures() -} function statusKind(s) { if (!s) return 'unknown' if (!s.reachable) return 'offline' @@ -191,11 +177,17 @@ function filteredInstances() { }) } -function renderInstances() { - const root = $('instances') +function deviceSummary(item) { + const s = statuses.get(item.id) + const sum = s?.summary || {} + return { item, s, sum, device: sum.device || {}, sim: sum.sim || {}, network: sum.network || {}, data: sum.data || {}, sms: sum.sms || {}, ota: sum.ota || {}, system: sum.system || {} } +} + +function renderHomeCards() { + const root = $('homeDeviceGrid') renderFleetStats() if (!instances.length) { - root.innerHTML = '

未配置设备

点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。

' + root.innerHTML = '

未配置设备

点击“添加设备”新增 SimAdmin 地址后,这里会出现设备卡片。

' return } const list = filteredInstances() @@ -204,52 +196,87 @@ function renderInstances() { return } root.innerHTML = list.map(item => { - const s = statuses.get(item.id) - const sum = s?.summary || {} - const network = sum.network || {} - const sim = sum.sim || {} - const system = sum.system || {} - return `
-
+ const { s, device, sim, network, data, sms, system, ota } = deviceSummary(item) + const dataState = firstValue(data.active, data.connected, data.enabled) + return `
+
-
${escapeHtml(item.name || item.id)}
-
${escapeHtml(item.url)}
+

${escapeHtml(item.id)}

+

${escapeHtml(item.name || item.id)}

+ ${escapeHtml(item.url)}
${escapeHtml(statusText(s))}
-
- 信号${escapeHtml(value(network.signal || sim.signal))} - 温度${escapeHtml(formatTemperature(system.temperature))} - 速率${escapeHtml(formatSpeed(system.networkSpeed))} - SIM${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))} +
${escapeHtml(value([device.manufacturer, device.model].filter(Boolean).join(' ') || device.imei || '未知设备'))}
+
+ SIM${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))} + 信号${escapeHtml(value(firstValue(network.signal, sim.signal)))} + 温度${escapeHtml(formatTemperature(system.temperature))} + 流量${escapeHtml(formatSpeed(system.networkSpeed))} + 数据${escapeHtml(dataState === true ? '已连接' : dataState === false ? '未连接' : '-')} + 短信${escapeHtml(sms.total != null ? `总 ${sms.total}` : '-')}
-
- - +
+ ${escapeHtml(value(network.operator || sim.operator || network.registration))} + ${escapeHtml(value(ota.currentVersion || device.firmware || device.revision))}
` }).join('') - $$('.instance-card', root).forEach(card => card.addEventListener('click', (event) => { - if (event.target.closest('button')) return - selectInstance(card.dataset.id) - })) - $$('[data-edit-id]', root).forEach(btn => btn.addEventListener('click', () => openInstanceDialog(btn.dataset.editId))) - $$('[data-delete-id]', root).forEach(btn => btn.addEventListener('click', () => deleteInstance(btn.dataset.deleteId))) + $$('[data-open-detail]', root).forEach(card => { + const open = () => openDeviceDetail(card.dataset.openDetail) + card.addEventListener('click', open) + card.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open() } + }) + }) +} + +function renderInstances() { renderHomeCards() } + +function openDeviceDetail(id) { + const item = instances.find(x => x.id === id) + if (!item) return + activeId = id + localStorage.setItem('multi-simadmin-active', id) + $('loginBtn').disabled = false + $('openExternal').href = item.url + $('openExternal').classList.remove('disabled') + $('emptyState').hidden = true + $('frame').hidden = false + $('frame').src = item.url + switchView('detail') + renderHomeCards() + renderOverview() + renderDeviceDetails() + renderDetailActions() +} + +function setActiveHome() { + activeId = null + localStorage.removeItem('multi-simadmin-active') + $('loginBtn').disabled = true + $('openExternal').href = '#' + $('openExternal').classList.add('disabled') + $('emptyState').hidden = false + $('frame').hidden = true + $('frame').removeAttribute('src') + switchView('home') + renderHomeCards() } function renderHero() { const { item, s, device, sim, network, system, data, ota } = getMonitorData() if (!item) { - $('activeKicker').textContent = 'DEVICE MONITOR' - $('activeName').textContent = '选择一个设备查看实时状态' - $('activeMeta').textContent = '主页面展示各设备的状态、温度、流量、SIM、信号、系统资源与短信统计。' + $('activeKicker').textContent = 'DEVICE DETAIL' + $('activeName').textContent = '选择一个 SimAdmin 设备' + $('activeMeta').textContent = '从首页卡片进入详情页后,可查看状态并使用功能交互。' $('signalPanel').innerHTML = [ ['状态', '等待选择'], ['设备', '-'], ['温度', '-'], ['流量速率', '-'], ].map(([k, v]) => `
${k}${v}
`).join('') return } $('activeKicker').textContent = `${item.id} · ${statusText(s)}` - $('activeName').textContent = `${item.name || item.id} 设备监控` + $('activeName').textContent = `${item.name || item.id} 详情页` $('activeMeta').textContent = `${device.manufacturer || ''} ${device.model || ''}`.trim() || item.url $('signalPanel').innerHTML = [ ['在线状态', statusText(s)], @@ -264,18 +291,8 @@ function renderHero() { } function renderOverview() { - const { item, s, device, sim, network, data, sms, system } = getMonitorData() - if (!item) { - $('overview').innerHTML = [ - metricCard('设备', `${instances.length} 台`), - metricCard('在线', [...statuses.values()].filter(x => statusKind(x) === 'online').length), - metricCard('温度', '选择设备后显示'), - metricCard('流量', '选择设备后显示'), - ].join('') - renderHero() - renderDeviceDetails() - return - } + const { item, s, sim, network, data, sms, system } = getMonitorData() + if (!item) return const dataState = firstValue(data.active, data.connected, data.enabled) $('overview').innerHTML = [ metricCard('状态', s ? (s.reachable ? (s.authenticated === false ? '在线/未登录' : '在线') : '离线') : '未知', pillClass(s)), @@ -288,16 +305,12 @@ function renderOverview() { metricCard('短信', sms.total != null ? `总 ${sms.total} / 收 ${value(sms.incoming)} / 发 ${value(sms.outgoing)}` : '-'), ].join('') renderHero() - renderDeviceDetails() } function renderDeviceDetails() { const { item, device, sim, network, data, sms, ota, system, s } = getMonitorData() - if (!item) { - $('featureGrid').innerHTML = `

设备监控大屏

选择左侧任一设备后,这里会按分类展示真实设备信息、温度、流量、系统资源、SIM 与短信状态。

` - return - } - $('featureGrid').innerHTML = [ + if (!item) return + $('detailDeviceGrid').innerHTML = [ detailCard('设备信息', [ ['名称', item.name || item.id], ['地址', item.url], ['型号', device.model], ['厂商', device.manufacturer], ['IMEI', device.imei], ['电源', boolText(device.powered)], ['在线', boolText(device.online)], ['固件/版本', ota.currentVersion || device.firmware || device.revision], ['Commit', ota.currentCommit], ], 'device'), @@ -316,7 +329,26 @@ function renderDeviceDetails() { ].join('') } -function renderFeatures() { renderDeviceDetails() } +function renderDetailActions() { + const disabled = activeId ? '' : ' disabled' + const primary = [ + ['设备状态', '/api/device'], ['SIM 信息', '/api/sim'], ['蜂窝网络', '/api/network'], ['系统资源', '/api/stats'], + ['短信统计', '/api/sms/stats'], ['通话记录', '/api/calls'], ['OTA 状态', '/api/ota/status'], ['数据连接', '/api/data'], + ] + $('detailActions').innerHTML = ` + + ${primary.map(([label, ep]) => ``).join('')} + ` + $$('[data-detail-ep]', $('detailActions')).forEach(btn => btn.addEventListener('click', () => { + switchView('api') + $('endpointInput').value = btn.dataset.detailEp + $('methodSelect').value = inferMethod(btn.dataset.detailEp) + runEndpoint() + })) + $$('[data-view]', $('detailActions')).forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view))) +} + +function renderFeatures() { renderDeviceDetails(); renderDetailActions() } function renderEndpointList() { const groups = catalog.length ? catalog : modules.map(m => ({ id: m.id, name: m.title, endpoints: m.endpoints })) @@ -332,26 +364,13 @@ function renderEndpointList() { } function switchView(view) { - $$('.phone-app[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)) + if (!['home', 'detail', 'rawpage', 'api'].includes(view)) view = activeId ? 'detail' : 'home' + $$('[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)) $$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`)) localStorage.setItem('multi-simadmin-view', view) } -function selectInstance(id) { - const item = instances.find(x => x.id === id) - if (!item) return - activeId = id - localStorage.setItem('multi-simadmin-active', id) - $('loginBtn').disabled = false - $('openExternal').href = item.url - $('openExternal').classList.remove('disabled') - $('emptyState').hidden = true - $('frame').hidden = false - $('frame').src = item.url - renderInstances() - renderOverview() - renderFeatures() -} +function selectInstance(id) { openDeviceDetail(id) } function openInstanceDialog(id = '') { const item = id ? instances.find(x => x.id === id) : null @@ -420,10 +439,8 @@ async function loadConfig({ preserveSelection = activeId } = {}) { const saved = preserveSelection || localStorage.getItem('multi-simadmin-active') activeId = saved && instances.some(x => x.id === saved) ? saved : null renderEndpointList() - renderInstances() - renderOverview() - renderFeatures() - if (activeId) selectInstance(activeId) + renderHomeCards() + if (activeId) openDeviceDetail(activeId) } async function refreshStatus({ silent = false } = {}) { @@ -434,9 +451,12 @@ async function refreshStatus({ silent = false } = {}) { if (!res.ok) throw new Error(`状态读取失败:HTTP ${res.status}`) const data = await res.json() statuses = new Map((data.instances || []).map(item => [item.id, item])) - renderInstances() - renderOverview() - renderFeatures() + renderHomeCards() + if (activeId) { + renderOverview() + renderDeviceDetails() + renderDetailActions() + } if (!silent) showToast(`已刷新 ${statuses.size} 个实例`) } catch (error) { showToast(error.message || '刷新失败', 4200) @@ -469,7 +489,7 @@ function parseBody() { async function runEndpoint() { if (!activeId) { - showToast('请先选择左侧设备') + showToast('请先在首页选择设备卡片') return } const endpoint = $('endpointInput').value.trim() @@ -505,16 +525,17 @@ function bindEvents() { $('addInstanceBtn').addEventListener('click', () => openInstanceDialog()) $('loginBtn').addEventListener('click', loginActive) $('runEndpoint').addEventListener('click', runEndpoint) - $('instanceSearch').addEventListener('input', renderInstances) + $('instanceSearch').addEventListener('input', renderHomeCards) + $('backToFleetBtn').addEventListener('click', setActiveHome) $('densityBtn').addEventListener('click', () => { document.body.classList.toggle('compact') localStorage.setItem('multi-simadmin-density', document.body.classList.contains('compact') ? 'compact' : 'normal') }) - $$('.phone-app[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view))) + $$('[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view))) $$('#statusFilters .tab-leaf').forEach(btn => btn.addEventListener('click', () => { activeFilter = btn.dataset.filter $$('#statusFilters .tab-leaf').forEach(x => x.classList.toggle('active', x === btn)) - renderInstances() + renderHomeCards() })) $('loginForm').addEventListener('submit', async (event) => { event.preventDefault() @@ -543,7 +564,7 @@ function bindEvents() { function restorePrefs() { if (localStorage.getItem('multi-simadmin-density') === 'compact') document.body.classList.add('compact') - switchView(localStorage.getItem('multi-simadmin-view') || 'dashboard') + switchView(localStorage.getItem('multi-simadmin-view') || 'home') } try { diff --git a/public/index.html b/public/index.html index 62edc1c..7869044 100644 --- a/public/index.html +++ b/public/index.html @@ -17,86 +17,95 @@
-
- - -
-
-
-

NO ISLAND SELECTED

-

选择一个 SimAdmin 小岛

-

集中展示设备信息、联网状态、SIM、信号、温度、流量、系统资源、短信与版本。

-
-
- - 原站打开 -
-
- - - -
-
-
实时状态
-
-
- -
- -
-
-
设备详情
-

按设备、SIM、蜂窝网络、系统资源、温度、流量和短信分类展示真实状态。

+
+
+
+
+
设备卡片墙
+

每张卡片对应一个 SimAdmin;卡片上直接显示在线状态、SIM、信号、温度、流量和短信摘要。

+
+
+
+ +
-
+
+
+ +
+
+
+

DEVICE DETAIL

+

选择一个 SimAdmin 设备

+

从首页设备卡片进入详情页后,可查看完整状态并使用功能入口。

+
+
+ + + 原站打开 +
+
+ +
+
+
实时状态
+
+
+
+
+
+
设备详情
+

设备、SIM、蜂窝网络、系统资源、温度、流量、短信与版本。

+
+
+
+
+
功能交互
+
+
+
+
+ +
+
+ + 原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。 +
+
+
等待选择
+

还没有选择设备

+

先在首页点击一个设备卡片,再进入原始页面。

+
+
@@ -110,22 +119,15 @@
-
选择设备后可调用统一代理接口。
+
从设备详情页进入后,可对当前设备调用代理接口。
- -
-
- 原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。 -
-
-
等待登岛
-

还没有选择设备

-

在左侧选择一个小岛后,这里会加载原 SimAdmin 页面。

-
- -
+ +
+ 本地配置 + config.json +
diff --git a/public/styles.css b/public/styles.css index 803edb2..fae2c5e 100644 --- a/public/styles.css +++ b/public/styles.css @@ -5,19 +5,13 @@ --animal-border-color:#aaa69d;--animal-border-color-hover:#827157;--animal-border-color-light:#e8e2d6; --animal-bg-color:#f8f8f0;--animal-bg-color-content:rgb(247,243,223);--animal-bg-color-secondary:#f0e8d8;--animal-bg-color-disabled:#f0ece2; --animal-font-family:Nunito,'Noto Sans SC',-apple-system,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif; - --animal-spacing-xs:4px;--animal-spacing-sm:8px;--animal-spacing-md:12px;--animal-spacing-lg:16px;--animal-spacing-xl:24px; - --animal-border-radius-sm:16px;--animal-border-radius-base:18px;--animal-border-radius-lg:24px;--animal-border-width:2px; --animal-shadow-sm:0 2px 4px 0 rgba(61,52,40,.06);--animal-shadow-base:0 3px 10px 0 rgba(61,52,40,.1);--animal-shadow-lg:0 8px 24px 0 rgba(61,52,40,.14); --animal-motion-duration-base:.25s;--animal-motion-ease:cubic-bezier(.4,0,.2,1); } -*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;min-height:100vh;font-family:var(--animal-font-family);font-weight:500;letter-spacing:.01em;color:var(--animal-text-color);background:var(--animal-bg-color);overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid #ffcc00;outline-offset:2px}.island-bg{position:fixed;inset:0;z-index:-1;overflow:hidden;background:radial-gradient(circle at 12% 10%,rgba(130,213,187,.28),transparent 24%),radial-gradient(circle at 88% 6%,rgba(247,205,103,.28),transparent 22%),linear-gradient(180deg,#f8f8f0 0%,#f4efd9 54%,#e8dfbd 100%)}.island-bg:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.cloud{position:absolute;border-radius:999px;background:rgba(255,255,255,.72);filter:blur(.1px)}.cloud:before,.cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.cloud-a{width:170px;height:48px;left:8%;top:7%}.cloud-a:before{width:72px;height:72px;left:28px;top:-28px}.cloud-a:after{width:56px;height:56px;right:26px;top:-18px}.cloud-b{width:210px;height:58px;right:8%;top:12%;opacity:.7}.cloud-b:before{width:86px;height:86px;left:34px;top:-36px}.cloud-b:after{width:72px;height:72px;right:28px;top:-26px}.leaf{position:absolute;color:#8ac68a;font-size:18px;opacity:.42;animation:leafFloat 7s ease-in-out infinite}.leaf-a{left:46%;top:8%}.leaf-b{right:26%;bottom:9%;animation-delay:1.7s}@keyframes leafFloat{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-12px) rotate(12deg)}} -.island-shell{height:100vh;display:grid;grid-template-columns:390px minmax(0,1fr);gap:24px;padding:24px}.nook-sidebar,.nook-workspace{min-height:0}.nook-sidebar{display:flex;flex-direction:column;gap:14px}.nook-workspace{display:flex;flex-direction:column;gap:16px;overflow:hidden}.island-card{border-radius:20px;background:var(--animal-bg-color-content);color:var(--animal-text-color-body);font-weight:500;transition:all .3s ease;border:1.5px solid #d4c4a8}.island-card:hover{transform:translateY(-2px)}.pattern-default{background:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px),rgb(247,243,223);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#d4c4a8}.pattern-app-teal{background:radial-gradient(circle,rgba(130,213,187,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(170,235,210,.12) 1px,transparent 1px),#e8faf5;background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#82d5bb;color:#2a6b5a}.pattern-app-yellow{background:radial-gradient(circle,rgba(247,205,103,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(255,230,160,.12) 1px,transparent 1px),#fff8e0;background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#f7cd67;color:#7a6528}.pattern-app-blue{background:radial-gradient(circle,rgba(136,157,240,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(180,195,255,.12) 1px,transparent 1px),#e8edff;background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#889df0;color:#4a5a8a}.pattern-purple{background:radial-gradient(circle,rgba(183,125,238,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(220,180,255,.12) 1px,transparent 1px),#f0e8ff;background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#b77dee;color:#6a3a9a}.pattern-app-pink{background:radial-gradient(circle,rgba(248,166,178,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(255,200,210,.12) 1px,transparent 1px),#fde4e8;background-size:28px 28px,14px 14px;background-position:0 0,7px 7px;border-color:#f8a6b2;color:#a85565} -.sidebar-card{padding:18px;text-align:center}.sidebar-subtitle{margin:12px 0 16px;color:var(--animal-text-color-muted);line-height:1.45}.sidebar-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}.block-btn{width:100%}.ribbon{--rf:#27d039;--rb:#20992a;--rk:#115017;--rt:#fff;position:relative;display:inline-flex;align-items:center;justify-content:center;height:2em;padding:0 1.6em;color:var(--rt);font-size:24px;font-weight:900;letter-spacing:.04em;white-space:nowrap;filter:drop-shadow(0 .08em .12em rgba(61,52,40,.08))}.compact-ribbon{font-size:19px}.ribbon-text{position:relative;z-index:4;height:2em;display:inline-flex;align-items:center;padding-top:.11em;color:var(--rt);text-shadow:0 .04em .08em rgba(61,52,40,.08)}.ribbon-back{position:absolute;bottom:-.4em;width:1.7em;height:1.7em;background:var(--rb);z-index:1}.ribbon-back-left{left:-.6em;border-radius:.08em 0 0 .08em;clip-path:polygon(100% 0,100% 100%,0 100%,30% 50%,0 0)}.ribbon-back-right{right:-.6em;border-radius:0 .08em .08em 0;clip-path:polygon(0 0,100% 0,70% 50%,100% 100%,0 100%)}.ribbon-fold{position:absolute;top:calc(100% - .05em);width:0;height:0;border-style:solid;z-index:2}.ribbon-fold-left{left:.15em;border-width:0 .95em .45em 0;border-color:transparent var(--rk) transparent transparent}.ribbon-fold-right{right:.16em;border-width:0 0 .45em .95em;border-color:transparent transparent transparent var(--rk)}.ribbon-front{position:absolute;inset:0 .1em;background:var(--rf);border-radius:.2em;z-index:3;transform:perspective(11.5em) rotateX(3deg);box-shadow:inset 0 -.06em 0 rgba(0,0,0,.05)}.section-ribbon{--rf:#8ac68a;--rb:#509050;--rk:#205020;--rt:#fff;display:inline-flex;align-items:center;min-height:34px;padding:6px 18px;border-radius:12px;background:var(--rf);color:var(--rt);font-weight:900;letter-spacing:.04em;box-shadow:0 4px 0 0 var(--rb);transform:perspective(800px) rotateY(3deg)}.color-app-yellow{--rf:#f7cd67;--rb:#d4a030;--rk:#8a6010;--rt:#725d42}.color-app-green{--rf:#8ac68a;--rb:#509050;--rk:#205020;--rt:#fff}.color-purple{--rf:#b77dee;--rb:#9050d0;--rk:#5a1a9a;--rt:#fff}.color-app-pink{--rf:#f8a6b2;--rb:#e06880;--rk:#a03060;--rt:#fff}.color-app-teal{--rf:#82d5bb;--rb:#40a880;--rk:#186048;--rt:#fff} -.animal-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:45px;padding:0 20px;border:2px solid transparent;border-radius:50px;font-weight:700;letter-spacing:.02em;text-decoration:none;line-height:1;transition:all var(--animal-motion-duration-base) var(--animal-motion-ease);user-select:none}.primary-btn{color:#794f27;background:#f8f8f0;border-color:#f8f8f0;box-shadow:0 5px 0 0 #bdaea0}.primary-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 6px 0 0 #bdaea0}.primary-btn:active:not(:disabled){transform:translateY(2px);box-shadow:0 1px 0 0 #bdaea0}.default-btn{color:var(--animal-text-color);background:var(--animal-bg-color);border-color:var(--animal-border-color);box-shadow:var(--animal-shadow-sm)}.default-btn:hover:not(:disabled){color:var(--animal-primary-color);border-color:var(--animal-primary-color);box-shadow:var(--animal-shadow-base);transform:translateY(-1px)}.danger-btn{color:#fff;background:var(--animal-error-color);border-color:var(--animal-error-color);box-shadow:0 5px 0 0 var(--animal-error-color-active)}.danger-btn:hover:not(:disabled){background:#e87878;transform:translateY(-1px);box-shadow:0 6px 0 0 var(--animal-error-color-active)}.disabled,button:disabled{opacity:.5;pointer-events:none}.wallet-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.fleet-card{border-radius:999px;background:#d1da49;color:#3d5a1a;border:2px solid rgba(90,107,40,.18);padding:9px 8px;text-align:center;box-shadow:inset 0 -2px 0 rgba(61,52,40,.08)}.fleet-card b{display:block;font-size:22px;font-weight:900;line-height:1}.fleet-card span{display:block;margin-top:2px;font-size:11px;font-weight:700}.search-pill{height:48px;display:flex;align-items:center;gap:8px;background:rgb(247,243,223);border:2.5px solid #c4b89e;border-radius:50px;padding:0 16px;color:#8a7b66}.search-pill:focus-within{border-color:#ffcc00;box-shadow:0 0 0 3px rgba(255,204,0,.18)}.search-pill input{width:100%;border:0;outline:0;background:transparent;color:#725d42;font-weight:600}.search-pill input::placeholder{color:#9f927d;font-weight:400}.island-tabs,.module-tabs{display:flex;gap:8px;flex-wrap:wrap}.tab-leaf,.module-tabs button{border:0;border-radius:24px;background:rgba(255,255,255,.6);color:#794f27;padding:9px 14px;font-weight:700;transition:all .25s cubic-bezier(.4,0,.2,1)}.tab-leaf:hover,.module-tabs button:hover{background:rgba(25,200,185,.1)}.tab-leaf.active,.module-tabs button.active{background:#0cc0b5;color:#fff9e3;box-shadow:0 3px 0 0 rgba(61,52,40,.08)}.island-list{min-height:0;overflow:auto;display:flex;flex-direction:column;gap:10px;padding-right:3px}.instance-card{border-radius:20px;background:rgb(247,243,223);border:1.5px solid #d4c4a8;padding:14px;color:#725d42;transition:all .3s ease;cursor:pointer}.instance-card:hover{transform:translateY(-2px)}.instance-card.active{border-color:#19c8b9;box-shadow:0 0 0 4px rgba(25,200,185,.15)}.instance-top{display:flex;justify-content:space-between;gap:10px}.instance-name{font-size:16px;font-weight:800;color:#794f27}.instance-url{margin-top:4px;color:#9f927d;font-size:12px;word-break:break-all}.instance-card-actions{display:flex;gap:7px;margin-top:10px}.mini-action{border:1.5px solid #d4c4a8;border-radius:16px;background:#fff9e3;color:#725d42;font-size:12px;font-weight:800;padding:5px 10px}.mini-action:hover{border-color:#19c8b9;color:#19c8b9}.mini-action.danger{border-color:#f8a6b2;color:#e05a5a}.instance-desc{margin-top:8px;color:#8a7b66;font-size:12px;line-height:1.45}.status-pill{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;border-radius:50px;padding:6px 10px;background:#f0e8d8;border:1.5px solid #e8e2d6;font-size:12px;font-weight:800;color:#8a7b66}.status-pill:before{content:"";width:8px;height:8px;border-radius:50%;background:#9f927d}.status-pill.ok{background:#e8f5e8;color:#3a6b3a;border-color:#8ac68a}.status-pill.ok:before{background:#6fba2c}.status-pill.warn{background:#fff8e0;color:#7a6528;border-color:#f7cd67}.status-pill.warn:before{background:#f5c31c}.status-pill.bad{background:#ffe8e8;color:#9a3a3a;border-color:#fc736d}.status-pill.bad:before{background:#e05a5a}.mini-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:12px}.mini-grid span{min-width:0;color:#9f927d;font-size:11px}.mini-grid b{display:block;color:#725d42;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.config-note{margin-top:auto;color:#8a7b66;font-size:12px}.config-note code{display:block;margin-top:5px;color:#725d42;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.workspace-hero{padding:22px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px}.eyebrow{margin:0 0 6px;color:#8a7b66;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1,h2,h3,p{margin:0}h1{font-size:34px;line-height:1.05;font-weight:900;color:#794f27;letter-spacing:.02em}.workspace-hero p:last-child{margin-top:8px;color:#725d42;line-height:1.5}.hero-actions{display:flex;gap:10px;white-space:nowrap}.phone-dock{display:grid;grid-template-columns:repeat(4,112px);gap:14px}.phone-app{height:96px;border:0;border-radius:36px;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-weight:900;transition:all .25s cubic-bezier(.4,0,.2,1);box-shadow:0 6px 0 rgba(61,52,40,.12)}.phone-app:hover{transform:translateY(-2px) rotate(-1deg)}.phone-app:active{transform:translateY(2px);box-shadow:0 2px 0 rgba(61,52,40,.12)}.phone-app.active{outline:4px solid rgba(255,204,0,.55)}.phone-app span{font-size:26px}.phone-app b{font-size:13px}.app-teal{background:#82d5bb}.app-yellow{background:#f7cd67;color:#725d42}.app-blue{background:#889df0}.app-green{background:#8ac68a}.view{display:none;min-height:0;flex:1;overflow:auto}.view.active{display:block}.dashboard-view{display:none}.dashboard-view.active{display:grid;grid-template-columns:minmax(280px,.82fr) minmax(0,1.18fr);grid-template-rows:auto minmax(0,1fr);gap:16px}.signal-board{padding:18px}.signal-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:18px}.signal-item,.metric{border-radius:20px;background:rgba(255,255,255,.56);border:2px solid rgba(232,226,214,.8);padding:13px}.signal-item span,.metric span{display:block;color:#9f927d;font-size:12px;font-weight:800}.signal-item b,.metric b{display:block;margin-top:6px;color:#794f27;font-size:18px;font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nook-metrics{display:grid;grid-template-columns:repeat(2,minmax(140px,1fr));gap:12px}.module-board{grid-column:1/-1;padding:18px;min-height:0}.board-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}.app-grid{display:grid;grid-template-columns:repeat(4,minmax(170px,1fr));gap:14px}.feature{min-height:168px;border-radius:34px;padding:16px;color:#fff;transition:all .25s cubic-bezier(.4,0,.2,1);box-shadow:0 5px 0 rgba(61,52,40,.13);overflow:hidden}.feature:nth-child(8n+1){background:#82d5bb}.feature:nth-child(8n+2){background:#f7cd67;color:#725d42}.feature:nth-child(8n+3){background:#889df0}.feature:nth-child(8n+4){background:#f8a6b2}.feature:nth-child(8n+5){background:#8ac68a}.feature:nth-child(8n+6){background:#e59266}.feature:nth-child(8n+7){background:#b77dee}.feature:nth-child(8n+8){background:#d1da49;color:#3d5a1a}.feature:hover{transform:translateY(-3px) rotate(-.6deg)}.feature-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.feature .eyebrow{color:inherit;opacity:.78}.feature h4{margin:0;font-size:18px;font-weight:900}.feature p{margin-top:8px;font-size:13px;line-height:1.45;color:inherit;opacity:.9}.endpoint-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:13px}.endpoint-pills button{border:2px solid rgba(255,255,255,.55);border-radius:16px;background:rgba(255,255,255,.28);color:inherit;font-size:11px;font-weight:800;padding:5px 7px}.endpoint-pills button:hover{background:rgba(255,255,255,.45)}.danger-note{margin-top:10px;font-size:12px;font-weight:800;color:#fff8e3}.api-layout.active{display:grid;grid-template-columns:330px minmax(0,1fr);gap:16px}.api-catalog,.api-console{padding:18px;min-height:0}.api-catalog{overflow:auto}.endpoint-group{margin:16px 0}.endpoint-group h4{margin:0 0 8px;font-size:14px;font-weight:900;color:inherit}.endpoint-row{display:block;width:100%;text-align:left;border:2px solid rgba(255,255,255,.7);border-radius:18px;background:rgba(255,255,255,.45);color:inherit;font-size:12px;font-weight:800;padding:9px 11px;margin-bottom:7px}.endpoint-row:hover,.endpoint-row.active{background:#fff9e3;border-color:#19c8b9;color:#794f27}.api-console{display:flex;flex-direction:column;gap:12px}.console-toolbar{display:flex;gap:10px}.console-toolbar select{width:110px}.console-toolbar input{flex:1}input,select,textarea{border:2.5px solid #c4b89e;background:rgb(247,243,223);border-radius:50px;padding:11px 15px;color:#725d42;font-weight:700}textarea{border-radius:20px}.request-body{min-height:96px;resize:vertical}.raw-output{flex:1;min-height:410px;margin:0;border:2px solid #d4c4a8;border-radius:20px;background:#fff8e7;color:#725d42;padding:16px;overflow:auto;font:600 12px/1.6 var(--animal-font-family)}.rawpage-view.active{display:flex;flex-direction:column;gap:12px}.frame-toolbar{padding:14px 16px;font-weight:700}.frame{width:100%;flex:1;min-height:580px;border:2px solid #d4c4a8;border-radius:24px;background:white}.empty-state{padding:42px}.mini-empty{padding:18px;text-align:center}.empty-state h3{margin-top:18px;font-size:24px;color:#794f27}.mini-empty h3{margin-top:0;font-size:18px}.empty-state p{margin-top:8px;color:#725d42}dialog{border:0;background:transparent}dialog::backdrop{background:rgba(61,52,40,.35);backdrop-filter:blur(3px)}.login-card,.instance-form{width:min(520px,90vw);padding:24px}.login-card h3{margin-top:18px;font-size:24px;color:#794f27}.login-card p,.form-help{margin-top:8px;color:#725d42;line-height:1.5}.login-card input{width:100%;margin:16px 0}.login-card menu,.form-menu{display:flex;justify-content:flex-end;gap:10px;margin:0;padding:0}.instance-form{max-height:88vh;overflow:auto}.instance-form label{display:block;margin-top:13px;color:#794f27;font-size:13px;font-weight:900}.instance-form input,.instance-form textarea{display:block;width:100%;margin-top:6px}.instance-form textarea{min-height:78px;resize:vertical}.form-menu{margin-top:18px}.form-menu .danger-btn{margin-right:auto}.toast{position:fixed;right:22px;bottom:22px;border:2px solid #d4c4a8;border-radius:20px;background:rgb(247,243,223);color:#794f27;padding:13px 16px;font-weight:800;box-shadow:var(--animal-shadow-lg);z-index:10}.compact .island-shell{gap:14px;padding:14px}.compact .feature{min-height:134px;padding:12px;border-radius:26px}.compact .workspace-hero{padding:16px}.compact .phone-app{height:78px;border-radius:28px}.compact .app-grid{gap:10px} -@media(max-width:1260px){.island-shell{grid-template-columns:340px minmax(0,1fr)}.app-grid{grid-template-columns:repeat(3,1fr)}.dashboard-view.active{grid-template-columns:1fr}.nook-metrics{grid-template-columns:repeat(4,1fr)}.phone-dock{grid-template-columns:repeat(4,96px)}}@media(max-width:900px){body{overflow:auto}.island-shell{height:auto;grid-template-columns:1fr;padding:14px}.nook-sidebar{max-height:none}.nook-workspace{overflow:visible}.workspace-hero{flex-direction:column;align-items:flex-start}.phone-dock{grid-template-columns:repeat(4,1fr)}.dashboard-view.active,.api-layout.active{grid-template-columns:1fr}.app-grid,.nook-metrics{grid-template-columns:1fr 1fr}}@media(max-width:560px){.wallet-row,.signal-grid,.app-grid,.nook-metrics{grid-template-columns:1fr}.sidebar-actions,.phone-dock{grid-template-columns:1fr 1fr}.hero-actions{flex-direction:column;width:100%}.hero-actions .animal-btn{width:100%}} -@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}} - - -/* Device monitoring rebuild: keep API as secondary, make dashboard data-first. */ -.device-dashboard.active{grid-template-columns:minmax(320px,.72fr) minmax(0,1.28fr);grid-template-rows:auto minmax(0,1fr);align-items:stretch}.monitor-summary-card{min-height:100%}.monitor-kpis{align-content:start}.device-detail-board{grid-column:2;grid-row:1 / span 2;padding:18px;overflow:auto}.board-caption{color:#8a7b66;margin-top:10px;line-height:1.5}.device-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px;margin-top:18px}.device-info-card{background:rgba(255,255,255,.62);border:2px solid rgba(212,196,168,.72);border-radius:24px;padding:16px;min-width:0;box-shadow:inset 0 -2px 0 rgba(61,52,40,.04)}.device-info-card h4{margin:0 0 12px;color:#794f27;font-size:18px;font-weight:900}.device-info-card dl{display:grid;gap:8px;margin:0}.device-info-card dl div{display:grid;grid-template-columns:86px minmax(0,1fr);gap:10px;align-items:baseline}.device-info-card dt{color:#9f927d;font-size:12px;font-weight:900}.device-info-card dd{margin:0;color:#4d3a22;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.monitor-device-card .instance-card-actions{margin-top:10px}.monitor-mini-grid{grid-template-columns:repeat(2,1fr);margin-top:12px}.monitor-metric.temperature b{color:#c05b28}.monitor-metric.traffic b{color:#2f7e98}.monitor-metric.signal b{color:#4b8f24}.status-pill.signal,.status-pill.temperature,.status-pill.traffic{background:#e6f9f6;color:#2a6b5a}.compact .device-info-card{padding:12px}.compact .device-info-card dl{gap:5px}.api-layout:before{content:'调试接口为次级入口;日常巡检请回到设备监控页。';display:block;grid-column:1 / -1;color:#8a7b66;font-weight:800;margin:0 0 8px 4px}@media(max-width:1260px){.device-dashboard.active{grid-template-columns:1fr}.device-detail-board{grid-column:auto;grid-row:auto}.device-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:760px){.device-detail-grid{grid-template-columns:1fr}.device-info-card dl div{grid-template-columns:76px minmax(0,1fr)}} +*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;min-height:100vh;font-family:var(--animal-font-family);font-weight:500;letter-spacing:.01em;color:var(--animal-text-color);background:var(--animal-bg-color);overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,.sim-device-card:focus-visible{outline:3px solid #ffcc00;outline-offset:3px} +.island-bg{position:fixed;inset:0;z-index:-1;overflow:hidden;background:radial-gradient(circle at 12% 10%,rgba(130,213,187,.28),transparent 24%),radial-gradient(circle at 88% 6%,rgba(247,205,103,.28),transparent 22%),linear-gradient(180deg,#f8f8f0 0%,#f4efd9 54%,#e8dfbd 100%)}.island-bg:before{content:"";position:absolute;inset:0;background-image:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.cloud{position:absolute;border-radius:999px;background:rgba(255,255,255,.72)}.cloud:before,.cloud:after{content:"";position:absolute;border-radius:50%;background:inherit}.cloud-a{width:170px;height:48px;left:8%;top:7%}.cloud-a:before{width:72px;height:72px;left:28px;top:-28px}.cloud-a:after{width:56px;height:56px;right:26px;top:-18px}.cloud-b{width:210px;height:58px;right:8%;top:12%;opacity:.7}.cloud-b:before{width:86px;height:86px;left:34px;top:-36px}.cloud-b:after{width:72px;height:72px;right:28px;top:-26px}.leaf{position:absolute;color:#8ac68a;font-size:18px;opacity:.42;animation:leafFloat 7s ease-in-out infinite}.leaf-a{left:46%;top:8%}.leaf-b{right:26%;bottom:9%;animation-delay:1.7s}@keyframes leafFloat{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-12px) rotate(12deg)}} +.island-card{border-radius:24px;background:var(--animal-bg-color-content);color:var(--animal-text-color-body);border:1.5px solid #d4c4a8;box-shadow:var(--animal-shadow-base)}.pattern-default{background:radial-gradient(circle,rgba(196,184,158,.15) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(196,184,158,.1) 1px,transparent 1px),rgb(247,243,223);background-size:28px 28px,14px 14px;background-position:0 0,7px 7px}.pattern-app-teal{background:radial-gradient(circle,rgba(130,213,187,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(170,235,210,.12) 1px,transparent 1px),#e8faf5;background-size:28px 28px,14px 14px;border-color:#82d5bb;color:#2a6b5a}.pattern-app-yellow{background:radial-gradient(circle,rgba(247,205,103,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(255,230,160,.12) 1px,transparent 1px),#fff8e0;background-size:28px 28px,14px 14px;border-color:#f7cd67;color:#7a6528}.pattern-app-blue{background:radial-gradient(circle,rgba(136,157,240,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(180,195,255,.12) 1px,transparent 1px),#e8edff;background-size:28px 28px,14px 14px;border-color:#889df0;color:#4a5a8a}.pattern-purple{background:radial-gradient(circle,rgba(183,125,238,.18) 1.5px,transparent 1.5px),radial-gradient(circle,rgba(220,180,255,.12) 1px,transparent 1px),#f0e8ff;background-size:28px 28px,14px 14px;border-color:#b77dee;color:#6a3a9a}.pattern-app-pink{background:#ffe8ee;border-color:#f8a6b2}.section-ribbon{--rf:#8ac68a;--rb:#509050;display:inline-flex;align-items:center;min-height:34px;padding:6px 18px;border-radius:12px;background:var(--rf);color:#fff;font-weight:900;letter-spacing:.04em;box-shadow:0 4px 0 0 var(--rb)}.color-app-yellow{--rf:#f7cd67;--rb:#d4a030;color:#725d42}.color-app-green{--rf:#8ac68a;--rb:#509050}.color-purple{--rf:#b77dee;--rb:#8a4ac0}.color-app-pink{--rf:#f8a6b2;--rb:#d77485} +h1,h2,h3,h4,p{margin:0}.eyebrow{margin:0 0 6px;color:#8a7b66;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:34px;line-height:1.05;font-weight:900;color:#794f27}.topbar-copy,.board-caption{margin-top:8px;color:#725d42;line-height:1.5}.animal-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:45px;padding:0 20px;border:2px solid transparent;border-radius:50px;font-weight:800;text-decoration:none;line-height:1;transition:all var(--animal-motion-duration-base) var(--animal-motion-ease);user-select:none}.primary-btn{color:#794f27;background:#f8f8f0;border-color:#f8f8f0;box-shadow:0 5px 0 0 #bdaea0}.primary-btn:hover:not(:disabled),.default-btn:hover:not(:disabled){transform:translateY(-1px)}.default-btn{color:var(--animal-text-color);background:var(--animal-bg-color);border-color:var(--animal-border-color);box-shadow:var(--animal-shadow-sm)}.danger-btn{color:#fff;background:var(--animal-error-color);border-color:var(--animal-error-color)}.disabled,button:disabled{opacity:.5;pointer-events:none} +.sim-shell{height:100vh;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:16px;padding:20px}.sim-topbar{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:20px 24px}.hero-actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.sim-main{min-height:0;overflow:hidden}.view{display:none;min-height:0;height:100%;overflow:auto}.view.active{display:block}.home-view{display:none}.home-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.home-toolbar{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(420px,1.2fr);gap:16px;align-items:end;padding:18px}.home-tools{display:grid;grid-template-columns:260px minmax(220px,1fr);gap:12px;align-items:center}.wallet-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.fleet-card{border-radius:999px;background:#d1da49;color:#3d5a1a;border:2px solid rgba(90,107,40,.18);padding:9px 8px;text-align:center;box-shadow:inset 0 -2px 0 rgba(61,52,40,.08)}.fleet-card b{display:block;font-size:22px;font-weight:900;line-height:1}.fleet-card span{display:block;margin-top:2px;font-size:11px;font-weight:800}.search-pill{height:48px;display:flex;align-items:center;gap:8px;background:rgb(247,243,223);border:2.5px solid #c4b89e;border-radius:50px;padding:0 16px;color:#8a7b66}.search-pill input{width:100%;border:0;outline:0;background:transparent}.island-tabs{grid-column:1 / -1;display:flex;gap:8px;flex-wrap:wrap}.tab-leaf{border:0;border-radius:999px;background:#f7f3df;color:#725d42;padding:9px 16px;font-weight:900}.tab-leaf.active{background:#19c8b9;color:#fff}.home-device-grid{min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;padding:2px 4px 16px}.sim-device-card{border:2px solid rgba(212,196,168,.88);border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,.76),rgba(247,243,223,.92));padding:18px;box-shadow:0 8px 22px rgba(61,52,40,.1);transition:transform .2s ease, box-shadow .2s ease, border-color .2s ease}.sim-device-card:hover{transform:translateY(-3px);box-shadow:0 14px 30px rgba(61,52,40,.15);border-color:#19c8b9}.sim-card-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.sim-card-head h2{font-size:24px;color:#4d3a22}.instance-url{display:block;margin-top:4px;color:#8a7b66;font-size:12px;word-break:break-all}.status-pill{display:inline-flex;align-items:center;justify-content:center;min-height:28px;padding:0 10px;border-radius:999px;background:#eee;color:#725d42;font-size:12px;font-weight:900;white-space:nowrap}.status-pill.ok,.sim-device-card.ok .status-pill{background:#dff5d7;color:#3f7624}.status-pill.warn,.sim-device-card.warn .status-pill{background:#fff0bf;color:#8a6010}.status-pill.bad,.sim-device-card.bad .status-pill{background:#ffe0e0;color:#9a3030}.sim-card-model{margin-top:14px;color:#725d42;font-weight:900}.sim-card-kpis{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px}.sim-card-kpis span,.signal-item,.metric{border-radius:18px;background:rgba(255,255,255,.62);border:2px solid rgba(232,226,214,.8);padding:12px;min-width:0}.sim-card-kpis em,.signal-item span,.metric span{display:block;color:#9f927d;font-size:12px;font-weight:900;font-style:normal}.sim-card-kpis b,.signal-item b,.metric b{display:block;margin-top:5px;color:#794f27;font-size:16px;font-weight:900;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sim-card-foot{display:flex;justify-content:space-between;gap:10px;margin-top:14px;color:#8a7b66;font-size:12px;font-weight:800}.sim-config-note{display:flex;gap:8px;align-items:center;justify-content:flex-end;color:#8a7b66;font-size:12px}.sim-config-note code{background:rgba(255,255,255,.6);border-radius:8px;padding:4px 8px} +.detail-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:16px}.workspace-hero{padding:20px 24px;display:flex;align-items:center;justify-content:space-between;gap:18px}.detail-layout{min-height:0;display:grid;grid-template-columns:minmax(320px,.75fr) minmax(0,1.25fr);grid-template-rows:auto minmax(0,1fr) auto;gap:16px;overflow:auto}.signal-board{padding:18px}.signal-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:18px}.nook-metrics{display:grid;grid-template-columns:repeat(2,minmax(140px,1fr));gap:12px}.device-detail-board{grid-column:2;grid-row:1 / span 2;padding:18px;overflow:auto}.device-detail-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px;margin-top:18px}.device-info-card{background:rgba(255,255,255,.62);border:2px solid rgba(212,196,168,.72);border-radius:24px;padding:16px;min-width:0;box-shadow:inset 0 -2px 0 rgba(61,52,40,.04)}.device-info-card h4{margin:0 0 12px;color:#794f27;font-size:18px;font-weight:900}.device-info-card dl{display:grid;gap:8px;margin:0}.device-info-card dl div{display:grid;grid-template-columns:86px minmax(0,1fr);gap:10px;align-items:baseline}.device-info-card dt{color:#9f927d;font-size:12px;font-weight:900}.device-info-card dd{margin:0;color:#4d3a22;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.detail-actions-card{grid-column:1 / -1;padding:18px}.detail-actions{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-top:18px}.detail-action{min-height:64px;border:2px solid rgba(212,196,168,.85);background:rgba(255,255,255,.72);border-radius:20px;padding:10px 14px;text-align:left;font-weight:900;color:#4d3a22}.detail-action:hover{border-color:#19c8b9;transform:translateY(-1px)}.detail-action span{display:block}.detail-action code{display:block;margin-top:4px;color:#8a7b66;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.raw-action{background:#e8edff;color:#4a5a8a}.api-layout.active{display:grid;grid-template-columns:320px minmax(0,1fr);gap:16px}.api-layout:before{content:'调试接口为次级入口;日常使用请从首页卡片进入设备详情。';display:block;grid-column:1 / -1;color:#8a7b66;font-weight:800;margin-left:4px}.api-catalog,.api-console{padding:18px}.endpoint-list{margin-top:16px;display:grid;gap:12px}.endpoint-group h4{margin:0 0 8px}.endpoint-row{width:100%;border:0;border-radius:14px;background:rgba(255,255,255,.58);padding:9px 10px;text-align:left;font-size:12px}.endpoint-row span{font-weight:900;color:#6a3a9a;margin-right:6px}.console-toolbar{display:grid;grid-template-columns:110px minmax(0,1fr) 100px;gap:10px}.console-toolbar select,.console-toolbar input,.request-body{border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px}.request-body{width:100%;min-height:110px;margin-top:12px}.raw-output{min-height:340px;overflow:auto;background:#3a3028;color:#f8f0dc;border-radius:18px;padding:16px;white-space:pre-wrap}.rawpage-view.active{display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px}.frame-toolbar{padding:14px 18px;display:flex;gap:12px;align-items:center}.frame{width:100%;height:100%;border:0;border-radius:24px;background:#fff}.empty-state{padding:28px;text-align:center}.login-card,.instance-form{width:min(560px,calc(100vw - 32px));padding:24px}.login-card input,.instance-form input,.instance-form textarea{width:100%;border:2px solid #d4c4a8;border-radius:14px;background:#fff;padding:10px;margin-top:6px}.instance-form{display:grid;gap:12px}.instance-form label{font-weight:900}.form-menu,dialog menu{display:flex;justify-content:flex-end;gap:10px;padding:0;margin:10px 0 0}.toast{position:fixed;right:20px;bottom:20px;background:#4d3a22;color:#fff;border-radius:999px;padding:12px 18px;font-weight:900;box-shadow:var(--animal-shadow-lg);z-index:20}.compact .sim-device-card{padding:14px}.compact .sim-card-kpis{grid-template-columns:repeat(3,1fr);gap:8px}.compact .device-info-card{padding:12px}.compact .device-info-card dl{gap:5px} +@media(max-width:1260px){.home-toolbar{grid-template-columns:1fr}.detail-layout{grid-template-columns:1fr}.device-detail-board{grid-column:auto;grid-row:auto}.detail-actions-card{grid-column:auto}.device-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.nook-metrics{grid-template-columns:repeat(4,1fr)}}@media(max-width:900px){body{overflow:auto}.sim-shell{height:auto;padding:14px}.sim-topbar,.workspace-hero{flex-direction:column;align-items:flex-start}.sim-main{overflow:visible}.view{overflow:visible}.home-tools{grid-template-columns:1fr}.home-device-grid{overflow:visible;grid-template-columns:1fr}.api-layout.active{grid-template-columns:1fr}.nook-metrics,.device-detail-grid{grid-template-columns:1fr 1fr}.hero-actions{justify-content:flex-start}}@media(max-width:560px){.wallet-row,.signal-grid,.nook-metrics,.device-detail-grid,.sim-card-kpis{grid-template-columns:1fr}.hero-actions{width:100%}.hero-actions .animal-btn{width:100%}.console-toolbar{grid-template-columns:1fr}.device-info-card dl div{grid-template-columns:76px minmax(0,1fr)}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}} diff --git a/test/ui-contract.test.js b/test/ui-contract.test.js index c194e87..438cb89 100644 --- a/test/ui-contract.test.js +++ b/test/ui-contract.test.js @@ -8,19 +8,30 @@ async function text(path) { return readFile(new URL(path, root), 'utf8') } -test('dashboard primary surface is a device monitoring board, not an API catalog', async () => { +test('home screen is a simadmin card wall and device detail owns interactions', async () => { const html = await text('public/index.html') const app = await text('public/app.js') - assert.match(html, /设备监控|设备详情|实时状态/) - assert.match(app, /renderDeviceDetails/) + assert.match(html, /id="homeView"/) + assert.match(html, /id="homeDeviceGrid"/) + assert.match(html, /设备卡片|设备卡片墙|SimAdmin 卡片/) + assert.match(html, /id="detailView"/) + assert.match(html, /id="backToFleetBtn"/) + assert.match(html, /id="detailActions"/) + assert.match(html, /id="detailDeviceGrid"/) + + assert.match(app, /function renderHomeCards/) + assert.match(app, /function openDeviceDetail/) + assert.match(app, /function renderDeviceDetails/) + assert.match(app, /data-open-detail/) + assert.match(app, /\['home', 'detail', 'rawpage', 'api'\]/) assert.match(app, /温度/) assert.match(app, /流量|速率/) assert.match(app, /内存|磁盘/) assert.match(app, /SIM|ICCID/) assert.match(app, /短信/) - const dashboardBlock = html.match(/
\s*
\s*