diff --git a/README.md b/README.md
index 4ead183..0bc2448 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
- 只需要在 `config.json` 配置多个 SimAdmin 地址。
- Animal Island / 动森风格 Dashboard:暖米色背景、棕色文字、Ribbon 飘带标题、NookPhone 应用格、胶囊按钮与波点卡片。
+- UI 内直接新增、编辑、删除设备地址,配置实时写入本地 `config.json`,并提供返回首页入口。
- 实例舰队列表、状态筛选、搜索、当前设备摘要。
- 支持无密码实例状态读取;有密码实例可通过本地服务代登录并保持会话。
- 统一代理 SimAdmin API,内置 API 工作台,便于读取/调试设备、SIM、网络、短信、eSIM、OTA 等接口。
diff --git a/public/app.js b/public/app.js
index 0b7edef..3b8fdf5 100644
--- a/public/app.js
+++ b/public/app.js
@@ -31,6 +31,31 @@ function boolText(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'
@@ -99,7 +124,7 @@ function renderInstances() {
const root = $('instances')
renderFleetStats()
if (!instances.length) {
- root.innerHTML = '未配置实例
请编辑 config.json 添加 instances,并重启服务。
'
+ root.innerHTML = '未配置设备
点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。
'
return
}
const list = filteredInstances()
@@ -120,6 +145,10 @@ function renderInstances() {
${escapeHtml(statusText(s))}
+
+
+
+
${item.description ? `${escapeHtml(item.description)}
` : ''}
认证${item.auth?.hasPassword ? '密码会话' : '无密码'}
@@ -129,7 +158,12 @@ function renderInstances() {
`
}).join('')
- $$('.instance-card', root).forEach(card => card.addEventListener('click', () => selectInstance(card.dataset.id)))
+ $$('.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() {
@@ -248,7 +282,63 @@ function selectInstance(id) {
renderFeatures()
}
-async function loadConfig() {
+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()
@@ -256,8 +346,8 @@ async function loadConfig() {
configPath = data.configPath || 'config.json'
$('configPath').textContent = configPath
if (catalogRes.ok) catalog = (await catalogRes.json()).groups || []
- const saved = localStorage.getItem('multi-simadmin-active')
- if (saved && instances.some(x => x.id === saved)) activeId = saved
+ const saved = preserveSelection || localStorage.getItem('multi-simadmin-active')
+ activeId = saved && instances.some(x => x.id === saved) ? saved : null
renderEndpointList()
renderInstances()
renderOverview()
@@ -340,6 +430,8 @@ async function runEndpoint() {
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)
@@ -364,6 +456,15 @@ function bindEvents() {
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()
})
diff --git a/public/index.html b/public/index.html
index 2f28228..f0c2e76 100644
--- a/public/index.html
+++ b/public/index.html
@@ -29,7 +29,11 @@
SimAdmin 岛屿
-
+
+
@@ -137,6 +141,25 @@
+
+