feat: add instance management to dashboard
This commit is contained in:
+106
-5
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user