111 lines
24 KiB
JavaScript
111 lines
24 KiB
JavaScript
import { escapeHtml, value, formatBytes } from './domain/formatters.js'
|
||
import { createApiClient, normalizeTargetUrl } from './infrastructure/api-client.js'
|
||
import { RequestCoordinator } from './state/request-coordinator.js'
|
||
import { filterAndSortFleet, fleetStatusKind, emptyFleetReason } from './state/fleet-view-model.js'
|
||
import { instanceDraftPayload, requestDraft, isWriteMethod } from './state/operation-draft.js'
|
||
import { createViewRouter, resolveView } from './router/view-router.js'
|
||
|
||
let instances = [], statuses = new Map(), catalog = [], activeId = null, activeFilter = 'all', activeSort = 'name'
|
||
let lastSuccessfulRefresh = null, currentView = 'home', outputText = '', outputPrettyText = '', pendingDeleteId = null, pendingApiRequest = null
|
||
let instanceDialogEpoch = 0, deleteDialogEpoch = 0, loginDialogEpoch = 0
|
||
const $ = id => document.getElementById(id), $$ = (selector, root = document) => [...root.querySelectorAll(selector)]
|
||
const requests = new RequestCoordinator(), api = createApiClient()
|
||
const routeView = createViewRouter({ controls: () => $$('[data-view]'), panels: () => $$('.view') })
|
||
const modules = [
|
||
{ name: '设备与网络', endpoints: ['/api/device','/api/sim','/api/network','/api/stats','/api/connectivity','/api/data','/api/roaming','/api/airplane-mode','/api/radio-mode','/api/band-lock','/api/cell-lock','/api/apn'] },
|
||
{ name: '短信与通话', endpoints: ['/api/sms/stats','/api/sms/list','/api/sms/conversation','/api/sms/send','/api/calls','/api/call/history'] },
|
||
{ name: '系统服务', endpoints: ['/api/work-mode','/api/esim/config','/api/notifications/config','/api/automation/config','/api/ota/status'] },
|
||
]
|
||
const firstValue = (...items) => items.find(x => x !== undefined && x !== null && x !== '') ?? null
|
||
const activeInstance = () => instances.find(x => x.id === activeId)
|
||
const activeStatus = () => statuses.get(activeId)
|
||
const tagsFromInput = text => String(text || '').split(',').map(x => x.trim()).filter(Boolean)
|
||
const statusText = s => ({ online: `在线${s?.latencyMs != null ? ` · ${s.latencyMs}ms` : ''}`, auth: '需登录', offline: '离线', unknown: '未知' })[fleetStatusKind(s)]
|
||
const showToast = message => { $('toast').textContent = message; $('toast').hidden = false; clearTimeout(showToast.timer); showToast.timer = setTimeout(() => { $('toast').hidden = true }, 3000) }
|
||
function persistentError(message = '') { $('persistentErrorText').textContent = message; $('persistentError').hidden = !message }
|
||
function setPending(button, pending, label) { button.disabled = pending; if (label) button.textContent = pending ? label : button.dataset.idleLabel }
|
||
function invalidateDialog(id) {
|
||
if (id === 'instanceDialog') { instanceDialogEpoch += 1; setPending($('saveInstanceBtn'), false, '保存中…') }
|
||
if (id === 'deleteDialog') { deleteDialogEpoch += 1; setPending($('confirmDeleteBtn'), false, '删除中…') }
|
||
if (id === 'loginDialog') { loginDialogEpoch += 1; requests.invalidate('login'); $('submitLogin').disabled = false; $('passwordInput').value = '' }
|
||
if (id === 'apiDeleteDialog') pendingApiRequest = null
|
||
}
|
||
function closeDialog(id) { invalidateDialog(id); const dialog=$(id); if(dialog.open)dialog.close() }
|
||
function frameClear() { $('frame').hidden = true; $('frame').removeAttribute('src') }
|
||
function switchView(requested) {
|
||
const view = resolveView(requested, Boolean(activeId)); currentView = view
|
||
if (view !== 'rawpage') frameClear()
|
||
if (view === 'rawpage') { const item = activeInstance(); $('frame').src = item.url; $('frame').hidden = false }
|
||
routeView(view)
|
||
const item = activeInstance(), titles = { home: '设备首页', detail: item ? `${item.name || item.id} · 设备详情` : '设备详情', rawpage: item ? `${item.name || item.id} · 原始页面` : '原始页面', api: item ? `${item.name || item.id} · API 工作台` : 'API 工作台' }
|
||
$('pageHeading').textContent = titles[view]; $('pageSubtitle').textContent = view === 'home' ? '集中巡检你的 SimAdmin 设备' : item?.url || ''
|
||
document.title = `${titles[view]} · Multi SimAdmin Island`
|
||
}
|
||
function formatSpeed(speed) {
|
||
if (!speed || typeof speed !== 'object') return '-'
|
||
if (Array.isArray(speed.interfaces)) return speed.interfaces.map(x => `${x.interface || 'net'} ↓ ${formatBytes(firstValue(x.rx_bytes_per_sec,x.rx))}/s · ↑ ${formatBytes(firstValue(x.tx_bytes_per_sec,x.tx))}/s`).join(' | ')
|
||
return `↓ ${formatBytes(firstValue(speed.rx,speed.download,speed.rx_bytes_per_sec))}/s · ↑ ${formatBytes(firstValue(speed.tx,speed.upload,speed.tx_bytes_per_sec))}/s`
|
||
}
|
||
function formatTemperature(temp) { if (temp == null || temp === '') return '-'; if (typeof temp === 'object') return Object.entries(temp).map(([k,v]) => `${k}: ${v}${typeof v === 'number' ? '℃' : ''}`).join(' · '); return `${temp}${typeof temp === 'number' ? '℃' : ''}` }
|
||
function renderFleetStats() {
|
||
const counts = { all: instances.length, online: 0, auth: 0, offline: 0, unknown: 0 }; for (const item of instances) counts[fleetStatusKind(statuses.get(item.id))]++
|
||
$('fleetStats').innerHTML = Object.entries({ all:'全部', online:'在线', auth:'需登录', offline:'离线', unknown:'未知' }).map(([k,label]) => `<button type="button" data-stat-filter="${k}" aria-pressed="${activeFilter === k}"><b>${counts[k]}</b><span>${label}</span></button>`).join('')
|
||
$$('[data-stat-filter]').forEach(btn => btn.addEventListener('click', () => setFilter(btn.dataset.statFilter)))
|
||
}
|
||
function setFilter(filter) { activeFilter = filter; $$('[data-filter]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.filter === filter))); renderHomeCards() }
|
||
function deviceSummary(item) { const s = statuses.get(item.id), sum = s?.summary || {}; return { s, device:sum.device||{}, sim:sum.sim||{}, network:sum.network||{}, data:sum.data||{}, sms:sum.sms||{}, system:sum.system||{}, ota:sum.ota||{} } }
|
||
function renderHomeCards() {
|
||
renderFleetStats(); const query = $('instanceSearch').value, list = filterAndSortFleet(instances, statuses, { filter:activeFilter, query, sort:activeSort }), reason = emptyFleetReason({ total:instances.length, query, filter:activeFilter, visible:list.length })
|
||
if (reason) { const copy = { config:['未配置设备','添加第一个 SimAdmin 地址,开始建设你的设备岛。'], search:['搜索无结果','没有设备匹配当前搜索词,请修改或清空搜索。'], filter:['筛选无结果','当前状态下没有设备,可切换到“全部”。'] }[reason]; $('homeDeviceGrid').innerHTML = `<section class="empty-state"><h2>${copy[0]}</h2><p>${copy[1]}</p></section>`; return }
|
||
$('homeDeviceGrid').innerHTML = list.map(item => { const { s,device,sim,network,data,sms,system,ota } = deviceSummary(item); return `<article class="sim-device-card ${fleetStatusKind(s)}"><div class="sim-card-head"><div><p class="eyebrow">${escapeHtml(item.id)}</p><h2>${escapeHtml(item.name || item.id)}</h2></div><span class="status-pill">${escapeHtml(statusText(s))}</span></div><p class="instance-url">${escapeHtml(item.url)}</p><p>${escapeHtml(item.description || '暂无描述')}</p><div class="tag-row">${(item.tags||[]).map(tag=>`<span>${escapeHtml(tag)}</span>`).join('')}</div><div class="sim-card-model">${escapeHtml([device.manufacturer,device.model].filter(Boolean).join(' ') || device.imei || '未知设备')}</div><dl class="sim-card-kpis"><div><dt>ICCID / SIM</dt><dd>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</dd></div><div><dt>信号</dt><dd>${escapeHtml(value(firstValue(network.signal,sim.signal)))}</dd></div><div><dt>温度</dt><dd>${escapeHtml(formatTemperature(system.temperature))}</dd></div><div><dt>实时流量</dt><dd>${escapeHtml(formatSpeed(system.networkSpeed))}</dd></div><div><dt>短信</dt><dd>${escapeHtml(sms.total != null ? `总 ${sms.total}` : '-')}</dd></div><div><dt>版本 / Commit</dt><dd>${escapeHtml(value(ota.currentVersion || device.firmware || device.revision))}${ota.currentCommit ? ` · ${escapeHtml(ota.currentCommit)}` : ''}</dd></div></dl><div class="card-actions"><button class="animal-btn primary-btn" type="button" data-open-detail="${escapeHtml(item.id)}">查看详情</button><button class="animal-btn default-btn" type="button" data-edit-instance="${escapeHtml(item.id)}">编辑设备</button></div></article>` }).join('')
|
||
$$('[data-open-detail]').forEach(b => b.addEventListener('click', () => openDeviceDetail(b.dataset.openDetail))); $$('[data-edit-instance]').forEach(b => b.addEventListener('click', () => openInstanceDialog(b.dataset.editInstance)))
|
||
}
|
||
function detailCard(title, rows) { return `<article class="device-info-card"><h4>${escapeHtml(title)}</h4><dl>${rows.map(([k,v])=>`<div><dt>${escapeHtml(k)}</dt><dd>${escapeHtml(value(v))}</dd></div>`).join('')}</dl></article>` }
|
||
function renderDeviceDetails() { renderDetail() }
|
||
function renderDetail() {
|
||
const item=activeInstance(), s=activeStatus(); if (!item) return; const {device,sim,network,data,sms,system,ota}=deviceSummary(item)
|
||
$('activeKicker').textContent=`${item.id} · ${statusText(s)}`; $('activeName').textContent=item.name||item.id; $('activeMeta').textContent=`${item.url}${item.description ? ` · ${item.description}` : ''}`
|
||
const signal=[['状态',statusText(s)],['运营商',network.operator||sim.operator],['信号',firstValue(network.signal,sim.signal)],['温度',formatTemperature(system.temperature)],['流量',formatSpeed(system.networkSpeed)],['数据连接',value(firstValue(data.active,data.connected,data.enabled))]]; $('signalPanel').innerHTML=signal.map(([k,v])=>`<div><span>${k}</span><b>${escapeHtml(value(v))}</b></div>`).join('')
|
||
$('overview').innerHTML=[['延迟',s?.latencyMs!=null?`${s.latencyMs}ms`:'-'],['SIM',sim.present===false?'未插卡':sim.iccid],['短信',sms.total],['版本',ota.currentVersion||device.firmware]].map(([k,v])=>`<div class="metric"><span>${k}</span><b>${escapeHtml(value(v))}</b></div>`).join('')
|
||
$('detailDeviceGrid').innerHTML=[detailCard('设备信息',[['名称',item.name],['ID',item.id],['地址',item.url],['型号',device.model],['厂商',device.manufacturer],['IMEI',device.imei],['版本',ota.currentVersion||device.firmware],['Commit',ota.currentCommit]]),detailCard('SIM 卡',[['ICCID',sim.iccid],['IMSI',sim.imsi],['号码',sim.phoneNumber],['短信中心',sim.smsCenter]]),detailCard('蜂窝网络',[['运营商',network.operator],['注册状态',network.registration],['网络制式',network.accessTechnology],['信号',network.signal]]),detailCard('系统资源',[['温度',formatTemperature(system.temperature)],['CPU',Array.isArray(system.cpuLoad)?system.cpuLoad.join(' / '):system.cpuLoad],['内存',JSON.stringify(system.memory||{})],['磁盘',JSON.stringify(system.disk||{})]])].join('')
|
||
const eps=['/api/device','/api/sim','/api/network','/api/stats','/api/sms/stats','/api/ota/status']; $('detailActions').innerHTML=`<button type="button" data-view="rawpage">打开原始管理页</button>${eps.map(ep=>`<button type="button" data-detail-ep="${ep}"><span>在工作台打开</span><code>${ep}</code></button>`).join('')}`; $$('[data-detail-ep]').forEach(b=>b.addEventListener('click',()=>{ switchView('api'); $('endpointInput').value=b.dataset.detailEp; $('methodSelect').value='GET'; updateWriteUI() })); $$('[data-view]',$('detailActions')).forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)))
|
||
}
|
||
function openDeviceDetail(id) { if (!instances.some(x=>x.id===id)) return; if(activeId!==id){ requests.invalidate('api-console'); requests.invalidate('login'); loginDialogEpoch += 1; $('submitLogin').disabled=false; pendingApiRequest=null; $('runEndpoint').disabled=false; $('runEndpoint').textContent='发送请求'; frameClear(); clearApiOutput() } activeId=id; localStorage.setItem('multi-simadmin-active',id); const item=activeInstance(); $('openExternal').href=item.url; renderDetail(); switchView('detail') }
|
||
function setActiveHome() { requests.invalidate('api-console'); requests.invalidate('login'); loginDialogEpoch += 1; $('submitLogin').disabled=false; pendingApiRequest=null; $('runEndpoint').disabled=false; $('runEndpoint').textContent='发送请求'; activeId=null; localStorage.removeItem('multi-simadmin-active'); frameClear(); switchView('home'); renderHomeCards() }
|
||
function openInstanceDialog(id='') { instanceDialogEpoch += 1; const item=instances.find(x=>x.id===id); $('instanceDialogTitle').textContent=item?'编辑设备':'添加设备'; $('instanceOriginalId').value=item?.id||''; $('instanceIdInput').value=item?.id||''; $('instanceNameInput').value=item?.name||''; $('instanceUrlInput').value=item?.url||''; $('instanceDescInput').value=item?.description||''; $('instanceTagsInput').value=(item?.tags||[]).join(', '); $('instancePasswordInput').value=''; $('clearPasswordConfirm').checked=false; $('passwordPreserve').checked=Boolean(item); $('passwordClear').checked=!item; $('passwordPreserve').disabled=!item; $('deleteInstanceBtn').hidden=!item; $('instanceFormError').hidden=true; updatePasswordUI(); $('instanceDialog').showModal() }
|
||
function updatePasswordUI(){ const action=$('instanceForm').elements.passwordAction.value; $('newPasswordLabel').hidden=action!=='set'; $('instancePasswordInput').required=action==='set'; $('clearPasswordLabel').hidden=action!=='clear' || !$('instanceOriginalId').value }
|
||
async function saveInstanceFromForm(){ const epoch=instanceDialogEpoch, editing=Boolean($('instanceOriginalId').value), btn=$('saveInstanceBtn'); if(btn.disabled)return; btn.dataset.idleLabel='保存'; setPending(btn,true,'保存中…'); $('instanceFormError').hidden=true; try { const payload=instanceDraftPayload({ id:$('instanceIdInput').value,name:$('instanceNameInput').value,url:normalizeTargetUrl($('instanceUrlInput').value),description:$('instanceDescInput').value,tags:tagsFromInput($('instanceTagsInput').value),passwordAction:$('instanceForm').elements.passwordAction.value,password:$('instancePasswordInput').value,clearConfirmed:$('clearPasswordConfirm').checked },editing); const data=editing?await api.updateInstance($('instanceOriginalId').value,payload):await api.createInstance(payload); if(epoch!==instanceDialogEpoch)return; $('instanceDialog').close(); await reload({ selectId:data.instance?.id||payload.id }); showToast(editing?'设备已更新':'设备已添加') } catch(error){ if(epoch!==instanceDialogEpoch)return; $('instanceFormError').textContent=error.message; $('instanceFormError').hidden=false } finally { if(epoch===instanceDialogEpoch)setPending(btn,false,'保存中…') } }
|
||
function openDeleteDialog(){ const item=instances.find(x=>x.id===$('instanceOriginalId').value); deleteDialogEpoch += 1; if(!item)return; pendingDeleteId=item.id; $('deleteDetails').innerHTML=`<dl><div><dt>名称</dt><dd>${escapeHtml(item.name||item.id)}</dd></div><div><dt>ID</dt><dd>${escapeHtml(item.id)}</dd></div><div><dt>URL</dt><dd>${escapeHtml(item.url)}</dd></div></dl>`; $('deleteError').hidden=true; $('deleteDialog').showModal() }
|
||
async function confirmDelete(){ const epoch=deleteDialogEpoch, btn=$('confirmDeleteBtn'); if(btn.disabled)return; btn.dataset.idleLabel='确认删除'; setPending(btn,true,'删除中…'); try{ await api.deleteInstance(pendingDeleteId); if(epoch!==deleteDialogEpoch)return; $('deleteDialog').close(); $('instanceDialog').close(); await reload({selectId:null}); setActiveHome(); showToast('设备已删除') }catch(error){if(epoch!==deleteDialogEpoch)return;$('deleteError').textContent=error.message;$('deleteError').hidden=false}finally{if(epoch===deleteDialogEpoch)setPending(btn,false,'删除中…')} }
|
||
function renderEndpointList(){ const q=$('endpointSearch').value.trim().toLowerCase(), groups=catalog.length?catalog:modules; $('endpointList').innerHTML=groups.map(g=>{ const eps=(g.endpoints||[]).filter(ep=>ep.toLowerCase().includes(q)); return eps.length?`<section><h3>${escapeHtml(g.name||g.id)}</h3>${eps.map(ep=>`<button type="button" class="endpoint-row" data-ep="${escapeHtml(ep)}">${escapeHtml(ep)}</button>`).join('')}</section>`:'' }).join('')||'<p>没有匹配接口</p>'; $$('.endpoint-row').forEach(b=>b.addEventListener('click',()=>{ $('endpointInput').value=b.dataset.ep; $('methodSelect').value='GET'; updateWriteUI(); $$('.endpoint-row').forEach(x=>x.setAttribute('aria-pressed',String(x===b))) })) }
|
||
function updateWriteUI(){ const write=isWriteMethod($('methodSelect').value); $('writeWarning').hidden=!write; $('requestBody').disabled=$('methodSelect').value==='GET' }
|
||
function clearApiOutput(){ outputText='';outputPrettyText='';$('rawOutput').textContent='设备已切换。选择接口并点击“发送请求”。' }
|
||
function parseBody(){ const raw=$('requestBody').value.trim(); return raw?JSON.parse(raw):undefined }
|
||
async function runEndpoint(confirmed=false){
|
||
if(!activeId)return
|
||
let draft
|
||
try {
|
||
if(confirmed){ draft=pendingApiRequest; pendingApiRequest=null; if(!draft||draft.owner!==activeId)return }
|
||
else {
|
||
const method=$('methodSelect').value, endpoint=$('endpointInput').value.trim()
|
||
if(!endpoint.startsWith('/')){persistentError('接口路径必须以 / 开头');return}
|
||
const body=method==='GET'?undefined:parseBody(); draft={...requestDraft(method,endpoint,body),owner:activeId,body}
|
||
if(isWriteMethod(method)){
|
||
pendingApiRequest=draft
|
||
$('apiDeleteDetails').textContent=`${activeInstance().name||activeId} (${activeId}) · ${method} ${endpoint}`
|
||
$('apiDeleteBody').textContent=body===undefined?'(无请求体)':JSON.stringify(body,null,2)
|
||
$('apiDeleteDialog').showModal(); return
|
||
}
|
||
}
|
||
} catch(error){ persistentError(`请求参数无效:${error.message||error}`); return }
|
||
const {owner,method,path:endpoint,body}=draft, btn=$('runEndpoint'), token=requests.next('api-console'); if(btn.disabled)return
|
||
btn.dataset.idleLabel='发送请求'; setPending(btn,true,'发送中…'); $('rawOutput').textContent='请求进行中…'; persistentError(''); const started=performance.now()
|
||
try{ const init={method}; if(isWriteMethod(method)){ const prepared=await api.prepareConfirmation({instanceId:owner,method,path:endpoint,body}); if(!requests.isCurrent('api-console',token)||owner!==activeId)return; init.headers={'content-type':'application/json','x-confirmation-token':prepared.token}; if(body!==undefined)init.body=JSON.stringify(body) } const res=await api.proxy(owner,endpoint,init), text=await res.text(); if(!requests.isCurrent('api-console',token)||owner!==activeId)return; const elapsed=Math.round(performance.now()-started); outputText=text; try{outputPrettyText=JSON.stringify(JSON.parse(text),null,2)}catch{outputPrettyText=text} $('rawOutput').textContent=`HTTP ${res.status} ${res.statusText} · ${elapsed}ms · ${draft.method} ${draft.path}\n\n${$('outputPretty').getAttribute('aria-pressed')==='true'?outputPrettyText:outputText}`; if(!res.ok)persistentError(`API 请求失败:HTTP ${res.status} ${res.statusText}\n${text}`) }catch(error){ if(requests.isCurrent('api-console',token)&&owner===activeId){$('rawOutput').textContent=error.message||String(error);persistentError(`API 请求失败:${error.message||error}`)} }finally{ if(requests.isCurrent('api-console',token)) setPending(btn,false,'发送中…') }
|
||
}
|
||
async function refreshStatus({silent=false}={}){ const btn=$('refreshBtn'), token=requests.next('fleet-status'); btn.disabled=true; try{const data=await api.status(); if(!requests.isCurrent('fleet-status',token))return; statuses=new Map((data.instances||[]).map(x=>[x.id,x]));lastSuccessfulRefresh=new Date();$('staleBanner').hidden=true;persistentError('');renderHomeCards();if(activeId)renderDetail();if(!silent)showToast(`已刷新 ${statuses.size} 个设备`)}catch(error){if(!requests.isCurrent('fleet-status',token))return;$('staleBanner').textContent=`刷新失败,当前显示上次成功数据。上次成功:${lastSuccessfulRefresh?lastSuccessfulRefresh.toLocaleString():'尚无'}。${error.message}`;$('staleBanner').hidden=false;persistentError(`状态刷新失败:${error.message}`)}finally{if(requests.isCurrent('fleet-status',token))btn.disabled=false} }
|
||
async function loadConfig(){ const [data,cat]=await Promise.all([api.config(),api.catalog().catch(()=>({groups:[]}))]);instances=data.instances||[];$('configPath').textContent=data.configPath||'config.json';catalog=cat.groups||[];renderEndpointList();const saved=localStorage.getItem('multi-simadmin-active');activeId=saved&&instances.some(x=>x.id===saved)?saved:null;renderHomeCards() }
|
||
async function reload({selectId=activeId}={}){await loadConfig();await refreshStatus({silent:true});if(selectId&&instances.some(x=>x.id===selectId))openDeviceDetail(selectId);else setActiveHome()}
|
||
async function initialize(){ $('initialLoading').hidden=false;$('fatalState').hidden=true;$('homeContent').hidden=true;persistentError('');try{await loadConfig();await refreshStatus({silent:true});$('initialLoading').hidden=true;$('homeContent').hidden=false;if(activeId){renderDetail();switchView(resolveView(localStorage.getItem('multi-simadmin-view')||'home',true))}else switchView('home')}catch(error){$('initialLoading').hidden=true;$('fatalText').textContent=error.message;$('fatalState').hidden=false} }
|
||
function bindEvents(){ $$('[data-dialog-cancel]').forEach(button=>button.addEventListener('click',()=>closeDialog(button.dataset.dialogCancel))); for(const id of ['instanceDialog','deleteDialog','loginDialog','apiDeleteDialog']) $(id).addEventListener('cancel',()=>invalidateDialog(id)); $('refreshBtn').addEventListener('click',()=>refreshStatus());$('addInstanceBtn').addEventListener('click',()=>openInstanceDialog());$('backToFleetBtn').addEventListener('click',setActiveHome);$('editActiveBtn').addEventListener('click',()=>openInstanceDialog(activeId));$('instanceSearch').addEventListener('input',renderHomeCards);$('sortSelect').addEventListener('change',()=>{activeSort=$('sortSelect').value;renderHomeCards()});$('densityBtn').addEventListener('click',()=>{const compact=document.body.classList.toggle('compact');$('densityBtn').setAttribute('aria-pressed',String(compact))});$$('[data-filter]').forEach(b=>b.addEventListener('click',()=>setFilter(b.dataset.filter)));$$('[data-view]').forEach(b=>b.addEventListener('click',e=>{e.preventDefault();switchView(b.dataset.view)}));$$('[name=passwordAction]').forEach(r=>r.addEventListener('change',updatePasswordUI));$('instanceForm').addEventListener('submit',e=>{e.preventDefault();saveInstanceFromForm()});$('deleteInstanceBtn').addEventListener('click',openDeleteDialog);$('confirmDeleteBtn').addEventListener('click',e=>{e.preventDefault();confirmDelete()});$('endpointSearch').addEventListener('input',renderEndpointList);$('methodSelect').addEventListener('change',updateWriteUI);$('runEndpoint').addEventListener('click',()=>runEndpoint());$('confirmApiDelete').addEventListener('click',e=>{e.preventDefault();$('apiDeleteDialog').close();runEndpoint(true)});$('outputPretty').addEventListener('click',()=>{$('outputPretty').setAttribute('aria-pressed','true');$('outputRaw').setAttribute('aria-pressed','false');if(outputText)$('rawOutput').textContent=$('rawOutput').textContent.split('\n\n')[0]+'\n\n'+outputPrettyText});$('outputRaw').addEventListener('click',()=>{$('outputPretty').setAttribute('aria-pressed','false');$('outputRaw').setAttribute('aria-pressed','true');if(outputText)$('rawOutput').textContent=$('rawOutput').textContent.split('\n\n')[0]+'\n\n'+outputText});$('copyOutput').addEventListener('click',async()=>{try{await navigator.clipboard.writeText($('rawOutput').textContent);showToast('结果已复制')}catch{showToast('复制不可用,请手动选择文本')}});$('dismissError').addEventListener('click',()=>persistentError(''));$('retryInit').addEventListener('click',initialize);$('loginBtn').addEventListener('click',()=>{loginDialogEpoch+=1;$('submitLogin').disabled=false;$('loginDialog').dataset.instanceId=activeId;$('passwordInput').value='';$('loginError').textContent='';$('loginError').hidden=true;$('loginDialog').showModal()});$('loginForm').addEventListener('submit',async e=>{e.preventDefault();const epoch=loginDialogEpoch,id=$('loginDialog').dataset.instanceId,btn=$('submitLogin');btn.disabled=true;$('loginError').hidden=true;const password=$('passwordInput').value;const result=await requests.run('login',id,()=>api.login(id,password));$('passwordInput').value='';if(epoch!==loginDialogEpoch)return;if(result.accepted&&id===activeId){if(result.error){$('loginError').textContent=result.error.message;$('loginError').hidden=false}else if(!result.value.authenticated){$('loginError').textContent='登录失败,请检查密码';$('loginError').hidden=false}else{$('loginDialog').close();await refreshStatus({silent:true});showToast('会话已刷新')}}if(epoch===loginDialogEpoch)btn.disabled=false});updateWriteUI() }
|
||
bindEvents(); await initialize()
|