fix: 修复内存泄漏和添加保存按钮加载状态

- router.js: 防止 hashchange 监听器重复绑定
- sidebar.js: 用事件委托替代每次重新绑定事件
- logs.js: 搜索定时器提升为模块级变量,添加 cleanup 导出
- models/agents/gateway/mcp: 保存按钮添加 disabled + 加载文本
This commit is contained in:
晴天
2026-02-26 23:35:33 +08:00
parent 352b85405d
commit c2e3f738b5
7 changed files with 78 additions and 18 deletions

View File

@@ -43,6 +43,8 @@ const ICONS = {
deploy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
}
let _delegated = false
export function renderSidebar(el) {
const current = getCurrentRoute()
@@ -86,14 +88,22 @@ export function renderSidebar(el) {
el.innerHTML = html
// 绑定导航点击事件
el.querySelectorAll('.nav-item[data-route]').forEach(item => {
item.onclick = () => navigate(item.dataset.route)
})
// 主题切换
el.querySelector('#btn-theme-toggle')?.addEventListener('click', () => {
toggleTheme()
renderSidebar(el)
})
// 事件委托:只绑定一次,避免重复绑定
if (!_delegated) {
_delegated = true
el.addEventListener('click', (e) => {
// 导航点击
const navItem = e.target.closest('.nav-item[data-route]')
if (navItem) {
navigate(navItem.dataset.route)
return
}
// 主题切换
const themeBtn = e.target.closest('#btn-theme-toggle')
if (themeBtn) {
toggleTheme()
renderSidebar(el)
}
})
}
}

View File

@@ -22,7 +22,17 @@ export async function render() {
const state = { config: null }
await loadConfig(page, state)
page.querySelector('#btn-save-agent').onclick = () => saveConfig(page, state)
page.querySelector('#btn-save-agent').onclick = async () => {
const btn = page.querySelector('#btn-save-agent')
btn.disabled = true
btn.textContent = '保存中...'
try {
await saveConfig(page, state)
} finally {
btn.disabled = false
btn.textContent = '保存配置'
}
}
return page
}

View File

@@ -21,7 +21,17 @@ export async function render() {
const state = { config: null }
await loadConfig(page, state)
page.querySelector('#btn-save-gw').onclick = () => saveConfig(page, state)
page.querySelector('#btn-save-gw').onclick = async () => {
const btn = page.querySelector('#btn-save-gw')
btn.disabled = true
btn.textContent = '保存中...'
try {
await saveConfig(page, state)
} finally {
btn.disabled = false
btn.textContent = '保存配置'
}
}
return page
}

View File

@@ -12,6 +12,8 @@ const LOG_TABS = [
{ key: 'config-audit', label: '审计日志' },
]
let _searchTimer = null
export async function render() {
const page = document.createElement('div')
page.className = 'page'
@@ -48,10 +50,9 @@ export async function render() {
})
// 搜索
let searchTimer = null
page.querySelector('#log-search').addEventListener('input', (e) => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
clearTimeout(_searchTimer)
_searchTimer = setTimeout(() => {
if (e.target.value.trim()) {
searchLog(page, currentTab, e.target.value.trim())
} else {
@@ -67,6 +68,11 @@ export async function render() {
return page
}
export function cleanup() {
clearTimeout(_searchTimer)
_searchTimer = null
}
async function loadLog(page, logName) {
const el = page.querySelector('#log-content')
el.innerHTML = '<div style="color:var(--text-tertiary)">加载中...</div>'

View File

@@ -24,7 +24,17 @@ export async function render() {
const state = { config: null }
await loadConfig(page, state)
page.querySelector('#btn-save-mcp').onclick = () => saveConfig(state)
page.querySelector('#btn-save-mcp').onclick = async () => {
const btn = page.querySelector('#btn-save-mcp')
btn.disabled = true
btn.textContent = '保存中...'
try {
await saveConfig(state)
} finally {
btn.disabled = false
btn.textContent = '保存配置'
}
}
page.querySelector('#btn-add-mcp').onclick = () => addServer(page, state)
return page
}

View File

@@ -24,7 +24,17 @@ export async function render() {
const state = { config: null }
await loadConfig(page, state)
page.querySelector('#btn-save-models').onclick = () => saveConfig(state)
page.querySelector('#btn-save-models').onclick = async () => {
const btn = page.querySelector('#btn-save-models')
btn.disabled = true
btn.textContent = '保存中...'
try {
await saveConfig(state)
} finally {
btn.disabled = false
btn.textContent = '保存配置'
}
}
page.querySelector('#btn-add-provider').onclick = () => addProvider(page, state)
return page

View File

@@ -5,6 +5,7 @@ const routes = {}
let _contentEl = null
let _loadId = 0
let _currentCleanup = null
let _initialized = false
export function registerRoute(path, loader) {
routes[path] = loader
@@ -16,7 +17,10 @@ export function navigate(path) {
export function initRouter(contentEl) {
_contentEl = contentEl
window.addEventListener('hashchange', () => loadRoute())
if (!_initialized) {
window.addEventListener('hashchange', () => loadRoute())
_initialized = true
}
loadRoute()
}