| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- // 模板渲染器
- // 渲染模板缩略图列表
- export function renderTemplates(templates, category = 'all') {
- const templatesContainer = document.getElementById('templates-container');
-
- if (!templatesContainer) return;
-
- templatesContainer.innerHTML = '';
-
- // 根据分类筛选模板
- const filteredTemplates = category === 'all'
- ? templates
- : templates.filter(template => template.category === category);
-
- // 创建一个文档片段,减少DOM操作
- const fragment = document.createDocumentFragment();
-
- filteredTemplates.forEach(template => {
- const templateItem = document.createElement('div');
- templateItem.className = 'template-item';
- templateItem.dataset.templateId = template.id;
-
- const thumbnail = document.createElement('img');
- // 使用较小的缩略图尺寸来加快加载
- thumbnail.src = template.thumbnail;
- thumbnail.className = 'template-thumbnail';
- thumbnail.alt = template.name;
- thumbnail.loading = 'lazy'; // 懒加载图片
-
- const templateName = document.createElement('div');
- templateName.className = 'template-name';
- templateName.textContent = template.name;
-
- templateItem.appendChild(thumbnail);
- templateItem.appendChild(templateName);
- fragment.appendChild(templateItem);
-
- // 添加点击事件
- templateItem.addEventListener('click', () => {
- // 移除其他模板的选中状态
- document.querySelectorAll('.template-item').forEach(item => {
- item.classList.remove('selected');
- });
-
- // 添加选中状态
- templateItem.classList.add('selected');
-
- // 初始化编辑器
- initializeEditor(template);
- });
- });
-
- // 一次性将所有元素添加到容器中
- templatesContainer.appendChild(fragment);
- }
- // 初始化编辑器
- export function initializeEditor(template) {
- const editForm = document.getElementById('edit-form');
- const posterPreview = document.getElementById('poster-preview');
- const downloadBtn = document.getElementById('download-btn');
-
- if (!editForm || !posterPreview) return;
-
- // 清空编辑表单
- editForm.innerHTML = '';
-
- // 存储当前模板数据
- window.currentTemplate = template;
- window.currentValues = {};
-
- // 创建一个空的数据对象,用于生成模板HTML
- const emptyData = {};
- template.fields.forEach(field => {
- // 为每个字段提供默认值,避免undefined
- emptyData[field.name] = field.default || '';
- });
-
- // 检查模板HTML中是否有硬编码的内容
- const templateHTML = template.template(emptyData);
- const hardcodedTexts = findHardcodedTexts(templateHTML);
-
- // 组合所有需要编辑的字段
- let allFields = [...template.fields];
-
- // 添加从模板中检测到的硬编码文本作为可编辑字段
- const existingDefaults = allFields.map(field => field.default);
-
- hardcodedTexts.forEach((text, index) => {
- // 检查文本是否已存在于字段中,或者是否是常见文本
- if (!existingDefaults.includes(text) && !isCommonText(text)) {
- let label = '检测到的文本';
-
- if (text.length > 15) {
- label = `${text.substring(0, 15)}...`;
- } else {
- label = text;
- }
-
- allFields.push({
- name: `detected_text_${index}`,
- type: 'text',
- label: label,
- default: text
- });
- }
- });
- // 创建特殊的布局容器用于前三个字段
- const specialFormGroup = document.createElement('div');
- specialFormGroup.className = 'form-group special-layout';
- specialFormGroup.style.display = 'flex';
- specialFormGroup.style.gap = '15px';
- specialFormGroup.style.marginBottom = '20px';
- // 左侧图片容器
- const leftColumn = document.createElement('div');
- leftColumn.style.flex = '1';
- leftColumn.classList.add('left-column');
- // 右侧颜色容器
- const rightColumn = document.createElement('div');
- rightColumn.style.flex = '1';
- rightColumn.style.display = 'flex';
- rightColumn.style.flexDirection = 'column';
- rightColumn.style.gap = '30px';
- rightColumn.classList.add('right-column');
- // 处理前三个特殊字段
- const firstThreeFields = allFields.slice(0, 3);
- const remainingFields = allFields.slice(3);
- firstThreeFields.forEach((field, index) => {
- const label = document.createElement('label');
- label.textContent = field.label;
- label.setAttribute('for', `field-${field.name}`);
- if (index === 0) { // 图片字段
- const imageUpload = document.createElement('div');
- imageUpload.className = 'image-upload';
-
- const imagePreview = document.createElement('div');
- imagePreview.className = 'upload-preview';
-
- const img = document.createElement('img');
- img.src = field.default || '';
- img.id = `preview-${field.name}`;
- imagePreview.appendChild(img);
-
- const uploadButton = document.createElement('label');
- uploadButton.className = 'upload-btn';
- uploadButton.textContent = '更换图片';
- uploadButton.setAttribute('for', `field-${field.name}`);
-
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = 'image/*';
- input.id = `field-${field.name}`;
- input.style.display = 'none';
-
- input.addEventListener('change', function(e) {
- const file = e.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = function(event) {
- img.src = event.target.result;
- window.currentValues[field.name] = event.target.result;
- updatePosterPreview();
- };
- reader.readAsDataURL(file);
- }
- });
-
- imageUpload.appendChild(imagePreview);
- imageUpload.appendChild(uploadButton);
- imageUpload.appendChild(input);
-
- leftColumn.appendChild(label);
- leftColumn.appendChild(imageUpload);
-
- window.currentValues[field.name] = field.default || '';
- } else { // 颜色字段
- const colorContainer = document.createElement('div');
- colorContainer.style.flex = '1';
-
- const input = document.createElement('input');
- input.type = 'color';
- input.className = 'color-picker';
- input.id = `field-${field.name}`;
- input.value = field.default || '#000000';
-
- input.addEventListener('input', function(e) {
- window.currentValues[field.name] = e.target.value;
- updatePosterPreview();
- });
-
- window.currentValues[field.name] = field.default || '';
-
- colorContainer.appendChild(label);
- colorContainer.appendChild(input);
- rightColumn.appendChild(colorContainer);
- }
- });
- specialFormGroup.appendChild(leftColumn);
- specialFormGroup.appendChild(rightColumn);
- editForm.appendChild(specialFormGroup);
- // 处理剩余字段
- remainingFields.forEach(field => {
- if (field.default === undefined || field.default === null) {
- field.default = '';
- }
-
- if (field.name.startsWith('detected_text_') && isCommonText(field.default)) {
- return;
- }
-
- const formGroup = document.createElement('div');
- formGroup.className = 'form-group';
-
- const label = document.createElement('label');
- label.textContent = field.label;
- label.setAttribute('for', `field-${field.name}`);
-
- let input;
-
- switch (field.type) {
- case 'textarea':
- input = document.createElement('textarea');
- input.className = 'form-control';
- input.id = `field-${field.name}`;
- input.value = field.default || '';
- input.rows = 3;
- break;
-
- case 'color':
- input = document.createElement('input');
- input.type = 'color';
- input.className = 'color-picker';
- input.id = `field-${field.name}`;
- input.value = field.default || '#000000';
- break;
-
- case 'image':
- const imageUpload = document.createElement('div');
- imageUpload.className = 'image-upload';
-
- const imagePreview = document.createElement('div');
- imagePreview.className = 'upload-preview';
-
- const img = document.createElement('img');
- img.src = field.default || '';
- img.id = `preview-${field.name}`;
- imagePreview.appendChild(img);
-
- const uploadButton = document.createElement('label');
- uploadButton.className = 'upload-btn';
- uploadButton.textContent = '选择图片';
- uploadButton.setAttribute('for', `field-${field.name}`);
-
- input = document.createElement('input');
- input.type = 'file';
- input.accept = 'image/*';
- input.id = `field-${field.name}`;
- input.style.display = 'none';
-
- input.addEventListener('change', function(e) {
- const file = e.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = function(event) {
- img.src = event.target.result;
- window.currentValues[field.name] = event.target.result;
- updatePosterPreview();
- };
- reader.readAsDataURL(file);
- }
- });
-
- imageUpload.appendChild(imagePreview);
- imageUpload.appendChild(uploadButton);
- imageUpload.appendChild(input);
-
- formGroup.appendChild(label);
- formGroup.appendChild(imageUpload);
- editForm.appendChild(formGroup);
-
- window.currentValues[field.name] = field.default || '';
-
- return;
-
- default:
- input = document.createElement('input');
- input.type = 'text';
- input.className = 'form-control';
- input.id = `field-${field.name}`;
- input.value = field.default || '';
- }
-
- input.addEventListener('input', function(e) {
- window.currentValues[field.name] = e.target.value;
- updatePosterPreview();
- });
-
- window.currentValues[field.name] = field.default || '';
-
- formGroup.appendChild(label);
- formGroup.appendChild(input);
- editForm.appendChild(formGroup);
- });
-
- // 启用下载按钮
- if (downloadBtn) downloadBtn.disabled = false;
-
- // 更新预览
- updatePosterPreview();
-
- // 自动切换到编辑标签页
- const editorTab = document.querySelector('.panel-tab[data-tab="editor-tab"]');
- if (editorTab) {
- editorTab.click();
- }
-
- // 添加一个提示信息,如果检测到了额外文本
- const detectedCount = allFields.filter(f => f.name.startsWith('detected_text_')).length;
- if (detectedCount > 0 && !document.querySelector('.detection-notice')) {
- const notice = document.createElement('div');
- notice.className = 'detection-notice';
- notice.innerHTML = `
- <div class="info-message">
- <i class="fas fa-info-circle"></i>
- <span>已自动检测到模板中的额外可编辑文本</span>
- </div>
- `;
- editForm.insertBefore(notice, editForm.firstChild);
- }
- }
- // 查找模板中的硬编码文本
- function findHardcodedTexts(html) {
- const texts = new Set();
-
- // 创建一个临时的DOM元素来解析HTML
- const temp = document.createElement('div');
- temp.innerHTML = html;
-
- // 递归收集文本节点
- const collectTextNodes = (element) => {
- Array.from(element.childNodes).forEach(node => {
- if (node.nodeType === Node.TEXT_NODE) {
- const text = node.textContent.trim();
- if (text && !isFontAwesomeClass(text) && !isCssValue(text)) {
- texts.add(text);
- }
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- collectTextNodes(node);
- }
- });
- };
-
- collectTextNodes(temp);
- return Array.from(texts);
- }
- // 检查是否是Font Awesome类名
- function isFontAwesomeClass(text) {
- return text.startsWith('fa-') || text.startsWith('fas ') || text.startsWith('far ') || text.startsWith('fab ');
- }
- // 检查是否是CSS值
- function isCssValue(text) {
- // CSS单位和值的正则表达式
- const cssValueRegex = /^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|deg|s|ms)?$/;
- const colorRegex = /^#[0-9a-f]{3,6}$/i;
- const rgbaRegex = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)$/;
-
- return cssValueRegex.test(text) || colorRegex.test(text) || rgbaRegex.test(text);
- }
- // 检查是否是常见文本
- function isCommonText(text) {
- // 如果文本太短,可能不需要编辑
- if (text.length < 3) return true;
-
- // 常见的不需要编辑的文本
- const commonTexts = [
- '选择图片',
- '上传',
- '下载',
- '分享',
- '编辑',
- '预览',
- '确定',
- '取消'
- ];
-
- return commonTexts.includes(text);
- }
- // 更新海报预览
- function updatePosterPreview() {
- const posterPreview = document.getElementById('poster-preview');
- if (!posterPreview || !window.currentTemplate) return;
-
- // 生成预览HTML
- const previewHTML = window.currentTemplate.template(window.currentValues);
- posterPreview.innerHTML = previewHTML;
- }
- // 下载海报
- window.downloadPoster = function(format = 'png', quality = 0.9) {
- const posterPreview = document.getElementById('poster-preview');
-
- if (!posterPreview) return;
-
- // 显示加载提示
- const loadingTip = document.createElement('div');
- loadingTip.className = 'loading-tip';
- loadingTip.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在生成海报...';
- document.body.appendChild(loadingTip);
- // 获取所有图片并保存原始样式
- const images = posterPreview.getElementsByTagName('img');
- const originalStyles = Array.from(images).map(img => ({
- width: img.style.width,
- height: img.style.height,
- maxWidth: img.style.maxWidth,
- maxHeight: img.style.maxHeight,
- objectFit: img.style.objectFit
- }));
- // 创建一个函数来清理资源
- const cleanup = () => {
- try {
- // 移除加载提示
- if (loadingTip && loadingTip.parentNode) {
- loadingTip.remove();
- }
-
- // 恢复图片原始样式
- Array.from(images).forEach((img, index) => {
- if (originalStyles[index]) {
- Object.assign(img.style, originalStyles[index]);
- }
- });
-
- // 恢复水印
- if (watermarkData) {
- watermarkData.parent.appendChild(watermarkData.element);
- }
- } catch (error) {
- console.error('清理资源时发生错误:', error);
- }
- };
-
- // 临时移除水印(如果有)以便导出时不包含水印
- const watermark = posterPreview.querySelector('.poster-watermark');
- let watermarkData = null;
-
- if (watermark) {
- watermarkData = {
- element: watermark,
- parent: watermark.parentNode
- };
- watermark.remove();
- }
-
- try {
- // 获取预览区域的实际尺寸
- const previewRect = posterPreview.getBoundingClientRect();
- const previewWidth = previewRect.width;
- const previewHeight = previewRect.height;
-
- // 优化图片尺寸和质量
- Array.from(images).forEach(img => {
- // 保持图片原始比例
- img.style.width = '100%';
- img.style.height = '100%';
- img.style.maxWidth = 'none'; // 移除最大宽度限制
- img.style.objectFit = 'cover';
- img.style.imageRendering = 'high-quality';
- img.style.webkitFontSmoothing = 'antialiased';
- img.style.mozOsxFontSmoothing = 'grayscale';
-
- // 强制浏览器使用高质量缩放
- if (img.naturalWidth && img.naturalHeight) {
- img.setAttribute('width', img.naturalWidth);
- img.setAttribute('height', img.naturalHeight);
- }
- });
- } catch (error) {
- console.error('设置图片样式时发生错误:', error);
- cleanup();
- return;
- }
-
- // 优化的html2canvas配置
- const canvasOptions = {
- scale: format === 'png' ? 3 : 2, // 提高缩放比例以获得更清晰的输出
- useCORS: true,
- allowTaint: true,
- backgroundColor: format === 'png' ? null : 'white',
- imageTimeout: 30000, // 30秒超时
- logging: false,
- onclone: function(clonedDoc) {
- try {
- const clonedImages = clonedDoc.getElementsByTagName('img');
- Array.from(clonedImages).forEach(img => {
- img.style.width = '100%';
- img.style.height = '100%';
- img.style.maxWidth = 'none';
- img.style.objectFit = 'cover';
- img.style.imageRendering = 'high-quality';
- img.style.webkitFontSmoothing = 'antialiased';
- img.style.mozOsxFontSmoothing = 'grayscale';
-
- if (img.naturalWidth && img.naturalHeight) {
- img.setAttribute('width', img.naturalWidth);
- img.setAttribute('height', img.naturalHeight);
- }
-
- // 确保图片已加载
- if (!img.complete) {
- return new Promise((resolve) => {
- img.onload = resolve;
- });
- }
- });
- } catch (error) {
- console.error('克隆文档时发生错误:', error);
- }
- }
- };
- // 使用Promise.race来添加超时处理
- const timeoutPromise = new Promise((_, reject) => {
- setTimeout(() => reject(new Error('生成超时')), 40000); // 40秒总超时
- });
- Promise.race([
- html2canvas(posterPreview, canvasOptions),
- timeoutPromise
- ]).then(canvas => {
- try {
- // 根据格式选择导出方式
- const exportQuality = format === 'png' ? undefined : 1.0; // 最高质量JPEG
- const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
-
- // 获取原始canvas的尺寸
- const originalWidth = canvas.width;
- const originalHeight = canvas.height;
-
- // 创建一个新的canvas,保持原始比例
- const tempCanvas = document.createElement('canvas');
- const ctx = tempCanvas.getContext('2d', {
- alpha: format === 'png',
- willReadFrequently: false,
- desynchronized: true
- });
-
- // 设置输出尺寸,保持原始比例
- const maxWidth = 1600; // 增加最大宽度以提高清晰度
- const scale = maxWidth / originalWidth;
- tempCanvas.width = maxWidth;
- tempCanvas.height = originalHeight * scale;
-
- // 使用高质量的图像平滑
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
-
- // 绘制调整后的图像,保持比例
- ctx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height);
-
- // 如果是PNG格式,尝试优化透明度处理
- if (format === 'png') {
- const imageData = ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
- ctx.putImageData(imageData, 0, 0);
- }
-
- tempCanvas.toBlob(blob => {
- const templateName = window.currentTemplate ? window.currentTemplate.name : 'poster';
- saveAs(blob, `${templateName}-${Date.now()}.${format}`);
- cleanup();
- }, mimeType, exportQuality);
- } catch (error) {
- console.error('导出图片时发生错误:', error);
- cleanup();
- }
- }).catch(error => {
- console.error('下载海报失败:', error);
- alert('生成海报失败,请重试');
- cleanup();
- });
- }
|