490 lines
22 KiB
JavaScript
490 lines
22 KiB
JavaScript
let instances = []
|
|
let statuses = new Map()
|
|
let catalog = []
|
|
let activeId = null
|
|
let activeFilter = 'all'
|
|
let activeModule = 'all'
|
|
let configPath = 'config.json'
|
|
let autoRefreshTimer = null
|
|
|
|
const $ = (id) => document.getElementById(id)
|
|
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]
|
|
|
|
const modules = [
|
|
{ id: 'overview', title: '设备总览', desc: '设备、SIM、网络、连接状态与流量统计。适合日常巡检和资产核对。', tone: 'ok', endpoints: ['/api/device', '/api/sim', '/api/network', '/api/stats', '/api/connectivity'] },
|
|
{ id: 'sms', title: '短信中心', desc: '短信统计、列表、会话读取与短信发送。发送类接口请先在 API 工作台确认请求体。', tone: 'warn', endpoints: ['/api/sms/stats', '/api/sms/list', '/api/sms/conversation', '/api/sms/send'] },
|
|
{ id: 'network', title: '蜂窝网络', desc: '数据开关、漫游、飞行模式、无线制式、频段/小区锁定与 APN。', tone: 'danger', endpoints: ['/api/data', '/api/roaming', '/api/airplane-mode', '/api/radio-mode', '/api/band-lock', '/api/cell-lock', '/api/apn', '/api/cells'] },
|
|
{ id: 'device-network', title: '设备网络', desc: 'WLAN、DDNS、连接地址与局域网访问能力。', tone: 'neutral', endpoints: ['/api/device-network/wlan/status', '/api/device-network/wlan/profiles', '/api/device-network/ddns/status', '/api/device-network/ddns/config', '/api/network/connection-addresses'] },
|
|
{ id: 'calls', title: '电话能力', desc: '通话列表、历史记录、IMS 与语音信箱状态。', tone: 'neutral', endpoints: ['/api/calls', '/api/call/history', '/api/ims/status', '/api/voicemail/status'] },
|
|
{ id: 'esim', title: 'eSIM / 工作模式', desc: 'eUICC、profile、LPAC 状态、工作模式与 eSIM 配置。', tone: 'warn', endpoints: ['/api/work-mode', '/api/esim/config', '/api/esim/lpac/status', '/api/esim/euicc', '/api/esim/profiles'] },
|
|
{ id: 'notify-auto-ota', title: '通知 / 自动化 / OTA', desc: '通知配置与日志、自动化任务日志、在线升级状态。', tone: 'neutral', endpoints: ['/api/notifications/config', '/api/notifications/logs', '/api/automation/config', '/api/automation/logs', '/api/ota/status'] },
|
|
]
|
|
|
|
function escapeHtml(text) {
|
|
return String(text ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]))
|
|
}
|
|
function value(v) { return v === undefined || v === null || v === '' ? '-' : String(v) }
|
|
function boolText(v) {
|
|
if (v === true) return '开启'
|
|
if (v === false) return '关闭'
|
|
return value(v)
|
|
}
|
|
function activeInstance() { return instances.find(x => x.id === activeId) }
|
|
function activeStatus() { return statuses.get(activeId) }
|
|
function tagsFromInput(text) { return String(text || '').split(',').map(x => x.trim()).filter(Boolean) }
|
|
function instancePayloadFromForm() {
|
|
return {
|
|
id: $('instanceIdInput').value.trim(),
|
|
name: $('instanceNameInput').value.trim(),
|
|
url: $('instanceUrlInput').value.trim(),
|
|
description: $('instanceDescInput').value.trim(),
|
|
tags: tagsFromInput($('instanceTagsInput').value),
|
|
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'
|
|
if (s.authenticated === false) return 'auth'
|
|
return 'online'
|
|
}
|
|
function statusText(s) {
|
|
const kind = statusKind(s)
|
|
if (kind === 'online') return `在线${s.latencyMs != null ? ` ${s.latencyMs}ms` : ''}`
|
|
if (kind === 'auth') return '需登录'
|
|
if (kind === 'offline') return '离线'
|
|
return '未知'
|
|
}
|
|
function pillClass(s) {
|
|
const kind = statusKind(s)
|
|
if (kind === 'online') return 'ok'
|
|
if (kind === 'auth') return 'warn'
|
|
if (kind === 'offline') return 'bad'
|
|
return ''
|
|
}
|
|
function showToast(message, timeout = 2600) {
|
|
const toast = $('toast')
|
|
toast.textContent = message
|
|
toast.hidden = false
|
|
clearTimeout(showToast._timer)
|
|
showToast._timer = setTimeout(() => { toast.hidden = true }, timeout)
|
|
}
|
|
function flattenEndpoints() {
|
|
return [...new Set([
|
|
...catalog.flatMap(g => g.endpoints || []),
|
|
...modules.flatMap(m => m.endpoints || []),
|
|
])].sort()
|
|
}
|
|
function inferMethod(endpoint) {
|
|
if (/send|config|data$|roaming$|airplane-mode$|radio-mode$|band-lock$|cell-lock$|work-mode|login|logout/i.test(endpoint)) return 'POST'
|
|
return 'GET'
|
|
}
|
|
function proxiedUrl(endpoint) {
|
|
if (!activeId) return null
|
|
const normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
|
return `/api/proxy/${encodeURIComponent(activeId)}${normalized}`
|
|
}
|
|
|
|
function renderFleetStats() {
|
|
const total = instances.length
|
|
const counts = { online: 0, auth: 0, offline: 0, unknown: 0 }
|
|
for (const inst of instances) counts[statusKind(statuses.get(inst.id))] += 1
|
|
$('fleetStats').innerHTML = [
|
|
['全部', total], ['在线', counts.online], ['需登录', counts.auth], ['离线', counts.offline],
|
|
].map(([label, num]) => `<div class="fleet-card"><b>${num}</b><span>${label}</span></div>`).join('')
|
|
}
|
|
|
|
function filteredInstances() {
|
|
const q = $('instanceSearch')?.value.trim().toLowerCase() || ''
|
|
return instances.filter(item => {
|
|
const s = statuses.get(item.id)
|
|
const kind = statusKind(s)
|
|
if (activeFilter !== 'all' && kind !== activeFilter) return false
|
|
if (!q) return true
|
|
const haystack = [item.id, item.name, item.url, item.description, JSON.stringify(s?.summary || {})].join(' ').toLowerCase()
|
|
return haystack.includes(q)
|
|
})
|
|
}
|
|
|
|
function renderInstances() {
|
|
const root = $('instances')
|
|
renderFleetStats()
|
|
if (!instances.length) {
|
|
root.innerHTML = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。</p></section>'
|
|
return
|
|
}
|
|
const list = filteredInstances()
|
|
if (!list.length) {
|
|
root.innerHTML = '<section class="empty-state"><h3>没有匹配设备</h3><p>调整搜索关键字或状态筛选。</p></section>'
|
|
return
|
|
}
|
|
root.innerHTML = list.map(item => {
|
|
const s = statuses.get(item.id)
|
|
const sum = s?.summary || {}
|
|
const network = sum.network || {}
|
|
const sim = sum.sim || {}
|
|
return `<article class="instance-card ${item.id === activeId ? 'active' : ''}" data-id="${escapeHtml(item.id)}">
|
|
<div class="instance-top">
|
|
<div>
|
|
<div class="instance-name">${escapeHtml(item.name || item.id)}</div>
|
|
<div class="instance-url">${escapeHtml(item.url)}</div>
|
|
</div>
|
|
<span class="status-pill ${pillClass(s)}">${escapeHtml(statusText(s))}</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>
|
|
${item.description ? `<p class="instance-desc">${escapeHtml(item.description)}</p>` : ''}
|
|
<div class="mini-grid">
|
|
<span>认证<b>${item.auth?.hasPassword ? '密码会话' : '无密码'}</b></span>
|
|
<span>运营商<b>${escapeHtml(value(network.operator || sim.operator))}</b></span>
|
|
<span>网络<b>${escapeHtml(value(network.accessTechnology || network.registration))}</b></span>
|
|
<span>ICCID<b>${escapeHtml(value(sim.iccid))}</b></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)))
|
|
}
|
|
|
|
function renderHero() {
|
|
const item = activeInstance()
|
|
const s = activeStatus()
|
|
const sum = s?.summary || {}
|
|
if (!item) {
|
|
$('activeKicker').textContent = 'NO DEVICE SELECTED'
|
|
$('activeName').textContent = '选择一个 SimAdmin 实例'
|
|
$('activeMeta').textContent = '统一查看设备、SIM、网络、短信、eSIM、OTA 与原始管理页面。'
|
|
$('signalPanel').innerHTML = [
|
|
['状态', '等待选择'], ['当前实例', '-'], ['会话', '-'], ['刷新', '-'],
|
|
].map(([k, v]) => `<div class="signal-item"><span>${k}</span><b>${v}</b></div>`).join('')
|
|
return
|
|
}
|
|
$('activeKicker').textContent = item.id
|
|
$('activeName').textContent = item.name || item.id
|
|
$('activeMeta').textContent = item.description ? `${item.description} · ${item.url}` : item.url
|
|
$('signalPanel').innerHTML = [
|
|
['状态', statusText(s)],
|
|
['设备', value(sum.device?.model || sum.device?.name)],
|
|
['运营商', value(sum.network?.operator || sum.sim?.operator)],
|
|
['网络', value(sum.network?.accessTechnology || sum.network?.registration)],
|
|
['短信', sum.sms ? `总 ${value(sum.sms.total)} / 未读 ${value(sum.sms.unread)}` : '-'],
|
|
['版本', value(sum.ota?.currentVersion || sum.device?.firmwareVersion)],
|
|
].map(([k, v]) => `<div class="signal-item"><span>${escapeHtml(k)}</span><b>${escapeHtml(v)}</b></div>`).join('')
|
|
}
|
|
|
|
function renderOverview() {
|
|
const item = activeInstance()
|
|
const s = activeStatus()
|
|
if (!item) {
|
|
$('overview').innerHTML = '<div class="metric"><span>提示</span><b>先选择左侧设备</b></div>'
|
|
renderHero()
|
|
return
|
|
}
|
|
const sum = s?.summary || {}
|
|
const metrics = [
|
|
['状态', s ? (s.reachable ? (s.authenticated === false ? '在线/未登录' : '在线') : `离线`) : '未知'],
|
|
['延迟', s?.latencyMs != null ? `${s.latencyMs}ms` : '-'],
|
|
['型号', sum.device?.model || sum.device?.name],
|
|
['IMEI', sum.device?.imei],
|
|
['ICCID', sum.sim?.iccid],
|
|
['号码', sum.sim?.phoneNumber || sum.sim?.msisdn],
|
|
['运营商', sum.network?.operator || sum.sim?.operator],
|
|
['网络', sum.network?.accessTechnology || sum.network?.registration],
|
|
['数据连接', sum.data ? `${boolText(sum.data.enabled)} / ${boolText(sum.data.connected)}` : '-'],
|
|
['短信', sum.sms ? `总 ${value(sum.sms.total)} / 未读 ${value(sum.sms.unread)}` : '-'],
|
|
]
|
|
$('overview').innerHTML = metrics.map(([k, v]) => `<div class="metric"><span>${escapeHtml(k)}</span><b title="${escapeHtml(value(v))}">${escapeHtml(value(v))}</b></div>`).join('')
|
|
renderHero()
|
|
}
|
|
|
|
function renderModuleTabs() {
|
|
const tabs = [{ id: 'all', title: '全部' }, ...modules.map(m => ({ id: m.id, title: m.title.split(/[ /]/)[0] }))]
|
|
$('moduleTabs').innerHTML = tabs.map(t => `<button class="${t.id === activeModule ? 'active' : ''}" data-module="${escapeHtml(t.id)}">${escapeHtml(t.title)}</button>`).join('')
|
|
$$('button[data-module]', $('moduleTabs')).forEach(btn => btn.addEventListener('click', () => {
|
|
activeModule = btn.dataset.module
|
|
renderFeatures()
|
|
}))
|
|
}
|
|
|
|
function renderFeatures() {
|
|
renderModuleTabs()
|
|
const disabled = activeId ? '' : ' disabled'
|
|
const visible = activeModule === 'all' ? modules : modules.filter(m => m.id === activeModule)
|
|
$('featureGrid').innerHTML = visible.map(m => `<article class="feature">
|
|
<div class="feature-head">
|
|
<div><p class="eyebrow ${m.tone === 'danger' ? '' : 'accent'}">${escapeHtml(m.id)}</p><h4>${escapeHtml(m.title)}</h4></div>
|
|
<span class="status-pill ${m.tone === 'danger' ? 'bad' : m.tone === 'warn' ? 'warn' : m.tone === 'ok' ? 'ok' : ''}">${m.endpoints.length} API</span>
|
|
</div>
|
|
<p>${escapeHtml(m.desc)}</p>
|
|
${m.tone === 'danger' ? '<div class="danger-note">包含写操作/网络切换类接口,执行前确认目标设备。</div>' : ''}
|
|
<div class="endpoint-pills">${m.endpoints.map(ep => `<button${disabled} data-ep="${escapeHtml(ep)}">${escapeHtml(ep)}</button>`).join('')}</div>
|
|
</article>`).join('')
|
|
$$('button[data-ep]', $('featureGrid')).forEach(btn => btn.addEventListener('click', () => {
|
|
switchView('api')
|
|
$('endpointInput').value = btn.dataset.ep
|
|
$('methodSelect').value = inferMethod(btn.dataset.ep)
|
|
runEndpoint()
|
|
}))
|
|
}
|
|
|
|
function renderEndpointList() {
|
|
const groups = catalog.length ? catalog : modules.map(m => ({ id: m.id, name: m.title, endpoints: m.endpoints }))
|
|
$('endpointList').innerHTML = groups.map(group => `<section class="endpoint-group">
|
|
<h4>${escapeHtml(group.name || group.id)}</h4>
|
|
${(group.endpoints || []).map(ep => `<button class="endpoint-row" data-ep="${escapeHtml(ep)}"><span>${escapeHtml(inferMethod(ep))}</span> ${escapeHtml(ep)}</button>`).join('')}
|
|
</section>`).join('')
|
|
$$('.endpoint-row', $('endpointList')).forEach(row => row.addEventListener('click', () => {
|
|
$('endpointInput').value = row.dataset.ep
|
|
$('methodSelect').value = inferMethod(row.dataset.ep)
|
|
$$('.endpoint-row', $('endpointList')).forEach(x => x.classList.toggle('active', x === row))
|
|
}))
|
|
}
|
|
|
|
function switchView(view) {
|
|
$$('.phone-app[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view))
|
|
$$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`))
|
|
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 openInstanceDialog(id = '') {
|
|
const item = id ? instances.find(x => x.id === id) : null
|
|
$('instanceOriginalId').value = item?.id || ''
|
|
$('instanceFormRibbon').textContent = item ? '编辑设备地址' : '添加设备地址'
|
|
$('instanceIdInput').value = item?.id || ''
|
|
$('instanceNameInput').value = item?.name || ''
|
|
$('instanceUrlInput').value = item?.url || ''
|
|
$('instanceDescInput').value = item?.description || ''
|
|
$('instanceTagsInput').value = (item?.tags || []).join(', ')
|
|
$('instancePasswordInput').value = ''
|
|
$('instancePasswordInput').placeholder = item?.auth?.hasPassword ? '留空将清除已保存密码;输入新密码可替换' : '留空表示无密码'
|
|
$('deleteInstanceBtn').hidden = !item
|
|
$('instanceDialog').showModal()
|
|
setTimeout(() => (item ? $('instanceNameInput') : $('instanceIdInput')).focus(), 50)
|
|
}
|
|
|
|
async function reloadConfigAndRefresh({ selectId = activeId, toast = '' } = {}) {
|
|
await loadConfig({ preserveSelection: selectId })
|
|
await refreshStatus({ silent: true })
|
|
if (selectId && instances.some(x => x.id === selectId)) selectInstance(selectId)
|
|
else setActiveHome()
|
|
if (toast) showToast(toast)
|
|
}
|
|
|
|
async function saveInstanceFromForm() {
|
|
const originalId = $('instanceOriginalId').value
|
|
const payload = instancePayloadFromForm()
|
|
const method = originalId ? 'PUT' : 'POST'
|
|
const url = originalId ? `/api/instances/${encodeURIComponent(originalId)}` : '/api/instances'
|
|
const res = await fetch(url, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
|
|
const data = await res.json().catch(() => ({}))
|
|
if (!res.ok) throw new Error(data.error || `保存失败:HTTP ${res.status}`)
|
|
$('instanceDialog').close()
|
|
await reloadConfigAndRefresh({ selectId: data.instance?.id || payload.id, toast: originalId ? '设备地址已更新' : '设备地址已添加' })
|
|
}
|
|
|
|
async function deleteInstance(id) {
|
|
const item = instances.find(x => x.id === id)
|
|
if (!item) return
|
|
if (!confirm(`确定删除设备“${item.name || item.id}”?\n配置会立即写入 config.json。`)) return
|
|
const res = await fetch(`/api/instances/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
const data = await res.json().catch(() => ({}))
|
|
if (!res.ok) {
|
|
showToast(data.error || `删除失败:HTTP ${res.status}`, 4200)
|
|
return
|
|
}
|
|
await reloadConfigAndRefresh({ selectId: null, toast: '设备地址已删除' })
|
|
}
|
|
|
|
async function deleteCurrentInstanceFromDialog() {
|
|
const id = $('instanceOriginalId').value
|
|
if (!id) return
|
|
$('instanceDialog').close()
|
|
await deleteInstance(id)
|
|
}
|
|
|
|
async function loadConfig({ preserveSelection = activeId } = {}) {
|
|
const [configRes, catalogRes] = await Promise.all([fetch('/api/config'), fetch('/api/catalog')])
|
|
if (!configRes.ok) throw new Error(`配置读取失败:HTTP ${configRes.status}`)
|
|
const data = await configRes.json()
|
|
instances = data.instances || []
|
|
configPath = data.configPath || 'config.json'
|
|
$('configPath').textContent = configPath
|
|
if (catalogRes.ok) catalog = (await catalogRes.json()).groups || []
|
|
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)
|
|
}
|
|
|
|
async function refreshStatus({ silent = false } = {}) {
|
|
$('refreshBtn').disabled = true
|
|
if (!silent) showToast('正在刷新全部实例…', 1200)
|
|
try {
|
|
const res = await fetch('/api/status')
|
|
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()
|
|
if (!silent) showToast(`已刷新 ${statuses.size} 个实例`)
|
|
} catch (error) {
|
|
showToast(error.message || '刷新失败', 4200)
|
|
} finally {
|
|
$('refreshBtn').disabled = false
|
|
}
|
|
}
|
|
|
|
async function loginActive() {
|
|
if (!activeId) return
|
|
const item = activeInstance()
|
|
if (!item) return
|
|
// If config already has a saved password, try direct refresh first; otherwise ask for password.
|
|
if (item.auth?.hasPassword) {
|
|
showToast('正在使用本地配置密码刷新会话…', 1600)
|
|
const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) })
|
|
if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200)
|
|
await refreshStatus({ silent: true })
|
|
return
|
|
}
|
|
$('loginDialog').showModal()
|
|
setTimeout(() => $('passwordInput').focus(), 50)
|
|
}
|
|
|
|
function parseBody() {
|
|
const raw = $('requestBody').value.trim()
|
|
if (!raw) return undefined
|
|
return JSON.parse(raw)
|
|
}
|
|
|
|
async function runEndpoint() {
|
|
if (!activeId) {
|
|
showToast('请先选择左侧设备')
|
|
return
|
|
}
|
|
const endpoint = $('endpointInput').value.trim()
|
|
const method = $('methodSelect').value
|
|
if (!endpoint.startsWith('/')) {
|
|
showToast('接口路径必须以 / 开头')
|
|
return
|
|
}
|
|
$('rawOutput').textContent = '调用中…'
|
|
try {
|
|
const init = { method }
|
|
if (method !== 'GET') {
|
|
init.headers = { 'content-type': 'application/json' }
|
|
const body = parseBody()
|
|
if (body !== undefined) init.body = JSON.stringify(body)
|
|
}
|
|
const started = performance.now()
|
|
const res = await fetch(proxiedUrl(endpoint), init)
|
|
const elapsed = Math.round(performance.now() - started)
|
|
const text = await res.text()
|
|
let formatted = text
|
|
try { formatted = JSON.stringify(JSON.parse(text), null, 2) } catch {}
|
|
$('rawOutput').textContent = `HTTP ${res.status} · ${elapsed}ms · ${method} ${endpoint}\n\n${formatted}`
|
|
} catch (error) {
|
|
$('rawOutput').textContent = error.message || String(error)
|
|
showToast('接口调用失败', 4200)
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
$('refreshBtn').addEventListener('click', () => refreshStatus())
|
|
$('homeBtn').addEventListener('click', setActiveHome)
|
|
$('addInstanceBtn').addEventListener('click', () => openInstanceDialog())
|
|
$('loginBtn').addEventListener('click', loginActive)
|
|
$('runEndpoint').addEventListener('click', runEndpoint)
|
|
$('instanceSearch').addEventListener('input', renderInstances)
|
|
$('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)))
|
|
$$('#statusFilters .tab-leaf').forEach(btn => btn.addEventListener('click', () => {
|
|
activeFilter = btn.dataset.filter
|
|
$$('#statusFilters .tab-leaf').forEach(x => x.classList.toggle('active', x === btn))
|
|
renderInstances()
|
|
}))
|
|
$('loginForm').addEventListener('submit', async (event) => {
|
|
event.preventDefault()
|
|
if (!activeId) return
|
|
const password = $('passwordInput').value
|
|
$('passwordInput').value = ''
|
|
$('loginDialog').close()
|
|
const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password }) })
|
|
if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200)
|
|
else showToast('会话已刷新')
|
|
await refreshStatus({ silent: true })
|
|
})
|
|
$('instanceForm').addEventListener('submit', async (event) => {
|
|
event.preventDefault()
|
|
try {
|
|
await saveInstanceFromForm()
|
|
} catch (error) {
|
|
showToast(error.message || '保存失败', 5200)
|
|
}
|
|
})
|
|
$('deleteInstanceBtn').addEventListener('click', deleteCurrentInstanceFromDialog)
|
|
$('endpointInput').addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') runEndpoint()
|
|
})
|
|
}
|
|
|
|
function restorePrefs() {
|
|
if (localStorage.getItem('multi-simadmin-density') === 'compact') document.body.classList.add('compact')
|
|
switchView(localStorage.getItem('multi-simadmin-view') || 'dashboard')
|
|
}
|
|
|
|
try {
|
|
restorePrefs()
|
|
bindEvents()
|
|
await loadConfig()
|
|
await refreshStatus({ silent: true })
|
|
autoRefreshTimer = setInterval(() => refreshStatus({ silent: true }), 30000)
|
|
window.addEventListener('beforeunload', () => clearInterval(autoRefreshTimer))
|
|
} catch (error) {
|
|
console.error(error)
|
|
showToast(error.message || '初始化失败', 8000)
|
|
$('rawOutput').textContent = error.stack || String(error)
|
|
}
|