515 lines
15 KiB
JavaScript
515 lines
15 KiB
JavaScript
/**
|
|
* UI控制器 - 管理模板切换和用户界面交互
|
|
*/
|
|
class UIController {
|
|
constructor() {
|
|
this.currentTable = null;
|
|
this.currentTemplate = 'list';
|
|
this.availableTemplates = [];
|
|
this.isLoading = false;
|
|
this.searchTimeout = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* 初始化UI控制器
|
|
*/
|
|
init() {
|
|
this.bindEvents();
|
|
this.setupKeyboardShortcuts();
|
|
this.setupResponsiveUI();
|
|
this.loadFromURL();
|
|
}
|
|
|
|
/**
|
|
* 绑定事件监听器
|
|
*/
|
|
bindEvents() {
|
|
// 表选择器
|
|
const tableSelector = document.getElementById('tableSelector');
|
|
if (tableSelector) {
|
|
tableSelector.addEventListener('change', (e) => {
|
|
this.changeTable(e.target.value);
|
|
});
|
|
}
|
|
|
|
// 搜索框
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
this.handleSearch(e.target.value);
|
|
});
|
|
}
|
|
|
|
// 分页
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('pagination-link')) {
|
|
e.preventDefault();
|
|
const page = parseInt(e.target.dataset.page);
|
|
this.goToPage(page);
|
|
}
|
|
});
|
|
|
|
// 排序
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('sort-header')) {
|
|
e.preventDefault();
|
|
const field = e.target.dataset.field;
|
|
this.toggleSort(field);
|
|
}
|
|
});
|
|
|
|
// 模板切换
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('template-switch')) {
|
|
e.preventDefault();
|
|
const template = e.target.dataset.template;
|
|
this.switchTemplate(template);
|
|
}
|
|
});
|
|
|
|
// 刷新按钮
|
|
const refreshBtn = document.getElementById('refreshBtn');
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', () => {
|
|
this.refreshData();
|
|
});
|
|
}
|
|
|
|
// 浏览器历史记录
|
|
window.addEventListener('popstate', () => {
|
|
this.loadFromURL();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 设置键盘快捷键
|
|
*/
|
|
setupKeyboardShortcuts() {
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl/Cmd + R: 刷新
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
|
|
e.preventDefault();
|
|
this.refreshData();
|
|
}
|
|
|
|
// Ctrl/Cmd + K: 聚焦搜索
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) searchInput.focus();
|
|
}
|
|
|
|
// Escape: 关闭详情弹窗
|
|
if (e.key === 'Escape') {
|
|
this.closeModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 设置响应式UI
|
|
*/
|
|
setupResponsiveUI() {
|
|
// 监听窗口大小变化
|
|
let resizeTimeout;
|
|
window.addEventListener('resize', () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
this.adjustUILayout();
|
|
}, 250);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 调整UI布局
|
|
*/
|
|
adjustUILayout() {
|
|
const width = window.innerWidth;
|
|
const container = document.getElementById('mainContent');
|
|
|
|
if (!container) return;
|
|
|
|
if (width < 768) {
|
|
container.classList.add('mobile-layout');
|
|
container.classList.remove('desktop-layout');
|
|
} else {
|
|
container.classList.add('desktop-layout');
|
|
container.classList.remove('mobile-layout');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从URL加载配置
|
|
*/
|
|
loadFromURL() {
|
|
const url = new URL(window.location);
|
|
const table = url.searchParams.get('table');
|
|
const template = url.searchParams.get('template') || 'list';
|
|
const page = parseInt(url.searchParams.get('page')) || 1;
|
|
const search = url.searchParams.get('search') || '';
|
|
|
|
if (table) {
|
|
this.currentTable = table;
|
|
this.currentTemplate = template;
|
|
this.loadTableData(table, { page, search, template });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 切换数据表
|
|
* @param {string} tableName - 表名
|
|
*/
|
|
async changeTable(tableName) {
|
|
if (this.isLoading) return;
|
|
|
|
this.currentTable = tableName;
|
|
this.currentTemplate = 'list'; // 重置为默认模板
|
|
|
|
try {
|
|
// 并行获取可用模板和预加载模板
|
|
const [templates] = await Promise.all([
|
|
apiClient.getAvailableTemplates(tableName),
|
|
templateEngine.preloadTemplates(tableName, ['list', 'card'])
|
|
]);
|
|
|
|
this.availableTemplates = templates.templates;
|
|
this.renderTemplateSelector();
|
|
|
|
// 加载数据
|
|
await this.loadTableData(tableName);
|
|
|
|
// 更新URL
|
|
this.updateURL();
|
|
|
|
} catch (error) {
|
|
console.error('Failed to change table:', error);
|
|
this.showError('切换数据表失败:' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 切换模板
|
|
* @param {string} templateName - 模板名称
|
|
*/
|
|
async switchTemplate(templateName) {
|
|
if (this.isLoading || !this.currentTable) return;
|
|
|
|
this.currentTemplate = templateName;
|
|
|
|
try {
|
|
await templateEngine.switchTemplate(templateName);
|
|
this.updateURL();
|
|
this.updateTemplateSelector(templateName);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to switch template:', error);
|
|
this.showError('切换模板失败:' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 加载数据表数据
|
|
* @param {string} tableName - 表名
|
|
* @param {Object} options - 选项
|
|
*/
|
|
async loadTableData(tableName, options = {}) {
|
|
const {
|
|
page = 1,
|
|
perPage = 20,
|
|
search = '',
|
|
sort = '',
|
|
order = 'desc',
|
|
template = this.currentTemplate
|
|
} = options;
|
|
|
|
this.showLoading();
|
|
|
|
try {
|
|
const rendered = await templateEngine.renderTable(tableName, {
|
|
page,
|
|
perPage,
|
|
search,
|
|
sort,
|
|
order,
|
|
template
|
|
});
|
|
|
|
this.updateContent(rendered);
|
|
this.renderPagination({ page, perPage, total: templateEngine.currentData?.total || 0 });
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load table data:', error);
|
|
this.showError('加载数据失败:' + error.message);
|
|
} finally {
|
|
this.hideLoading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 渲染模板选择器
|
|
*/
|
|
renderTemplateSelector() {
|
|
const container = document.getElementById('templateSelector');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = `
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-sm text-gray-600">视图:</span>
|
|
${this.availableTemplates.map(template => `
|
|
<button
|
|
class="template-switch px-3 py-1 text-sm rounded ${
|
|
template === this.currentTemplate
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
}"
|
|
data-template="${template}"
|
|
>
|
|
${this.getTemplateDisplayName(template)}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 更新模板选择器状态
|
|
* @param {string} activeTemplate - 当前激活的模板
|
|
*/
|
|
updateTemplateSelector(activeTemplate) {
|
|
const buttons = document.querySelectorAll('.template-switch');
|
|
buttons.forEach(btn => {
|
|
if (btn.dataset.template === activeTemplate) {
|
|
btn.className = 'template-switch px-3 py-1 text-sm rounded bg-blue-500 text-white';
|
|
} else {
|
|
btn.className = 'template-switch px-3 py-1 text-sm rounded bg-gray-200 text-gray-700 hover:bg-gray-300';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取模板显示名称
|
|
* @param {string} template - 模板名称
|
|
*/
|
|
getTemplateDisplayName(template) {
|
|
const names = {
|
|
'list': '列表',
|
|
'card': '卡片',
|
|
'timeline': '时间轴'
|
|
};
|
|
return names[template] || template;
|
|
}
|
|
|
|
/**
|
|
* 渲染分页
|
|
* @param {Object} pagination - 分页信息
|
|
*/
|
|
renderPagination({ page, perPage, total }) {
|
|
const container = document.getElementById('pagination');
|
|
if (!container) return;
|
|
|
|
const pages = Math.ceil(total / perPage);
|
|
const startPage = Math.max(1, page - 2);
|
|
const endPage = Math.min(pages, page + 2);
|
|
|
|
let html = `
|
|
<div class="flex justify-between items-center">
|
|
<div class="text-sm text-gray-700">
|
|
共 ${total} 条记录,第 ${page} / ${pages} 页
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
`;
|
|
|
|
if (page > 1) {
|
|
html += `
|
|
<button class="pagination-link px-3 py-1 text-sm border rounded hover:bg-gray-50" data-page="${page - 1}">
|
|
上一页
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
html += `
|
|
<button class="pagination-link px-3 py-1 text-sm border rounded ${
|
|
i === page ? 'bg-blue-500 text-white' : 'hover:bg-gray-50'
|
|
}" data-page="${i}">
|
|
${i}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
if (page < pages) {
|
|
html += `
|
|
<button class="pagination-link px-3 py-1 text-sm border rounded hover:bg-gray-50" data-page="${page + 1}">
|
|
下一页
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* 处理搜索
|
|
* @param {string} query - 搜索关键词
|
|
*/
|
|
handleSearch(query) {
|
|
clearTimeout(this.searchTimeout);
|
|
|
|
this.searchTimeout = setTimeout(() => {
|
|
if (this.currentTable) {
|
|
this.loadTableData(this.currentTable, { search: query, page: 1 });
|
|
this.updateURL();
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* 跳转到指定页面
|
|
* @param {number} page - 页码
|
|
*/
|
|
goToPage(page) {
|
|
if (this.currentTable) {
|
|
this.loadTableData(this.currentTable, { page });
|
|
this.updateURL();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 切换排序
|
|
* @param {string} field - 排序字段
|
|
*/
|
|
toggleSort(field) {
|
|
const url = new URL(window.location);
|
|
const currentSort = url.searchParams.get('sort');
|
|
const currentOrder = url.searchParams.get('order') || 'asc';
|
|
|
|
let newOrder = 'asc';
|
|
if (currentSort === field && currentOrder === 'asc') {
|
|
newOrder = 'desc';
|
|
}
|
|
|
|
if (this.currentTable) {
|
|
this.loadTableData(this.currentTable, {
|
|
sort: field,
|
|
order: newOrder,
|
|
page: 1
|
|
});
|
|
this.updateURL();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 刷新数据
|
|
*/
|
|
async refreshData() {
|
|
if (this.currentTable) {
|
|
templateEngine.clearCache();
|
|
await this.loadTableData(this.currentTable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 更新URL参数
|
|
*/
|
|
updateURL() {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('table', this.currentTable);
|
|
url.searchParams.set('template', this.currentTemplate);
|
|
|
|
const currentOptions = this.getCurrentOptions();
|
|
if (currentOptions.page > 1) {
|
|
url.searchParams.set('page', currentOptions.page);
|
|
} else {
|
|
url.searchParams.delete('page');
|
|
}
|
|
|
|
if (currentOptions.search) {
|
|
url.searchParams.set('search', currentOptions.search);
|
|
} else {
|
|
url.searchParams.delete('search');
|
|
}
|
|
|
|
window.history.pushState({}, '', url);
|
|
}
|
|
|
|
/**
|
|
* 显示加载状态
|
|
*/
|
|
showLoading() {
|
|
this.isLoading = true;
|
|
const container = document.getElementById('mainContent');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="flex justify-center items-center h-64">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<span class="ml-3 text-gray-600">加载中...</span>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 隐藏加载状态
|
|
*/
|
|
hideLoading() {
|
|
this.isLoading = false;
|
|
}
|
|
|
|
/**
|
|
* 更新内容
|
|
* @param {string} content - 内容HTML
|
|
*/
|
|
updateContent(content) {
|
|
const container = document.getElementById('mainContent');
|
|
if (container) {
|
|
container.innerHTML = content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 显示错误消息
|
|
* @param {string} message - 错误消息
|
|
*/
|
|
showError(message) {
|
|
const container = document.getElementById('mainContent');
|
|
if (container) {
|
|
container.innerHTML = `
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div class="flex">
|
|
<div class="text-red-600">
|
|
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800">错误</h3>
|
|
<div class="mt-2 text-sm text-red-700">
|
|
<p>${message}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 关闭模态框
|
|
*/
|
|
closeModal() {
|
|
const modal = document.getElementById('detailModal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 全局UI控制器实例
|
|
window.uiController = new UIController(); |