/**
* 模板引擎 - 动态模板渲染系统
*/
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}}
|
{{this.alias}}
|
{{/if}}
{{/each}}
操作 |
{{#each data}}
{{#each ../columns}}
{{#if this.showInList}}
|
{{renderField (lookup ../this this.name) this.renderType this}}
|
{{/if}}
{{/each}}
|
{{/each}}
`,
'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 `
`;
}
/**
* 设置缓存(带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();