feat: add instance management to dashboard

This commit is contained in:
chick
2026-07-07 23:04:32 +08:00
parent 182bd19c95
commit 2505ee561a
7 changed files with 267 additions and 12 deletions
+106 -5
View File
@@ -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 = '<section class="empty-state"><h3>未配置实例</h3><p>请编辑 config.json 添加 instances,并重启服务。</p></section>'
root.innerHTML = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。</p></section>'
return
}
const list = filteredInstances()
@@ -120,6 +145,10 @@ function renderInstances() {
</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>
@@ -129,7 +158,12 @@ function renderInstances() {
</div>
</article>`
}).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()
})