Refocus UI on device card details
This commit is contained in:
+112
-91
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user