Refocus UI on device card details
This commit is contained in:
+111
-90
@@ -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 = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。</p></section>'
|
||||
root.innerHTML = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击“添加设备”新增 SimAdmin 地址后,这里会出现设备卡片。</p></section>'
|
||||
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 `<article class="instance-card monitor-device-card ${item.id === activeId ? 'active' : ''}" data-id="${escapeHtml(item.id)}">
|
||||
<div class="instance-top">
|
||||
const { s, device, sim, network, data, sms, system, ota } = deviceSummary(item)
|
||||
const dataState = firstValue(data.active, data.connected, data.enabled)
|
||||
return `<article class="sim-device-card ${pillClass(s)}" data-open-detail="${escapeHtml(item.id)}" role="button" tabindex="0" aria-label="打开 ${escapeHtml(item.name || item.id)} 详情">
|
||||
<div class="sim-card-head">
|
||||
<div>
|
||||
<div class="instance-name">${escapeHtml(item.name || item.id)}</div>
|
||||
<div class="instance-url">${escapeHtml(item.url)}</div>
|
||||
<p class="eyebrow">${escapeHtml(item.id)}</p>
|
||||
<h2>${escapeHtml(item.name || item.id)}</h2>
|
||||
<span class="instance-url">${escapeHtml(item.url)}</span>
|
||||
</div>
|
||||
<span class="status-pill ${pillClass(s)}">${escapeHtml(statusText(s))}</span>
|
||||
</div>
|
||||
<div class="mini-grid monitor-mini-grid">
|
||||
<span>信号<b>${escapeHtml(value(network.signal || sim.signal))}</b></span>
|
||||
<span>温度<b>${escapeHtml(formatTemperature(system.temperature))}</b></span>
|
||||
<span>速率<b>${escapeHtml(formatSpeed(system.networkSpeed))}</b></span>
|
||||
<span>SIM<b>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</b></span>
|
||||
<div class="sim-card-model">${escapeHtml(value([device.manufacturer, device.model].filter(Boolean).join(' ') || device.imei || '未知设备'))}</div>
|
||||
<div class="sim-card-kpis">
|
||||
<span><em>SIM</em><b>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</b></span>
|
||||
<span><em>信号</em><b>${escapeHtml(value(firstValue(network.signal, sim.signal)))}</b></span>
|
||||
<span><em>温度</em><b>${escapeHtml(formatTemperature(system.temperature))}</b></span>
|
||||
<span><em>流量</em><b>${escapeHtml(formatSpeed(system.networkSpeed))}</b></span>
|
||||
<span><em>数据</em><b>${escapeHtml(dataState === true ? '已连接' : dataState === false ? '未连接' : '-')}</b></span>
|
||||
<span><em>短信</em><b>${escapeHtml(sms.total != null ? `总 ${sms.total}` : '-')}</b></span>
|
||||
</div>
|
||||
<div class="instance-card-actions">
|
||||
<button type="button" class="mini-action" data-edit-id="${escapeHtml(item.id)}">编辑</button>
|
||||
<button type="button" class="mini-action danger" data-delete-id="${escapeHtml(item.id)}">删除</button>
|
||||
<div class="sim-card-foot">
|
||||
<span>${escapeHtml(value(network.operator || sim.operator || network.registration))}</span>
|
||||
<span>${escapeHtml(value(ota.currentVersion || device.firmware || device.revision))}</span>
|
||||
</div>
|
||||
</article>`
|
||||
}).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]) => `<div class="signal-item"><span>${k}</span><b>${v}</b></div>`).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 = `<section class="empty-state mini-empty"><h3>设备监控大屏</h3><p>选择左侧任一设备后,这里会按分类展示真实设备信息、温度、流量、系统资源、SIM 与短信状态。</p></section>`
|
||||
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 = `
|
||||
<button type="button" class="detail-action raw-action" data-view="rawpage">打开原始管理页</button>
|
||||
${primary.map(([label, ep]) => `<button type="button" class="detail-action"${disabled} data-detail-ep="${escapeHtml(ep)}"><span>${escapeHtml(label)}</span><code>${escapeHtml(ep)}</code></button>`).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()
|
||||
renderHomeCards()
|
||||
if (activeId) {
|
||||
renderOverview()
|
||||
renderFeatures()
|
||||
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 {
|
||||
|
||||
+57
-55
@@ -17,86 +17,95 @@
|
||||
<span class="leaf leaf-b">●</span>
|
||||
</div>
|
||||
|
||||
<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 id="appShell" class="sim-shell">
|
||||
<header class="sim-topbar island-card pattern-app-teal">
|
||||
<div>
|
||||
<p class="eyebrow">MULTI SIMADMIN</p>
|
||||
<h1>SimAdmin 设备卡片墙</h1>
|
||||
<p class="topbar-copy">首页直接展示每个 SimAdmin 设备卡片;点击卡片进入设备详情页,再进行状态查看和功能交互。</p>
|
||||
</div>
|
||||
<p class="sidebar-subtitle">像 NookPhone 一样管理每个 CPE 小岛</p>
|
||||
<div class="sidebar-actions">
|
||||
<button id="homeBtn" class="animal-btn default-btn" type="button">返回首页</button>
|
||||
<div class="hero-actions">
|
||||
<button id="homeBtn" class="animal-btn default-btn" type="button">设备首页</button>
|
||||
<button id="refreshBtn" class="animal-btn primary-btn" type="button">刷新状态</button>
|
||||
<button id="addInstanceBtn" class="animal-btn primary-btn" type="button">+ 添加设备</button>
|
||||
<button id="densityBtn" class="animal-btn default-btn" type="button">切换密度</button>
|
||||
</div>
|
||||
<button id="addInstanceBtn" class="animal-btn primary-btn block-btn" type="button">+ 添加设备地址</button>
|
||||
</header>
|
||||
|
||||
<main class="sim-main">
|
||||
<section id="homeView" class="view active home-view" aria-label="SimAdmin 卡片首页">
|
||||
<section class="home-toolbar island-card pattern-default">
|
||||
<div>
|
||||
<div class="section-ribbon color-app-green">设备卡片墙</div>
|
||||
<p class="board-caption">每张卡片对应一个 SimAdmin;卡片上直接显示在线状态、SIM、信号、温度、流量和短信摘要。</p>
|
||||
</div>
|
||||
<div class="home-tools">
|
||||
<section class="wallet-row" id="fleetStats"></section>
|
||||
|
||||
<label class="search-pill" aria-label="搜索设备">
|
||||
<span>⌕</span>
|
||||
<input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址…" autocomplete="off" />
|
||||
<input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址、ICCID…" autocomplete="off" />
|
||||
</label>
|
||||
|
||||
<nav class="island-tabs" id="statusFilters" aria-label="状态筛选">
|
||||
<button class="tab-leaf active" data-filter="all">全部</button>
|
||||
<button class="tab-leaf" data-filter="online">在线</button>
|
||||
<button class="tab-leaf" data-filter="auth">需登录</button>
|
||||
<button class="tab-leaf" data-filter="offline">离线</button>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
<section id="homeDeviceGrid" class="home-device-grid" aria-label="设备卡片列表"></section>
|
||||
</section>
|
||||
|
||||
<div id="instances" class="island-list"></div>
|
||||
<footer class="config-note">
|
||||
<span>本地配置</span>
|
||||
<code id="configPath">config.json</code>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<main class="nook-workspace">
|
||||
<header class="workspace-hero island-card pattern-app-teal">
|
||||
<section id="detailView" class="view detail-view" aria-label="设备详情页">
|
||||
<header class="workspace-hero island-card pattern-app-teal detail-hero">
|
||||
<div>
|
||||
<p id="activeKicker" class="eyebrow">NO ISLAND SELECTED</p>
|
||||
<h1 id="activeName">选择一个 SimAdmin 小岛</h1>
|
||||
<p id="activeMeta">集中展示设备信息、联网状态、SIM、信号、温度、流量、系统资源、短信与版本。</p>
|
||||
<p id="activeKicker" class="eyebrow">DEVICE DETAIL</p>
|
||||
<h1 id="activeName">选择一个 SimAdmin 设备</h1>
|
||||
<p id="activeMeta">从首页设备卡片进入详情页后,可查看完整状态并使用功能入口。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button id="backToFleetBtn" type="button" class="animal-btn default-btn">← 返回卡片墙</button>
|
||||
<button id="loginBtn" type="button" class="animal-btn default-btn" disabled>登录/刷新会话</button>
|
||||
<a id="openExternal" href="#" target="_blank" rel="noreferrer" class="animal-btn default-btn disabled">原站打开</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="phone-dock" aria-label="主视图">
|
||||
<button class="phone-app active app-teal" data-view="dashboard"><span>📡</span><b>设备监控</b></button>
|
||||
<button class="phone-app app-blue" data-view="rawpage"><span>↗</span><b>原页</b></button>
|
||||
<button class="phone-app app-yellow" data-view="api"><span>{}</span><b>调试接口</b></button>
|
||||
<button id="densityBtn" class="phone-app app-green" type="button"><span>↕</span><b>密度</b></button>
|
||||
</nav>
|
||||
|
||||
<section id="dashboardView" class="view active dashboard-view device-dashboard" aria-label="设备监控大屏">
|
||||
<section class="detail-layout">
|
||||
<section class="signal-board island-card pattern-app-yellow monitor-summary-card">
|
||||
<div class="section-ribbon color-app-yellow">实时状态</div>
|
||||
<div id="signalPanel" class="signal-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="overview" class="nook-metrics monitor-kpis" aria-label="关键状态指标"></section>
|
||||
|
||||
<section class="module-board island-card pattern-default device-detail-board">
|
||||
<div class="board-head">
|
||||
<div class="section-ribbon color-app-green">设备详情</div>
|
||||
<p class="board-caption">按设备、SIM、蜂窝网络、系统资源、温度、流量和短信分类展示真实状态。</p>
|
||||
<p class="board-caption">设备、SIM、蜂窝网络、系统资源、温度、流量、短信与版本。</p>
|
||||
</div>
|
||||
<div id="featureGrid" class="device-detail-grid"></div>
|
||||
<div id="detailDeviceGrid" class="device-detail-grid"></div>
|
||||
</section>
|
||||
<section class="island-card pattern-purple detail-actions-card">
|
||||
<div class="section-ribbon color-purple">功能交互</div>
|
||||
<div id="detailActions" class="detail-actions"></div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="rawpageView" class="view rawpage-view">
|
||||
<div class="frame-toolbar island-card pattern-app-blue">
|
||||
<button type="button" class="animal-btn default-btn" data-view="detail">← 返回详情</button>
|
||||
<span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span>
|
||||
</div>
|
||||
<section id="emptyState" class="empty-state island-card pattern-default">
|
||||
<div class="section-ribbon color-app-pink">等待选择</div>
|
||||
<h3>还没有选择设备</h3>
|
||||
<p>先在首页点击一个设备卡片,再进入原始页面。</p>
|
||||
</section>
|
||||
<iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe>
|
||||
</section>
|
||||
|
||||
<section id="apiView" class="view api-layout">
|
||||
<aside class="api-catalog island-card pattern-purple">
|
||||
<div class="section-ribbon color-purple">API 地图</div>
|
||||
<div class="section-ribbon color-purple">调试接口</div>
|
||||
<div id="endpointList" class="endpoint-list"></div>
|
||||
</aside>
|
||||
<section class="api-console island-card pattern-default">
|
||||
@@ -110,22 +119,15 @@
|
||||
<button id="runEndpoint" class="animal-btn primary-btn" type="button">发送</button>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="rawpageView" class="view rawpage-view">
|
||||
<div class="frame-toolbar island-card pattern-app-blue">
|
||||
<span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span>
|
||||
</div>
|
||||
<section id="emptyState" class="empty-state island-card pattern-default">
|
||||
<div class="section-ribbon color-app-pink">等待登岛</div>
|
||||
<h3>还没有选择设备</h3>
|
||||
<p>在左侧选择一个小岛后,这里会加载原 SimAdmin 页面。</p>
|
||||
</section>
|
||||
<iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="config-note sim-config-note">
|
||||
<span>本地配置</span>
|
||||
<code id="configPath">config.json</code>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<dialog id="loginDialog">
|
||||
|
||||
+7
-13
File diff suppressed because one or more lines are too long
@@ -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(/<section id="dashboardView"[\s\S]*?<\/section>\s*<section id="apiView"/)
|
||||
assert.ok(dashboardBlock, 'dashboardView block should exist before apiView')
|
||||
assert.doesNotMatch(dashboardBlock[0], /API 地图|endpointList|接口路径/)
|
||||
const homeBlock = html.match(/<section id="homeView"[\s\S]*?<\/section>\s*<section id="detailView"/)
|
||||
assert.ok(homeBlock, 'homeView should exist before detailView')
|
||||
assert.doesNotMatch(homeBlock[0], /API 地图|endpointList|接口路径|rawOutput/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user