feat: update ui

This commit is contained in:
2025-08-06 11:12:16 +08:00
parent e034a2e54e
commit c4ad0c1dc9
12 changed files with 4983 additions and 226 deletions

655
web/static/js/app.js Normal file
View File

@@ -0,0 +1,655 @@
/**
* 数据管理应用主逻辑
* 包含主题切换、侧边栏管理、搜索功能、视图控制等
*/
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 description = link.querySelector('.nav-description').textContent.toLowerCase();
const matches = title.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">
<label class="detail-label">${key}</label>
<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.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');
});