659 lines
21 KiB
JavaScript
659 lines
21 KiB
JavaScript
/**
|
|
* 数据管理应用主逻辑
|
|
* 包含主题切换、侧边栏管理、搜索功能、视图控制等
|
|
*/
|
|
|
|
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 += `
|
|
<div class="detail-item">
|
|
<div class="detail-label">${key}</div>
|
|
<div class="detail-value">${value || ''}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
content.innerHTML = html;
|
|
modal.classList.remove('hidden');
|
|
|
|
// 聚焦到模态框
|
|
const firstFocusable = modal.querySelector('button, [href], [tabindex]:not([tabindex="-1"])');
|
|
if (firstFocusable) {
|
|
firstFocusable.focus();
|
|
}
|
|
}
|
|
|
|
closeModal() {
|
|
const modal = document.getElementById('detailModal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// 工具函数
|
|
getCurrentTable() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get('table') || '';
|
|
}
|
|
|
|
showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification notification-${type}`;
|
|
notification.setAttribute('role', 'alert');
|
|
notification.setAttribute('aria-live', 'polite');
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
showLoading() {
|
|
const loading = document.createElement('div');
|
|
loading.className = 'loading-overlay';
|
|
loading.innerHTML = '<div class="loading-spinner"></div>';
|
|
document.body.appendChild(loading);
|
|
}
|
|
|
|
hideLoading() {
|
|
const loading = document.querySelector('.loading-overlay');
|
|
if (loading) {
|
|
loading.remove();
|
|
}
|
|
}
|
|
|
|
restoreUserPreferences() {
|
|
// 恢复视图模式
|
|
const savedViewMode = localStorage.getItem('viewMode') || 'table';
|
|
this.setViewMode(savedViewMode);
|
|
|
|
// 恢复每页显示数量
|
|
const savedPerPage = localStorage.getItem('perPage');
|
|
if (savedPerPage) {
|
|
const perPageSelect = document.querySelector('.per-page-select');
|
|
if (perPageSelect) {
|
|
perPageSelect.value = savedPerPage;
|
|
}
|
|
}
|
|
}
|
|
|
|
openSettings() {
|
|
this.showNotification('设置功能开发中...', 'info');
|
|
}
|
|
|
|
debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
}
|
|
|
|
// 全局函数
|
|
window.toggleSidebar = () => app.toggleSidebar();
|
|
window.closeSidebar = () => app.closeSidebar();
|
|
window.toggleTheme = () => app.toggleTheme();
|
|
window.debounceSearch = (query) => app.handleSearchInput(query);
|
|
window.performSearch = () => {
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) app.performSearch(searchInput.value);
|
|
};
|
|
window.clearSearch = () => app.clearSearch();
|
|
window.filterTables = (query) => app.filterTables(query);
|
|
window.setViewMode = (mode) => app.setViewMode(mode);
|
|
window.toggleExportMenu = () => app.toggleExportMenu();
|
|
window.exportData = (format) => app.exportData(format);
|
|
window.changePerPage = (perPage) => app.changePerPage(perPage);
|
|
window.showDetail = (id) => app.showDetail(id);
|
|
window.closeModal = () => app.closeModal();
|
|
window.openSettings = () => app.openSettings();
|
|
|
|
// 初始化应用
|
|
const app = new AppState();
|
|
|
|
// 页面加载完成后的初始化
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log('数据管理应用已初始化');
|
|
|
|
// 添加初始化完成事件
|
|
const initEvent = new CustomEvent('app:initialized');
|
|
document.dispatchEvent(initEvent);
|
|
});
|
|
|
|
// 处理回退按钮
|
|
window.addEventListener('popstate', () => {
|
|
// 重新加载页面数据
|
|
location.reload();
|
|
});
|
|
|
|
// 错误处理
|
|
window.addEventListener('error', (e) => {
|
|
console.error('应用错误:', e.error);
|
|
app.showNotification('发生错误,请重试', 'error');
|
|
});
|
|
|
|
// 未捕获的Promise错误
|
|
window.addEventListener('unhandledrejection', (e) => {
|
|
console.error('未处理的Promise错误:', e.reason);
|
|
app.showNotification('网络错误,请检查连接', 'error');
|
|
}); |