/** * 模板引擎 - 动态模板渲染系统 */ class TemplateEngine { constructor() { this.templates = new Map(); this.cache = new Map(); this.currentTable = null; this.currentTemplate = 'list'; this.currentData = null; this.isLoading = false; this.preloadQueue = new Set(); this.cacheLimit = 50; // 限制缓存大小 this.cacheAccess = new Map(); // 记录缓存访问时间,用于LRU } /** * 加载模板 * @param {string} templateName - 模板名称 * @param {string} tableName - 表名 */ async loadTemplate(templateName, tableName) { const cacheKey = `${templateName}:${tableName}`; // 检查缓存 if (this.cache.has(cacheKey)) { this.cacheAccess.set(cacheKey, Date.now()); return this.cache.get(cacheKey); } try { // 使用 AbortController 支持请求取消 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 // 优先使用自定义模板 const customPath = `/templates/custom/${tableName}/${templateName}.html`; let response = await fetch(customPath, { signal: controller.signal }); if (!response.ok) { // 回退到默认自定义模板 const defaultPath = `/templates/custom/_default/${templateName}.html`; response = await fetch(defaultPath, { signal: controller.signal }); } if (!response.ok) { // 回退到内置模板 const builtinPath = `/templates/builtin/${templateName}.html`; response = await fetch(builtinPath, { signal: controller.signal }); } clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Template ${templateName} not found`); } const template = await response.text(); this.setCache(cacheKey, template); return template; } catch (error) { console.error('Failed to load template:', error); return this.getFallbackTemplate(templateName); } } /** * 获取回退模板 */ getFallbackTemplate(templateName) { const fallbacks = { 'list': `
{{#each columns}} {{#if this.showInList}} {{/if}} {{/each}} {{#each data}} {{#each ../columns}} {{#if this.showInList}} {{/if}} {{/each}} {{/each}}
{{this.alias}} 操作
{{renderField (lookup ../this this.name) this.renderType this}}
`, 'card': `
{{#each data}}
{{#each ../columns}} {{#if this.showInList}}
{{renderField (lookup ../this this.name) this.renderType this}}
{{/if}} {{/each}}
{{/each}}
` }; return fallbacks[templateName] || fallbacks['list']; } /** * 渲染字段 * @param {*} value - 字段值 * @param {string} type - 渲染类型 * @param {*} column - 列配置 */ renderField(value, type, column) { const renderers = { 'raw': (v) => v || '-', 'time': (v) => { if (!v) return '-'; const date = new Date(v); return column.format ? date.toLocaleString('zh-CN') : date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }, 'tag': (v, col) => { if (!v || !col.values) return v || '-'; const tag = col.values[v.toString()]; if (!tag) return v; return `${tag.label}`; }, 'category': (v) => v || '-', 'markdown': (v) => { if (!v) return '-'; return marked.parse(v.toString()); } }; return renderers[type] ? renderers[type](value, column) : value || '-'; } /** * 渲染模板 * @param {string} template - 模板内容 * @param {Object} data - 渲染数据 */ renderTemplate(template, data) { try { // 简单压缩HTML模板 const compressedTemplate = this.compressHTML(template); // 简单的模板引擎实现 - 优化性能 let rendered = compressedTemplate; // 预编译正则表达式,提升性能 const eachRegex = /\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g; const fieldRegex = /\{\{renderField ([^}]+)\}\}/g; const variableRegex = /\{\{([^}]+)\}\}/g; const thisRegex = /\{\{this\.([^}]+)\}\}/g; const simpleThisRegex = /\{\{this\}\}/g; // 处理循环 rendered = rendered.replace(eachRegex, (match, path, content) => { const items = this.getValue(data, path); if (!Array.isArray(items)) return ''; return items.map(item => { let itemContent = content; itemContent = itemContent.replace(thisRegex, (m, key) => this.getValue(item, key) || ''); itemContent = itemContent.replace(simpleThisRegex, item); return itemContent; }).join(''); }); // 处理字段渲染 rendered = rendered.replace(fieldRegex, (match, params) => { const [valuePath, typePath, columnPath] = params.split(' '); const value = this.getValue(data, valuePath); const type = this.getValue(data, typePath); const column = this.getValue(data, columnPath); return this.renderField(value, type, column); }); // 处理简单变量 rendered = rendered.replace(variableRegex, (match, path) => { return this.getValue(data, path) || ''; }); return rendered; } catch (error) { console.error('Template rendering failed:', error); return this.renderError('模板渲染失败:' + error.message); } } /** * 获取对象值 */ getValue(obj, path) { if (!obj || !path) return undefined; const keys = path.split('.'); let value = obj; for (const key of keys) { if (value == null) return undefined; value = value[key]; } return value; } /** * 渲染数据表 * @param {string} tableName - 表名 * @param {Object} options - 渲染选项 */ async renderTable(tableName, options = {}) { try { this.showLoading(); const { page = 1, perPage = 20, search = '', sort = '', order = 'desc', template = 'list' } = options; // 并行加载数据和模板 const [data, templateContent] = await Promise.all([ apiClient.getTableData(tableName, { page, per_page: perPage, search, sort, order }), this.loadTemplate(template, tableName) ]); // 预加载其他常用模板 if (template !== 'list') { this.preloadTemplates(tableName, ['list']); } if (template !== 'card') { this.preloadTemplates(tableName, ['card']); } // 渲染数据 const rendered = this.renderTemplate(templateContent, { data: data.data, columns: data.columns || [], total: data.total, page: data.page, perPage: data.per_page, pages: data.pages }); this.currentTable = tableName; this.currentTemplate = template; this.currentData = data; this.hideLoading(); return rendered; } catch (error) { console.error('Failed to render table:', error); this.hideLoading(); return this.renderError(error.message); } } /** * 切换模板 * @param {string} templateName - 模板名称 */ async switchTemplate(templateName) { if (!this.currentTable) return; try { const rendered = await this.renderTable(this.currentTable, { ...this.getCurrentOptions(), template: templateName }); this.updateContent(rendered); // 更新URL参数 const url = new URL(window.location); url.searchParams.set('template', templateName); window.history.pushState({}, '', url); } catch (error) { console.error('Failed to switch template:', error); } } /** * 获取当前查询参数 */ getCurrentOptions() { const url = new URL(window.location); return { page: parseInt(url.searchParams.get('page')) || 1, perPage: parseInt(url.searchParams.get('per_page')) || 20, search: url.searchParams.get('search') || '', sort: url.searchParams.get('sort') || '', order: url.searchParams.get('order') || 'desc', template: this.currentTemplate }; } /** * 显示加载状态 */ showLoading() { this.isLoading = true; const container = document.getElementById('mainContent'); if (container) { container.innerHTML = `
加载中...
`; } } /** * 隐藏加载状态 */ hideLoading() { this.isLoading = false; } /** * 更新内容 * @param {string} content - 渲染后的HTML内容 */ updateContent(content) { const container = document.getElementById('mainContent'); if (container) { container.innerHTML = content; } } /** * 渲染错误 * @param {string} message - 错误消息 */ renderError(message) { return `

加载失败

${message}

`; } /** * 设置缓存(带LRU策略) * @param {string} key - 缓存键 * @param {string} value - 缓存值 */ setCache(key, value) { if (this.cache.size >= this.cacheLimit) { // 使用LRU策略移除最少使用的缓存 const oldestKey = this.getOldestCacheKey(); if (oldestKey) { this.cache.delete(oldestKey); this.cacheAccess.delete(oldestKey); } } this.cache.set(key, value); this.cacheAccess.set(key, Date.now()); } /** * 获取最久未使用的缓存键 */ getOldestCacheKey() { let oldestKey = null; let oldestTime = Date.now(); for (const [key, time] of this.cacheAccess) { if (time < oldestTime) { oldestTime = time; oldestKey = key; } } return oldestKey; } /** * 预加载模板 * @param {string} tableName - 表名 * @param {Array} templateNames - 模板名称列表 */ async preloadTemplates(tableName, templateNames) { const promises = templateNames.map(templateName => this.loadTemplate(templateName, tableName).catch(() => { // 预加载失败时不影响主流程 console.warn(`Failed to preload template: ${templateName}`); }) ); await Promise.allSettled(promises); } /** * 清空缓存 */ clearCache() { this.cache.clear(); this.cacheAccess.clear(); } /** * 压缩HTML内容(简单压缩) * @param {string} html - HTML内容 */ compressHTML(html) { return html .replace(/\s+/g, ' ') .replace(/>\s+<') .trim(); } } // 全局模板引擎实例 window.templateEngine = new TemplateEngine();