Refocus UI on device card details

This commit is contained in:
chick
2026-07-08 00:17:22 +08:00
parent 0e746f961c
commit 4f80e951d4
4 changed files with 224 additions and 196 deletions
+112 -91
View File
@@ -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()
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 {