/** * 数据管理应用主逻辑 * 包含主题切换、侧边栏管理、搜索功能、视图控制等 */ class AppState { constructor() { this.searchTimeout = null; this.currentView = 'table'; this.exportMenuOpen = false; this.isMobile = false; this.init(); } init() { this.setupThemeToggle(); this.setupSidebarToggle(); this.setupSearch(); this.setupKeyboardShortcuts(); this.setupAccessibility(); this.setupResponsive(); this.setupViewControls(); this.setupExport(); this.setupPagination(); this.restoreUserPreferences(); } // 主题切换 setupThemeToggle() { const themeToggle = document.querySelector('.theme-toggle'); if (themeToggle) { themeToggle.addEventListener('click', () => this.toggleTheme()); } // 初始化主题 const savedTheme = localStorage.getItem('theme') || this.detectSystemTheme(); this.setTheme(savedTheme); // 监听系统主题变化 if (window.matchMedia) { const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); darkModeQuery.addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } } detectSystemTheme() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; this.setTheme(newTheme); localStorage.setItem('theme', newTheme); } setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); // 更新主题切换按钮文本 const themeToggle = document.querySelector('.theme-toggle'); if (themeToggle) { themeToggle.setAttribute('aria-label', theme === 'light' ? '切换到暗色主题' : '切换到亮色主题'); } } // 侧边栏管理 setupSidebarToggle() { const toggle = document.querySelector('.mobile-menu-toggle'); const sidebar = document.querySelector('.sidebar'); const backdrop = document.querySelector('.sidebar-backdrop'); if (toggle) { toggle.addEventListener('click', () => this.toggleSidebar()); } if (backdrop) { backdrop.addEventListener('click', () => this.closeSidebar()); } // 响应式处理 window.addEventListener('resize', () => this.handleResize()); this.handleResize(); // 初始化 } handleResize() { const wasMobile = this.isMobile; this.isMobile = window.innerWidth <= 768; if (wasMobile !== this.isMobile) { if (!this.isMobile) { this.closeSidebar(); } } } toggleSidebar() { const sidebar = document.querySelector('.sidebar'); const toggle = document.querySelector('.mobile-menu-toggle'); const isOpen = sidebar.classList.contains('open'); if (isOpen) { this.closeSidebar(); } else { this.openSidebar(); } } openSidebar() { const sidebar = document.querySelector('.sidebar'); const toggle = document.querySelector('.mobile-menu-toggle'); const backdrop = document.querySelector('.sidebar-backdrop'); sidebar.classList.add('open'); if (toggle) toggle.setAttribute('aria-expanded', 'true'); if (backdrop) backdrop.style.display = 'block'; document.body.style.overflow = 'hidden'; // 聚焦到第一个可聚焦元素 const firstFocusable = sidebar.querySelector('a, button, input'); if (firstFocusable) { setTimeout(() => firstFocusable.focus(), 100); } } closeSidebar() { const sidebar = document.querySelector('.sidebar'); const toggle = document.querySelector('.mobile-menu-toggle'); const backdrop = document.querySelector('.sidebar-backdrop'); sidebar.classList.remove('open'); if (toggle) toggle.setAttribute('aria-expanded', 'false'); if (backdrop) backdrop.style.display = 'none'; document.body.style.overflow = ''; // 将焦点返回到菜单按钮 if (toggle) toggle.focus(); } // 智能搜索 setupSearch() { const searchInput = document.getElementById('searchInput'); const searchClear = document.querySelector('.search-clear'); const sidebarSearchInput = document.querySelector('.sidebar-search-input'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.handleSearchInput(e.target.value); this.updateClearButton(e.target.value); }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.performSearch(e.target.value); } }); } if (searchClear) { searchClear.addEventListener('click', () => this.clearSearch()); } if (sidebarSearchInput) { sidebarSearchInput.addEventListener('input', (e) => { this.filterTables(e.target.value); }); } } handleSearchInput(query) { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.performSearch(query); }, 300); } updateClearButton(value) { const searchClear = document.querySelector('.search-clear'); if (searchClear) { searchClear.style.display = value ? 'block' : 'none'; } } performSearch(query) { const params = new URLSearchParams(window.location.search); const table = params.get('table') || this.getCurrentTable(); if (query.trim()) { params.set('search', query.trim()); } else { params.delete('search'); } params.set('page', '1'); window.location.href = `/?${params.toString()}`; } clearSearch() { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.value = ''; this.updateClearButton(''); this.performSearch(''); } } filterTables(query) { const navLinks = document.querySelectorAll('.nav-link'); const searchTerm = query.toLowerCase(); navLinks.forEach(link => { const title = link.querySelector('.nav-title').textContent.toLowerCase(); const subtitle = link.querySelector('.nav-subtitle').textContent.toLowerCase(); const description = link.querySelector('.nav-description').textContent.toLowerCase(); const matches = title.includes(searchTerm) || subtitle.includes(searchTerm) || description.includes(searchTerm); link.style.display = matches ? 'flex' : 'none'; link.parentElement.style.display = matches ? 'block' : 'none'; }); // 显示搜索结果提示 const visibleLinks = Array.from(navLinks).filter(link => link.style.display !== 'none'); if (visibleLinks.length === 0 && query.trim()) { this.showNotification('没有找到匹配的表格', 'info'); } } // 视图控制 setupViewControls() { const viewButtons = document.querySelectorAll('.view-button'); viewButtons.forEach(button => { button.addEventListener('click', (e) => { const mode = e.currentTarget.getAttribute('onclick') .match(/setViewMode\('(\w+)'\)/)[1]; this.setViewMode(mode); }); }); } setViewMode(mode) { this.currentView = mode; // 更新按钮状态 document.querySelectorAll('.view-button').forEach(btn => { btn.classList.remove('active'); }); const activeButton = document.querySelector(`[onclick*="setViewMode('${mode}')"]`); if (activeButton) { activeButton.classList.add('active'); } // 保存用户偏好 localStorage.setItem('viewMode', mode); // 切换视图显示 const contentContainer = document.querySelector('.content-main'); if (contentContainer) { contentContainer.setAttribute('data-view', mode); } // 切换数据展示方式 this.switchDataDisplay(mode); } switchDataDisplay(mode) { const tableView = document.querySelector('.data-table-container'); const cardView = document.querySelector('.data-cards-container'); if (tableView && cardView) { if (mode === 'table') { tableView.style.display = 'block'; cardView.style.display = 'none'; } else { tableView.style.display = 'none'; cardView.style.display = 'grid'; } } } // 导出功能 setupExport() { const exportButton = document.querySelector('.export-button'); if (exportButton) { exportButton.addEventListener('click', () => this.toggleExportMenu()); } const exportOptions = document.querySelectorAll('.export-option'); exportOptions.forEach(option => { option.addEventListener('click', (e) => { const format = e.target.getAttribute('onclick') .match(/exportData\('(\w+)'\)/)[1]; this.exportData(format); }); }); } toggleExportMenu() { const menu = document.querySelector('.export-menu'); const button = document.querySelector('.export-button'); if (!menu || !button) return; if (menu.classList.contains('show')) { this.closeExportMenu(); } else { this.openExportMenu(); } } openExportMenu() { const menu = document.querySelector('.export-menu'); const button = document.querySelector('.export-button'); if (!menu || !button) return; menu.classList.add('show'); button.setAttribute('aria-expanded', 'true'); // 点击外部关闭 setTimeout(() => { document.addEventListener('click', this.handleExportClickOutside, true); }, 0); } closeExportMenu() { const menu = document.querySelector('.export-menu'); const button = document.querySelector('.export-button'); if (!menu || !button) return; menu.classList.remove('show'); button.setAttribute('aria-expanded', 'false'); document.removeEventListener('click', this.handleExportClickOutside, true); } handleExportClickOutside = (e) => { const menu = document.querySelector('.export-menu'); const button = document.querySelector('.export-button'); if (!button.contains(e.target) && !menu.contains(e.target)) { this.closeExportMenu(); } } exportData(format) { const params = new URLSearchParams(window.location.search); const table = params.get('table') || this.getCurrentTable(); params.set('format', format); window.location.href = `/api/export/${table}?${params.toString()}`; this.closeExportMenu(); } // 分页控制 setupPagination() { const perPageSelect = document.querySelector('.per-page-select'); if (perPageSelect) { perPageSelect.addEventListener('change', (e) => { this.changePerPage(e.target.value); }); } } changePerPage(perPage) { const params = new URLSearchParams(window.location.search); params.set('per_page', perPage); params.set('page', '1'); window.location.href = `/?${params.toString()}`; } // 键盘快捷键 setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl/Cmd + K 聚焦搜索 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.focus(); searchInput.select(); } } // Esc 关闭侧边栏/菜单 if (e.key === 'Escape') { this.closeSidebar(); this.closeExportMenu(); // 关闭模态框 const modal = document.getElementById('detailModal'); if (modal && !modal.classList.contains('hidden')) { this.closeModal(); } } // Ctrl/Cmd + , 打开设置 if ((e.ctrlKey || e.metaKey) && e.key === ',') { e.preventDefault(); this.openSettings(); } // 方向键导航 if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { this.handleArrowNavigation(e); } }); } handleArrowNavigation(e) { // 添加键盘导航支持 const activeNav = document.querySelector('.nav-link.active'); if (activeNav && document.activeElement === activeNav) { e.preventDefault(); // 这里可以添加更复杂的键盘导航逻辑 } } // 无障碍支持 setupAccessibility() { // 焦点管理 document.addEventListener('focusin', (e) => { const sidebar = document.querySelector('.sidebar'); if (!sidebar) return; const isInSidebar = sidebar.contains(e.target); const isMobile = window.innerWidth <= 768; if (isMobile && !isInSidebar && sidebar.classList.contains('open')) { this.closeSidebar(); } }); // 添加ARIA实时区域 this.setupAriaLiveRegions(); } setupAriaLiveRegions() { // 创建屏幕阅读器通知区域 const liveRegion = document.createElement('div'); liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); liveRegion.className = 'sr-only'; document.body.appendChild(liveRegion); } // 响应式处理 setupResponsive() { const handleResize = () => { const isMobile = window.innerWidth <= 768; if (isMobile !== this.isMobile) { this.isMobile = isMobile; this.handleResponsiveChange(isMobile); } }; window.addEventListener('resize', this.debounce(handleResize, 250)); handleResize(); // 初始化 } handleResponsiveChange(isMobile) { if (!isMobile) { this.closeSidebar(); } // 更新断点属性 document.documentElement.setAttribute('data-breakpoint', isMobile ? 'mobile' : 'desktop'); } // 模态框控制 setupModal() { const modal = document.getElementById('detailModal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { this.closeModal(); } }); } } showDetail(id) { const table = this.getCurrentTable(); this.showLoading(); fetch(`/api/data/${table}/detail/${id}`) .then(response => response.json()) .then(data => { this.hideLoading(); this.renderDetailModal(data); }) .catch(error => { this.hideLoading(); this.showNotification('加载详情失败', 'error'); console.error('Error loading detail:', error); }); } renderDetailModal(data) { const modal = document.getElementById('detailModal'); const content = document.getElementById('detailContent'); if (!modal || !content) return; let html = ''; for (const [key, value] of Object.entries(data.data || {})) { html += `