SVG修复器
由豆包生成。此工具用于修复PPT导出的SVG缺乏XML标签的问题。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SVG修复器</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> <script> tailwind.config = { theme: { extend: { colors: { primary: '#3B82F6', success: '#10B981', danger: '#EF4444', neutral: '#64748B' }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], }, } } } </script> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .file-drop-active { @apply border-primary bg-blue-50; } .transition-height { transition: max-height 0.3s ease-in-out; } } </style> </head> <body class="bg-gray-50 font-sans text-gray-800 min-h-screen flex flex-col"> <!-- 主要内容 --> <main class="flex-grow container mx-auto p-4 md:p-6"> <!-- 拖放上传区域 --> <div id="dropArea" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-8 cursor-pointer transition-all hover:border-primary"> <i class="fa fa-upload text-5xl text-gray-400 mb-4"></i> <p class="text-gray-600 mb-2">拖放SVG文件到此处,或</p> <label class="inline-block bg-primary text-white py-2 px-6 rounded-md cursor-pointer hover:bg-primary/90 transition-colors"> <i class="fa fa-file-image-o mr-1"></i> 选择文件 <input type="file" id="fileInput" accept=".svg" multiple class="hidden"> </label> </div> <!-- 文件列表 --> <div id="fileListContainer" class="mb-8"> <div class="flex justify-between items-center mb-4"> <h2 class="text-lg font-semibold">文件列表</h2> <div class="flex space-x-2"> <button id="fixAllBtn" class="bg-primary text-white py-1 px-4 rounded hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hidden"> <i class="fa fa-magic mr-1"></i> 全部修复 </button> <button id="downloadAllBtn" class="bg-success text-white py-1 px-4 rounded hover:bg-success/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hidden"> <i class="fa fa-download mr-1"></i> 全部下载 </button> <button id="clearAllBtn" class="bg-neutral text-white py-1 px-4 rounded hover:bg-neutral/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed hidden"> <i class="fa fa-trash mr-1"></i> 清空 </button> </div> </div> <div id="fileList" class="divide-y divide-gray-200 max-h-[500px] overflow-y-auto"> <!-- 文件项将通过JS动态添加 --> <div class="text-center text-gray-500 py-12"> <i class="fa fa-file-o text-3xl mb-2 block"></i> 未选择任何文件 </div> </div> </div> </main> <!-- 帮助提示 --> <div id="helpModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden"> <div class="bg-white rounded-lg p-6 max-w-md w-full m-4"> <div class="flex justify-between items-center mb-4"> <h3 class="text-lg font-bold text-primary">使用帮助</h3> <button id="closeHelpBtn" class="text-gray-500 hover:text-gray-700"> <i class="fa fa-times"></i> </button> </div> <div class="text-gray-700 space-y-3 text-sm"> <p>1. 拖放SVG文件到上传区域,或点击"选择文件"按钮上传</p> <p>2. 点击"修复"按钮处理单个文件,或"全部修复"处理所有文件</p> <p>3. 修复完成后,可下载单个文件或点击"全部下载"获取所有修复后的文件</p> <p class="text-primary">系统会为缺少XML声明的SVG添加:<code><?xml version="1.0" encoding="utf-8"?></code></p> </div> </div> </div> <script> // 存储处理后的文件 let processedFiles = []; // DOM元素 const dropArea = document.getElementById('dropArea'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const fixAllBtn = document.getElementById('fixAllBtn'); const downloadAllBtn = document.getElementById('downloadAllBtn'); const clearAllBtn = document.getElementById('clearAllBtn'); const helpBtn = document.getElementById('helpBtn'); const helpModal = document.getElementById('helpModal'); const closeHelpBtn = document.getElementById('closeHelpBtn'); // 事件监听:拖放功能 ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } ['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, unhighlight, false); }); function highlight() { dropArea.classList.add('file-drop-active'); } function unhighlight() { dropArea.classList.remove('file-drop-active'); } // 处理文件拖放 dropArea.addEventListener('drop', handleDrop, false); function handleDrop(e) { const dt = e.dataTransfer; const files = dt.files; handleFiles(files); } // 处理文件选择 fileInput.addEventListener('change', function() { handleFiles(this.files); }); // 点击上传区域触发文件选择 dropArea.addEventListener('click', function() { fileInput.click(); }); // 处理文件 function handleFiles(files) { // 过滤出SVG文件 const svgFiles = Array.from(files).filter(file => file.type === 'image/svg+xml' || file.name.endsWith('.svg')); if (svgFiles.length === 0) { showNotification('请选择SVG文件', 'error'); return; } // 清空初始提示 if (fileList.querySelector('.text-center')) { fileList.innerHTML = ''; } // 添加文件到列表 svgFiles.forEach(file => { addFileToUI(file); }); updateControlButtons(); } // 添加文件到UI function addFileToUI(file) { const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const fileItem = document.createElement('div'); fileItem.id = fileId; fileItem.className = 'p-4 flex flex-col md:flex-row md:items-center justify-between hover:bg-gray-50 transition-colors'; fileItem.innerHTML = ` <div class="flex items-center mb-2 md:mb-0"> <i class="fa fa-file-code-o text-primary mr-3 text-xl"></i> <div> <p class="font-medium truncate max-w-[200px] md:max-w-[300px]">${file.name}</p> <p class="text-xs text-gray-500">${formatFileSize(file.size)}</p> </div> </div> <div class="flex items-center space-x-2"> <span class="status text-sm px-2 py-1 rounded bg-yellow-100 text-yellow-800">等待处理</span> <button class="fix-btn p-1.5 text-primary hover:bg-primary/10 rounded transition-colors" data-file-id="${fileId}"> <i class="fa fa-wrench"></i> </button> <button class="download-btn p-1.5 text-success hover:bg-success/10 rounded transition-colors hidden" data-file-id="${fileId}"> <i class="fa fa-download"></i> </button> <button class="remove-btn p-1.5 text-gray-500 hover:bg-gray-100 rounded transition-colors" data-file-id="${fileId}"> <i class="fa fa-times"></i> </button> </div> `; fileList.appendChild(fileItem); // 存储文件引用 processedFiles.push({ id: fileId, originalFile: file, fixedFile: null, status: 'pending' }); // 添加事件监听 fileItem.querySelector('.fix-btn').addEventListener('click', () => fixFile(fileId)); fileItem.querySelector('.remove-btn').addEventListener('click', () => removeFile(fileId)); fileItem.querySelector('.download-btn').addEventListener('click', () => downloadFile(fileId)); } // 修复文件 function fixFile(fileId) { const fileData = processedFiles.find(f => f.id === fileId); if (!fileData) return; const fileItem = document.getElementById(fileId); const statusEl = fileItem.querySelector('.status'); const fixBtn = fileItem.querySelector('.fix-btn'); // 更新状态 statusEl.textContent = '处理中'; statusEl.className = 'status text-sm px-2 py-1 rounded bg-blue-100 text-blue-800'; fixBtn.disabled = true; const reader = new FileReader(); reader.onload = function(e) { try { const content = e.target.result; const xmlDeclaration = '<?xml version="1.0" encoding="utf-8"?>\n'; // 检查是否已包含正确的XML声明 let fixedContent = content; const hasValidDeclaration = content.trimStart().startsWith(xmlDeclaration.trim()); if (!hasValidDeclaration) { // 检查是否有错误的声明并替换 if (content.trimStart().startsWith('<?xml')) { fixedContent = content.replace(/<\?xml.*?\?>/, xmlDeclaration); } else { // 添加正确的声明 fixedContent = xmlDeclaration + content; } } // 创建修复后的文件 const blob = new Blob([fixedContent], { type: 'image/svg+xml' }); const fixedFileName = getFixedFileName(fileData.originalFile.name); // 更新文件数据 fileData.fixedFile = { blob: blob, name: fixedFileName }; fileData.status = 'fixed'; // 更新UI statusEl.textContent = hasValidDeclaration ? '无需修复' : '已修复'; statusEl.className = `status text-sm px-2 py-1 rounded ${hasValidDeclaration ? 'bg-green-100 text-green-800' : 'bg-success text-white'}`; fileItem.querySelector('.download-btn').classList.remove('hidden'); fixBtn.classList.add('hidden'); showNotification(`${fileData.originalFile.name} 处理完成`, 'success'); } catch (error) { statusEl.textContent = '处理失败'; statusEl.className = 'status text-sm px-2 py-1 rounded bg-danger text-white'; fileData.status = 'error'; showNotification(`处理失败: ${error.message}`, 'error'); } finally { fixBtn.disabled = false; updateControlButtons(); } }; reader.readAsText(fileData.originalFile); } // 修复所有文件 fixAllBtn.addEventListener('click', function() { const pendingFiles = processedFiles.filter(f => f.status === 'pending'); if (pendingFiles.length === 0) return; // 依次修复所有待处理文件 pendingFiles.forEach((file, index) => { // 延迟处理,避免同时处理过多文件 setTimeout(() => { fixFile(file.id); }, index * 300); }); }); // 下载文件 function downloadFile(fileId) { const fileData = processedFiles.find(f => f.id === fileId); if (!fileData || !fileData.fixedFile) return; const url = URL.createObjectURL(fileData.fixedFile.blob); const a = document.createElement('a'); a.href = url; a.download = fileData.fixedFile.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(`已下载 ${fileData.fixedFile.name}`, 'success'); } // 下载所有文件 downloadAllBtn.addEventListener('click', function() { const fixedFiles = processedFiles.filter(f => f.status === 'fixed' && f.fixedFile); if (fixedFiles.length === 0) { showNotification('没有可下载的文件', 'error'); return; } // 如果只有一个文件,直接下载 if (fixedFiles.length === 1) { downloadFile(fixedFiles[0].id); return; } // 多个文件,这里简单处理为依次下载 // 实际应用中可能需要打包成ZIP,但浏览器端实现复杂,这里简化处理 fixedFiles.forEach((file, index) => { setTimeout(() => { downloadFile(file.id); }, index * 500); }); showNotification(`开始下载 ${fixedFiles.length} 个文件`, 'success'); }); // 移除文件 function removeFile(fileId) { const fileItem = document.getElementById(fileId); if (fileItem) { fileItem.classList.add('opacity-0', 'scale-95'); fileItem.style.transition = 'all 0.2s ease-out'; setTimeout(() => { fileItem.remove(); // 更新文件列表 processedFiles = processedFiles.filter(f => f.id !== fileId); updateControlButtons(); // 如果列表为空,显示提示 if (processedFiles.length === 0) { fileList.innerHTML = ` <div class="text-center text-gray-500 py-12"> <i class="fa fa-file-o text-3xl mb-2 block"></i> 未选择任何文件 </div> `; } }, 200); } } // 清空所有文件 clearAllBtn.addEventListener('click', function() { if (processedFiles.length === 0) return; // 添加动画效果 const fileItems = fileList.querySelectorAll('div[id^="file-"]'); fileItems.forEach((item, index) => { setTimeout(() => { item.classList.add('opacity-0', 'scale-95'); item.style.transition = 'all 0.2s ease-out'; }, index * 50); }); setTimeout(() => { processedFiles = []; fileList.innerHTML = ` <div class="text-center text-gray-500 py-12"> <i class="fa fa-file-o text-3xl mb-2 block"></i> 未选择任何文件 </div> `; updateControlButtons(); }, fileItems.length * 50 + 200); }); // 更新控制按钮状态 function updateControlButtons() { const hasFiles = processedFiles.length > 0; const hasPendingFiles = processedFiles.some(f => f.status === 'pending'); const hasFixedFiles = processedFiles.some(f => f.status === 'fixed' && f.fixedFile); // 更新按钮显示状态 fixAllBtn.classList.toggle('hidden', !hasFiles || !hasPendingFiles); downloadAllBtn.classList.toggle('hidden', !hasFiles || !hasFixedFiles); clearAllBtn.classList.toggle('hidden', !hasFiles); // 更新按钮禁用状态 fixAllBtn.disabled = !hasPendingFiles; downloadAllBtn.disabled = !hasFixedFiles; clearAllBtn.disabled = !hasFiles; } // 辅助函数:格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 辅助函数:获取修复后的文件名 function getFixedFileName(originalName) { const dotIndex = originalName.lastIndexOf('.'); if (dotIndex === -1) { return originalName + '-fixed'; } return originalName.substring(0, dotIndex) + '-fixed' + originalName.substring(dotIndex); } // 显示通知 function showNotification(message, type = 'info') { // 检查是否已有通知,如有则移除 const existingNotify = document.querySelector('.notification'); if (existingNotify) { existingNotify.remove(); } // 创建通知元素 const notification = document.createElement('div'); notification.className = `notification fixed bottom-4 right-4 px-4 py-3 rounded shadow-lg transform transition-all duration-300 translate-y-20 opacity-0 z-50 ${ type === 'success' ? 'bg-success text-white' : type === 'error' ? 'bg-danger text-white' : 'bg-gray-800 text-white' }`; notification.innerHTML = ` <i class="fa ${ type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle' } mr-2"></i> ${message} `; document.body.appendChild(notification); // 显示通知 setTimeout(() => { notification.classList.remove('translate-y-20', 'opacity-0'); }, 10); // 3秒后隐藏通知 setTimeout(() => { notification.classList.add('translate-y-20', 'opacity-0'); setTimeout(() => { notification.remove(); }, 300); }, 3000); } // 帮助模态框 helpBtn.addEventListener('click', () => { helpModal.classList.remove('hidden'); // 添加淡入动画 setTimeout(() => { helpModal.querySelector('div').classList.add('scale-100'); helpModal.querySelector('div').classList.remove('scale-95', 'opacity-0'); }, 10); }); closeHelpBtn.addEventListener('click', () => { const modalContent = helpModal.querySelector('div'); modalContent.classList.add('scale-95', 'opacity-0'); modalContent.classList.remove('scale-100'); setTimeout(() => { helpModal.classList.add('hidden'); }, 300); }); // 点击模态框外部关闭 helpModal.addEventListener('click', (e) => { if (e.target === helpModal) { closeHelpBtn.click(); } }); // 初始化模态框样式 document.addEventListener('DOMContentLoaded', () => { const modalContent = helpModal.querySelector('div'); modalContent.classList.add('transition-all', 'duration-300', 'scale-95', 'opacity-0'); }); </script> </body> </html>
