/** * 侧边导航栏 */ import { navigate, getCurrentRoute, reloadCurrentRoute } from '../router.js' import { toggleTheme, getTheme } from '../lib/theme.js' import { isOpenclawReady } from '../lib/app-state.js' import { api } from '../lib/tauri-api.js' import { toast } from './toast.js' const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0' import { t, getLang, setLang, getAvailableLangs } from '../lib/i18n.js' import { isFeatureAvailable } from '../lib/feature-gates.js' import { getActiveEngine, getActiveEngineId, listEngines, switchEngine, onEngineChange } from '../lib/engine-manager.js' function NAV_ITEMS_FULL() { return [ { section: t('sidebar.sectionMonitor'), items: [ { route: '/dashboard', label: t('sidebar.dashboard'), icon: 'dashboard' }, { route: '/assistant', label: t('sidebar.assistant'), icon: 'assistant' }, { route: '/chat', label: t('sidebar.chat'), icon: 'chat' }, { route: '/route-map', label: t('sidebar.routeMap'), icon: 'route-map' }, { route: '/services', label: t('sidebar.services'), icon: 'services' }, { route: '/logs', label: t('sidebar.logs'), icon: 'logs' }, ] }, { section: t('sidebar.sectionConfig'), items: [ { route: '/models', label: t('sidebar.models'), icon: 'models' }, { route: '/agents', label: t('sidebar.agents'), icon: 'agents' }, { route: '/gateway', label: t('sidebar.gateway'), icon: 'gateway' }, { route: '/channels', label: t('sidebar.channels'), icon: 'channels' }, { route: '/communication', label: t('sidebar.communication'), icon: 'settings' }, { route: '/security', label: t('sidebar.security'), icon: 'security' }, ] }, { section: t('sidebar.sectionData'), items: [ { route: '/memory', label: t('sidebar.memory'), icon: 'memory', gate: 'memory' }, { route: '/dreaming', label: t('sidebar.dreaming'), icon: 'dreaming', gate: 'dreaming' }, { route: '/cron', label: t('sidebar.cron'), icon: 'clock', gate: 'cron' }, { route: '/usage', label: t('sidebar.usage'), icon: 'bar-chart' }, ] }, { section: t('sidebar.sectionExtension'), items: [ { route: '/skills', label: t('sidebar.skills'), icon: 'skills', gate: 'skills' }, { route: '/plugin-hub', label: t('sidebar.pluginHub'), icon: 'extensions' }, ] }, { section: '', items: [ { route: '/settings', label: t('sidebar.settings'), icon: 'settings' }, { route: '/chat-debug', label: t('sidebar.checkRepair'), icon: 'diagnose' }, { route: '/about', label: t('sidebar.about'), icon: 'about' }, ] } ] } function NAV_ITEMS_SETUP() { return [ { section: '', items: [ { route: '/setup', label: t('sidebar.setup'), icon: 'setup' }, { route: '/assistant', label: t('sidebar.assistant'), icon: 'assistant' }, ] }, { section: '', items: [ { route: '/settings', label: t('sidebar.settings'), icon: 'settings' }, { route: '/chat-debug', label: t('sidebar.chatDebug'), icon: 'debug' }, { route: '/about', label: t('sidebar.about'), icon: 'about' }, ] } ] } const ICONS = { setup: '', dashboard: '', chat: '', services: '', logs: '', models: '', agents: '', gateway: '', memory: '', extensions: '', about: '', assistant: '', security: '', dreaming: '', skills: '', channels: '', clock: '', 'bar-chart': '', settings: '', debug: '', 'route-map': '', diagnose: '', } let _delegated = false // === 引擎切换器 === function _renderEngineSwitcher() { const engines = listEngines() if (engines.length < 2) return '' // 只有一个引擎时不显示 const active = getActiveEngine() if (!active) return '' return `
${_escSidebar(t('engine.switcherSectionLabel'))}
${engines.map(e => `
${e.icon || ''} ${_escSidebar(e.name)} ${e.id === active.id ? '' : ''}
`).join('')}
` } function _closeEngineDropdown() { const dd = document.getElementById('engine-dropdown') if (dd) dd.classList.remove('open') const btn = document.getElementById('btn-engine-toggle') if (btn) btn.setAttribute('aria-expanded', 'false') } function _toggleEngineDropdown() { const dd = document.getElementById('engine-dropdown') if (!dd) return const btn = document.getElementById('btn-engine-toggle') if (dd.classList.contains('open')) { dd.classList.remove('open') if (btn) btn.setAttribute('aria-expanded', 'false') return } dd.classList.add('open') if (btn) btn.setAttribute('aria-expanded', 'true') } const LS_SIDEBAR_COLLAPSED = 'clawpanel_sidebar_collapsed' function _isDesktopSidebarCollapsed() { try { return localStorage.getItem(LS_SIDEBAR_COLLAPSED) === '1' } catch { return false } } function _setDesktopSidebarCollapsed(collapsed) { try { localStorage.setItem(LS_SIDEBAR_COLLAPSED, collapsed ? '1' : '0') } catch {} const sidebar = document.getElementById('sidebar') if (sidebar) { sidebar.classList.toggle('sidebar-collapsed', !!collapsed) } const btn = document.getElementById('btn-sidebar-collapse') if (btn) btn.textContent = collapsed ? '»' : '«' } export function renderSidebar(el) { const current = getCurrentRoute() const collapsed = _isDesktopSidebarCollapsed() let html = ` ${_renderEngineSwitcher()} ' // 主题切换按钮 const isDark = getTheme() === 'dark' const sunIcon = '' const moonIcon = '' const langCode = getLang() const langs = getAvailableLangs() const currentLang = langs.find(l => l.code === langCode) || langs[0] const globeIcon = '' const checkIcon = '' const langOptions = langs.map(l => `
${l.label} ${l.code} ${l.code === langCode ? `${checkIcon}` : ''}
`).join('') html += ` ` el.innerHTML = html // 应用折叠态(桌面端) _setDesktopSidebarCollapsed(collapsed) // 事件委托:只绑定一次,避免重复绑定 if (!_delegated) { _delegated = true el.addEventListener('click', (e) => { // 导航点击 const navItem = e.target.closest('.nav-item[data-route]') if (navItem) { navigate(navItem.dataset.route) _closeMobileSidebar() return } // 移动端关闭按钮 if (e.target.closest('#btn-sidebar-close')) { _closeMobileSidebar() return } // 侧边栏折叠 const collapseBtn = e.target.closest('#btn-sidebar-collapse') if (collapseBtn) { _setDesktopSidebarCollapsed(!_isDesktopSidebarCollapsed()) // 不需要整体重渲染 return } // 主题切换 const themeBtn = e.target.closest('#btn-theme-toggle') if (themeBtn) { toggleTheme(() => renderSidebar(el)) return } // 语言切换器:打开/关闭下拉 const langBtn = e.target.closest('#btn-lang-toggle') if (langBtn) { _toggleLangDropdown(el) return } // 语言选项点击 const langOpt = e.target.closest('.lang-option[data-lang]') if (langOpt) { const code = langOpt.dataset.lang if (code !== getLang()) { setLang(code) renderSidebar(el) reloadCurrentRoute() } else { _closeLangDropdown() } return } // 引擎切换器:打开/关闭下拉 const engineBtn = e.target.closest('#btn-engine-toggle') if (engineBtn) { _toggleEngineDropdown() return } // 引擎选项点击 const engineOpt = e.target.closest('.engine-option[data-engine]') if (engineOpt) { const eid = engineOpt.dataset.engine _closeEngineDropdown() if (eid !== getActiveEngineId()) { engineOpt.style.opacity = '0.5' // 立即在内容区显示加载骨架,避免切换期间空白 const contentEl = document.getElementById('content') if (contentEl) { contentEl.innerHTML = `
${[1,2,3].map(() => '
').join('')}
` } switchEngine(eid).then(() => { toast(t('engine.switchedTo', { name: getActiveEngine()?.name || eid }), 'success') renderSidebar(el) // 跳转到新引擎的默认或 setup 页 const eng = getActiveEngine() if (eng) { navigate(eng.isReady() ? eng.getDefaultRoute() : eng.getSetupRoute()) } }).catch(err => { console.error('[sidebar] 切换引擎失败:', err) toast(t('engine.switchFailed') || '引擎切换失败,请稍后重试', 'error') renderSidebar(el) // 恢复内容区:重新加载当前路由或显示错误占位 const contentEl = document.getElementById('content') if (contentEl) { const hash = window.location.hash.slice(1) || '/' if (hash) { reloadCurrentRoute() } else { contentEl.innerHTML = `
加载失败,请刷新页面重试
` } } }) } return } // 点击其他区域关闭下拉 if (!e.target.closest('.engine-switcher')) { _closeEngineDropdown() } if (!e.target.closest('.lang-switcher')) { _closeLangDropdown() } }) } } function _escSidebar(s) { return String(s || '').replace(//g, '>') } // === 移动端侧边栏 === function _closeMobileSidebar() { const sidebar = document.getElementById('sidebar') const overlay = document.getElementById('sidebar-overlay') if (sidebar) sidebar.classList.remove('sidebar-open') if (overlay) overlay.classList.remove('visible') } export function openMobileSidebar() { const sidebar = document.getElementById('sidebar') if (!sidebar) return sidebar.classList.add('sidebar-open') let overlay = document.getElementById('sidebar-overlay') if (!overlay) { overlay = document.createElement('div') overlay.id = 'sidebar-overlay' overlay.className = 'sidebar-overlay' overlay.addEventListener('click', _closeMobileSidebar) document.getElementById('app').appendChild(overlay) } requestAnimationFrame(() => overlay.classList.add('visible')) } function _closeLangDropdown() { const sw = document.getElementById('lang-switcher') const dd = document.getElementById('lang-dropdown') if (dd) dd.classList.remove('open') if (sw) sw.classList.remove('open') } function _toggleLangDropdown(sidebarEl) { const sw = document.getElementById('lang-switcher') const dd = document.getElementById('lang-dropdown') if (!dd) return if (dd.classList.contains('open')) { dd.classList.remove('open'); if (sw) sw.classList.remove('open'); return } dd.classList.add('open') if (sw) sw.classList.add('open') const searchInput = dd.querySelector('#lang-search') if (searchInput) { searchInput.value = '' _filterLangOptions('') requestAnimationFrame(() => searchInput.focus()) searchInput.oninput = () => _filterLangOptions(searchInput.value) } } function _filterLangOptions(query) { const opts = document.querySelectorAll('#lang-options .lang-option') const q = query.toLowerCase().trim() opts.forEach(opt => { if (!q) { opt.style.display = ''; return } const label = (opt.querySelector('.lang-option-label')?.textContent || '').toLowerCase() const code = (opt.querySelector('.lang-option-code')?.textContent || '').toLowerCase() opt.style.display = (label.includes(q) || code.includes(q)) ? '' : 'none' }) }