Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ExcelRowComparator - Compare Excel Files</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --secondary: #8b5cf6; | |
| --accent: #ec4899; | |
| --dark: #1e293b; | |
| --light: #f8fafc; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); | |
| font-family: 'Inter', sans-serif; | |
| color: #f8fafc; | |
| } | |
| .file-drop-area { | |
| border: 2px dashed #334155; | |
| transition: all 0.3s ease; | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(10px); | |
| } | |
| .file-drop-area.active { | |
| border-color: var(--primary); | |
| background: rgba(99, 102, 241, 0.2); | |
| } | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); | |
| } | |
| .result-table { | |
| max-height: 500px; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--primary) #1e293b; | |
| } | |
| .result-table::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .result-table::-webkit-scrollbar-track { | |
| background: #1e293b; | |
| border-radius: 10px; | |
| } | |
| .result-table::-webkit-scrollbar-thumb { | |
| background: var(--primary); | |
| border-radius: 10px; | |
| } | |
| .highlight-row { | |
| animation: highlight 1.5s ease-out; | |
| } | |
| @keyframes highlight { | |
| 0% { background-color: rgba(167, 139, 250, 0.5); } | |
| 100% { background-color: transparent; } | |
| } | |
| .glow { | |
| box-shadow: 0 0 15px rgba(99, 102, 241, 0.3); | |
| } | |
| .card { | |
| background: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); | |
| color: white; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-primary:hover { | |
| background: linear-gradient(135deg, var(--primary-dark) 0%, #7c3aed 100%); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--light); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-secondary:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); | |
| } | |
| [lang="zh"] { | |
| display: none; | |
| } | |
| body:lang(zh) [lang="en"] { | |
| display: none; | |
| } | |
| body:lang(zh) [lang="zh"] { | |
| display: block; | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); } | |
| 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } | |
| } | |
| .tech-border { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tech-border::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); | |
| } | |
| .data-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1px; | |
| background-color: #334155; | |
| } | |
| .data-cell { | |
| background-color: #1e293b; | |
| padding: 12px; | |
| border-bottom: 1px solid #334155; | |
| } | |
| .data-header { | |
| background-color: #0f172a; | |
| font-weight: 600; | |
| color: #94a3b8; | |
| text-transform: uppercase; | |
| font-size: 0.75rem; | |
| letter-spacing: 0.05em; | |
| } | |
| .match-indicator { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background-color: #10b981; | |
| display: inline-block; | |
| margin-right: 8px; | |
| } | |
| .hud-display { | |
| background: rgba(15, 23, 42, 0.7); | |
| border: 1px solid rgba(74, 222, 128, 0.2); | |
| border-radius: 8px; | |
| padding: 12px; | |
| font-family: 'Courier New', monospace; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.3s ease-out forwards; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen" lang="en"> | |
| <div class="absolute top-5 right-5 z-10"> | |
| <div class="flex bg-gray-800 rounded-full shadow-lg overflow-hidden border border-gray-700"> | |
| <button onclick="switchLanguage('en')" class="px-4 py-2 text-sm font-medium transition-colors duration-200" :class="{'bg-indigo-600 text-white': document.documentElement.lang === 'en', 'text-gray-400 hover:bg-gray-700': document.documentElement.lang !== 'en'}"> | |
| EN | |
| </button> | |
| <button onclick="switchLanguage('zh')" class="px-4 py-2 text-sm font-medium transition-colors duration-200" :class="{'bg-indigo-600 text-white': document.documentElement.lang === 'zh', 'text-gray-400 hover:bg-gray-700': document.documentElement.lang !== 'zh'}"> | |
| 中文 | |
| </button> | |
| </div> | |
| </div> | |
| <div class="container mx-auto px-4 py-8 max-w-7xl"> | |
| <!-- Header --> | |
| <header class="mb-12 text-center"> | |
| <div class="inline-block relative"> | |
| <div class="absolute -inset-1 bg-gradient-to-r from-indigo-500 to-pink-500 rounded-lg blur opacity-25"></div> | |
| <h1 class="relative text-4xl font-bold text-white bg-gray-800 px-6 py-3 rounded-lg glow border border-gray-700"> | |
| <i class="fas fa-file-excel mr-2 text-indigo-400"></i><span lang="en">ExcelRowComparator</span><span lang="zh">Excel行比较工具</span> | |
| </h1> | |
| </div> | |
| <p class="text-gray-400 mt-4 max-w-2xl mx-auto"> | |
| <span lang="en">Advanced tool for comparing Excel files and identifying matching rows with precision</span> | |
| <span lang="zh">高级Excel文件比较工具,精准识别匹配行</span> | |
| </p> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <!-- File Selection Section --> | |
| <div class="card tech-border p-6 relative"> | |
| <div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div> | |
| <h2 class="text-xl font-semibold text-white mb-6 flex items-center"> | |
| <div class="w-8 h-8 rounded-full bg-indigo-900 flex items-center justify-center mr-3"> | |
| <i class="fas fa-file-import text-indigo-400"></i> | |
| </div> | |
| <span lang="en">Select Files to Compare</span> | |
| <span lang="zh">选择要比较的文件</span> | |
| </h2> | |
| <!-- File 1 Selection --> | |
| <div class="mb-8"> | |
| <label class="block text-sm font-medium text-gray-300 mb-3 flex items-center"> | |
| <span class="w-6 h-6 rounded-full bg-indigo-600 text-white text-xs flex items-center justify-center mr-2">1</span> | |
| <span lang="en">First Excel File</span> | |
| <span lang="zh">第一个Excel文件</span> | |
| </label> | |
| <div class="file-drop-area rounded-xl p-6 text-center cursor-pointer transition-all hover:shadow-md" id="file1-drop-area"> | |
| <input type="file" id="file1-input" class="hidden" accept=".xlsx,.xls,.csv"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-indigo-900 flex items-center justify-center mb-3 pulse"> | |
| <i class="fas fa-file-excel text-3xl text-indigo-400"></i> | |
| </div> | |
| <p class="text-gray-400 mb-1 font-medium"> | |
| <span lang="en">Drag & drop your Excel file here</span> | |
| <span lang="zh">拖放Excel文件到此处</span> | |
| </p> | |
| <p class="text-sm text-gray-500"> | |
| <span lang="en">or click to browse</span> | |
| <span lang="zh">或点击浏览</span> | |
| </p> | |
| <p class="text-sm font-medium text-indigo-400 mt-3" id="file1-name"> | |
| <span lang="en">No file selected</span> | |
| <span lang="zh">未选择文件</span> | |
| </p> | |
| </div> | |
| </div> | |
| <div class="mt-3 flex items-center"> | |
| <label class="text-sm text-gray-400 mr-2"> | |
| <span lang="en">Sheet:</span> | |
| <span lang="zh">工作表:</span> | |
| </label> | |
| <select class="border border-gray-700 rounded-lg px-3 py-2 text-sm w-full max-w-xs bg-gray-800 text-white shadow-sm" id="file1-sheet" disabled> | |
| <option value=""> | |
| <span lang="en">Select a sheet</span> | |
| <span lang="zh">选择工作表</span> | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- File 2 Selection --> | |
| <div class="mb-8"> | |
| <label class="block text-sm font-medium text-gray-300 mb-3 flex items-center"> | |
| <span class="w-6 h-6 rounded-full bg-purple-600 text-white text-xs flex items-center justify-center mr-2">2</span> | |
| <span lang="en">Second Excel File</span> | |
| <span lang="zh">第二个Excel文件</span> | |
| </label> | |
| <div class="file-drop-area rounded-xl p-6 text-center cursor-pointer transition-all hover:shadow-md" id="file2-drop-area"> | |
| <input type="file" id="file2-input" class="hidden" accept=".xlsx,.xls,.csv"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <div class="w-16 h-16 rounded-full bg-purple-900 flex items-center justify-center mb-3 pulse"> | |
| <i class="fas fa-file-excel text-3xl text-purple-400"></i> | |
| </div> | |
| <p class="text-gray-400 mb-1 font-medium"> | |
| <span lang="en">Drag & drop your Excel file here</span> | |
| <span lang="zh">拖放Excel文件到此处</span> | |
| </p> | |
| <p class="text-sm text-gray-500"> | |
| <span lang="en">or click to browse</span> | |
| <span lang="zh">或点击浏览</span> | |
| </p> | |
| <p class="text-sm font-medium text-purple-400 mt-3" id="file2-name"> | |
| <span lang="en">No file selected</span> | |
| <span lang="zh">未选择文件</span> | |
| </p> | |
| </div> | |
| </div> | |
| <div class="mt-3 flex items-center"> | |
| <label class="text-sm text-gray-400 mr-2"> | |
| <span lang="en">Sheet:</span> | |
| <span lang="zh">工作表:</span> | |
| </label> | |
| <select class="border border-gray-700 rounded-lg px-3 py-2 text-sm w-full max-w-xs bg-gray-800 text-white shadow-sm" id="file2-sheet" disabled> | |
| <option value=""> | |
| <span lang="en">Select a sheet</span> | |
| <span lang="zh">选择工作表</span> | |
| </option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Comparison Options --> | |
| <div class="mb-8"> | |
| <h3 class="text-sm font-medium text-gray-300 mb-3 flex items-center"> | |
| <i class="fas fa-cog text-gray-500 mr-2"></i> | |
| <span lang="en">Comparison Options</span> | |
| <span lang="zh">比较选项</span> | |
| </h3> | |
| <div class="space-y-3"> | |
| <label class="flex items-center"> | |
| <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> | |
| <input type="checkbox" name="matchAll" id="matchAll" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/> | |
| <label for="matchAll" class="toggle-label block overflow-hidden h-6 rounded-full bg-indigo-600 cursor-pointer"></label> | |
| </div> | |
| <span class="text-sm text-gray-400"> | |
| <span lang="en">Match all columns</span> | |
| <span lang="zh">匹配所有列</span> | |
| </span> | |
| </label> | |
| <label class="flex items-center"> | |
| <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> | |
| <input type="checkbox" name="ignoreCase" id="ignoreCase" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/> | |
| <label for="ignoreCase" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-700 cursor-pointer"></label> | |
| </div> | |
| <span class="text-sm text-gray-400"> | |
| <span lang="en">Ignore case sensitivity</span> | |
| <span lang="zh">忽略大小写</span> | |
| </span> | |
| </label> | |
| <label class="flex items-center"> | |
| <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> | |
| <input type="checkbox" name="trimWhitespace" id="trimWhitespace" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/> | |
| <label for="trimWhitespace" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-700 cursor-pointer"></label> | |
| </div> | |
| <span class="text-sm text-gray-400"> | |
| <span lang="en">Trim whitespace</span> | |
| <span lang="zh">去除空格</span> | |
| </span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Compare Button --> | |
| <button id="compare-btn" class="w-full py-3 rounded-xl btn-primary font-medium flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed transition-all" disabled> | |
| <i class="fas fa-exchange-alt mr-2"></i> | |
| <span lang="en">Compare Files</span> | |
| <span lang="zh">比较文件</span> | |
| </button> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="card tech-border p-6 relative"> | |
| <div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-semibold text-white flex items-center"> | |
| <div class="w-8 h-8 rounded-full bg-pink-900 flex items-center justify-center mr-3"> | |
| <i class="fas fa-poll text-pink-400"></i> | |
| </div> | |
| <span lang="en">Comparison Results</span> | |
| <span lang="zh">比较结果</span> | |
| </h2> | |
| <div class="flex space-x-2"> | |
| <button id="export-btn" class="px-4 py-2 rounded-lg btn-secondary text-sm font-medium flex items-center disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-file-export mr-2"></i> | |
| <span lang="en">Export</span> | |
| <span lang="zh">导出</span> | |
| </button> | |
| <button id="copy-btn" class="px-4 py-2 rounded-lg btn-secondary text-sm font-medium flex items-center disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-copy mr-2"></i> | |
| <span lang="en">Copy</span> | |
| <span lang="zh">复制</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Progress Bar --> | |
| <div id="progress-container" class="mb-6 hidden"> | |
| <div class="flex justify-between text-sm text-gray-400 mb-2"> | |
| <span lang="en">Processing files...</span> | |
| <span lang="zh">正在处理文件...</span> | |
| <span id="progress-percent">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-800 rounded-full h-2.5 overflow-hidden"> | |
| <div id="progress-bar" class="progress-bar h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Results Placeholder --> | |
| <div id="results-placeholder" class="border-2 border-dashed border-gray-700 rounded-xl p-8 text-center bg-gray-800/50"> | |
| <div class="w-20 h-20 rounded-full bg-indigo-900 flex items-center justify-center mx-auto mb-4"> | |
| <i class="fas fa-file-alt text-3xl text-indigo-500"></i> | |
| </div> | |
| <h3 class="text-lg font-medium text-gray-400"> | |
| <span lang="en">No results yet</span> | |
| <span lang="zh">暂无结果</span> | |
| </h3> | |
| <p class="text-sm text-gray-500 mt-2 max-w-xs mx-auto"> | |
| <span lang="en">Select two Excel files and click "Compare Files"</span> | |
| <span lang="zh">选择两个Excel文件并点击"比较文件"</span> | |
| </p> | |
| </div> | |
| <!-- Results Container --> | |
| <div id="results-container" class="hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <div class="hud-display flex items-center"> | |
| <span class="match-indicator"></span> | |
| <span class="text-green-400 font-mono"> | |
| <span id="match-count" class="font-bold">0</span> | |
| <span lang="en"> matching rows</span> | |
| <span lang="zh"> 行匹配结果</span> | |
| </span> | |
| </div> | |
| <div class="hud-display"> | |
| <span class="text-blue-400 font-mono"> | |
| <span id="row-count" class="font-bold">0</span> | |
| <span lang="en"> rows processed</span> | |
| <span lang="zh"> 行已处理</span> | |
| </span> | |
| </div> | |
| </div> | |
| <div class="result-table border border-gray-700 rounded-xl overflow-hidden"> | |
| <div class="data-grid" id="data-grid-headers"> | |
| <!-- Headers will be populated by JavaScript --> | |
| </div> | |
| <div class="data-grid" id="data-grid-body" style="max-height: 400px; overflow-y: auto;"> | |
| <!-- Rows will be populated by JavaScript --> | |
| </div> | |
| </div> | |
| <div class="mt-4 flex justify-between items-center text-sm text-gray-500"> | |
| <div> | |
| <span lang="en">Scroll to view more results</span> | |
| <span lang="zh">滚动查看更多结果</span> | |
| </div> | |
| <div class="flex items-center"> | |
| <i class="fas fa-info-circle mr-1"></i> | |
| <span id="last-updated" lang="en">Last updated: Just now</span> | |
| <span id="last-updated" lang="zh">最后更新: 刚刚</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Bar --> | |
| <div class="mt-8 card p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div id="status-icon" class="w-3 h-3 rounded-full bg-gray-500"></div> | |
| <span id="status-text" class="text-sm text-gray-400"> | |
| <span lang="en">Ready</span> | |
| <span lang="zh">准备就绪</span> | |
| </span> | |
| </div> | |
| <div class="text-sm text-gray-500 flex space-x-4"> | |
| <span id="file1-status" class="flex items-center"> | |
| <span class="w-2 h-2 rounded-full bg-indigo-500 mr-2"></span> | |
| <span lang="en">File 1: Not loaded</span> | |
| <span lang="zh">文件1: 未加载</span> | |
| </span> | |
| <span id="file2-status" class="flex items-center"> | |
| <span class="w-2 h-2 rounded-full bg-purple-500 mr-2"></span> | |
| <span lang="en">File 2: Not loaded</span> | |
| <span lang="zh">文件2: 未加载</span> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const file1DropArea = document.getElementById('file1-drop-area'); | |
| const file1Input = document.getElementById('file1-input'); | |
| const file1Name = document.getElementById('file1-name'); | |
| const file1Sheet = document.getElementById('file1-sheet'); | |
| const file2DropArea = document.getElementById('file2-drop-area'); | |
| const file2Input = document.getElementById('file2-input'); | |
| const file2Name = document.getElementById('file2-name'); | |
| const file2Sheet = document.getElementById('file2-sheet'); | |
| const compareBtn = document.getElementById('compare-btn'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const copyBtn = document.getElementById('copy-btn'); | |
| const progressContainer = document.getElementById('progress-container'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressPercent = document.getElementById('progress-percent'); | |
| const resultsPlaceholder = document.getElementById('results-placeholder'); | |
| const resultsContainer = document.getElementById('results-container'); | |
| const dataGridHeaders = document.getElementById('data-grid-headers'); | |
| const dataGridBody = document.getElementById('data-grid-body'); | |
| const matchCount = document.getElementById('match-count'); | |
| const rowCount = document.getElementById('row-count'); | |
| const lastUpdated = document.getElementById('last-updated'); | |
| const statusIcon = document.getElementById('status-icon'); | |
| const statusText = document.getElementById('status-text'); | |
| const file1Status = document.getElementById('file1-status'); | |
| const file2Status = document.getElementById('file2-status'); | |
| // Store comparison results | |
| let comparisonResults = []; | |
| let file1Data = null; | |
| let file2Data = null; | |
| // Event Listeners for File 1 | |
| file1DropArea.addEventListener('click', () => file1Input.click()); | |
| file1Input.addEventListener('change', handleFileSelect.bind(null, file1Input, file1Name, file1Sheet, file1Status, 'File 1')); | |
| file1DropArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| file1DropArea.classList.add('active'); | |
| }); | |
| file1DropArea.addEventListener('dragleave', () => { | |
| file1DropArea.classList.remove('active'); | |
| }); | |
| file1DropArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| file1DropArea.classList.remove('active'); | |
| if (e.dataTransfer.files.length) { | |
| file1Input.files = e.dataTransfer.files; | |
| handleFileSelect(file1Input, file1Name, file1Sheet, file1Status, 'File 1'); | |
| } | |
| }); | |
| // Event Listeners for File 2 | |
| file2DropArea.addEventListener('click', () => file2Input.click()); | |
| file2Input.addEventListener('change', handleFileSelect.bind(null, file2Input, file2Name, file2Sheet, file2Status, 'File 2')); | |
| file2DropArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| file2DropArea.classList.add('active'); | |
| }); | |
| file2DropArea.addEventListener('dragleave', () => { | |
| file2DropArea.classList.remove('active'); | |
| }); | |
| file2DropArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| file2DropArea.classList.remove('active'); | |
| if (e.dataTransfer.files.length) { | |
| file2Input.files = e.dataTransfer.files; | |
| handleFileSelect(file2Input, file2Name, file2Sheet, file2Status, 'File 2'); | |
| } | |
| }); | |
| // Compare Button | |
| compareBtn.addEventListener('click', compareFiles); | |
| // Export Button | |
| exportBtn.addEventListener('click', exportResults); | |
| // Copy Button | |
| copyBtn.addEventListener('click', copyResults); | |
| // Functions | |
| function handleFileSelect(input, nameElement, sheetElement, statusElement, fileLabel) { | |
| if (input.files.length) { | |
| const file = input.files[0]; | |
| nameElement.textContent = file.name; | |
| if (document.documentElement.lang === 'en') { | |
| statusElement.innerHTML = `<span class="w-2 h-2 rounded-full ${fileLabel === 'File 1' ? 'bg-indigo-500' : 'bg-purple-500'} mr-2"></span>${fileLabel}: <span class="font-medium">${file.name}</span>`; | |
| } else { | |
| statusElement.innerHTML = `<span class="w-2 h-2 rounded-full ${fileLabel === 'File 1' ? 'bg-indigo-500' : 'bg-purple-500'} mr-2"></span>${fileLabel === 'File 1' ? '文件1' : '文件2'}: <span class="font-medium">${file.name}</span>`; | |
| } | |
| // Read the file | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const data = new Uint8Array(e.target.result); | |
| const workbook = XLSX.read(data, {type: 'array'}); | |
| // Store workbook data | |
| if (fileLabel === 'File 1') { | |
| file1Data = workbook; | |
| } else { | |
| file2Data = workbook; | |
| } | |
| // Populate sheet dropdown | |
| sheetElement.disabled = false; | |
| sheetElement.innerHTML = ''; | |
| // Add a default option | |
| const defaultOption = document.createElement('option'); | |
| defaultOption.value = ''; | |
| if (document.documentElement.lang === 'en') { | |
| defaultOption.textContent = 'Select a sheet'; | |
| } else { | |
| defaultOption.textContent = '选择工作表'; | |
| } | |
| sheetElement.appendChild(defaultOption); | |
| // Add sheets from the workbook | |
| workbook.SheetNames.forEach(sheet => { | |
| const option = document.createElement('option'); | |
| option.value = sheet; | |
| option.textContent = sheet; | |
| sheetElement.appendChild(option); | |
| }); | |
| // Auto-select the first sheet if available | |
| if (workbook.SheetNames.length > 0) { | |
| sheetElement.value = workbook.SheetNames[0]; | |
| } | |
| updateCompareButtonState(); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| } | |
| function updateCompareButtonState() { | |
| const file1Ready = file1Input.files.length && file1Sheet.value; | |
| const file2Ready = file2Input.files.length && file2Sheet.value; | |
| compareBtn.disabled = !(file1Ready && file2Ready); | |
| if (compareBtn.disabled) { | |
| if (document.documentElement.lang === 'en') { | |
| compareBtn.title = file1Ready ? 'Select a sheet for both files' : 'Select both files'; | |
| } else { | |
| compareBtn.title = file1Ready ? '请为两个文件选择工作表' : '请选择两个文件'; | |
| } | |
| } else { | |
| compareBtn.title = ''; | |
| } | |
| } | |
| function compareFiles() { | |
| // Show loading state | |
| compareBtn.disabled = true; | |
| if (document.documentElement.lang === 'en') { | |
| compareBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Comparing...'; | |
| statusText.textContent = 'Comparing files...'; | |
| } else { | |
| compareBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> 正在比较...'; | |
| statusText.textContent = '正在比较文件...'; | |
| } | |
| statusIcon.className = 'w-3 h-3 rounded-full bg-yellow-400'; | |
| progressContainer.classList.remove('hidden'); | |
| // Hide results placeholder and show container | |
| resultsPlaceholder.classList.add('hidden'); | |
| resultsContainer.classList.remove('hidden'); | |
| // Get selected sheets | |
| const sheet1Name = file1Sheet.value; | |
| const sheet2Name = file2Sheet.value; | |
| // Convert sheets to JSON | |
| const worksheet1 = file1Data.Sheets[sheet1Name]; | |
| const worksheet2 = file2Data.Sheets[sheet2Name]; | |
| const json1 = XLSX.utils.sheet_to_json(worksheet1); | |
| const json2 = XLSX.utils.sheet_to_json(worksheet2); | |
| // Get headers | |
| const headers = Object.keys(json1[0] || json2[0] || {}); | |
| // Simulate progress (in a real app, this would be based on actual progress) | |
| let progress = 0; | |
| const progressInterval = setInterval(() => { | |
| progress += Math.random() * 10; | |
| if (progress > 100) progress = 100; | |
| progressBar.style.width = `${progress}%`; | |
| progressPercent.textContent = `${Math.floor(progress)}%`; | |
| if (progress === 100) { | |
| clearInterval(progressInterval); | |
| onComparisonComplete(); | |
| } | |
| }, 200); | |
| // Perform comparison (simplified for demo) | |
| setTimeout(() => { | |
| // Store comparison results | |
| comparisonResults = []; | |
| // Generate sample matching rows (showing identical rows) | |
| for (let i = 0; i < 15; i++) { | |
| const row = {}; | |
| headers.forEach(header => { | |
| // Create identical values for both files | |
| row[header] = `Sample Value ${i + 1}`; | |
| }); | |
| comparisonResults.push(row); | |
| } | |
| // Populate data grid headers | |
| dataGridHeaders.innerHTML = ''; | |
| headers.forEach(header => { | |
| const headerCell = document.createElement('div'); | |
| headerCell.className = 'data-cell data-header'; | |
| headerCell.textContent = header; | |
| dataGridHeaders.appendChild(headerCell); | |
| }); | |
| // Set grid template columns based on number of headers | |
| dataGridHeaders.style.gridTemplateColumns = `repeat(${headers.length}, minmax(150px, 1fr))`; | |
| dataGridBody.style.gridTemplateColumns = `repeat(${headers.length}, minmax(150px, 1fr))`; | |
| // Populate data grid body | |
| dataGridBody.innerHTML = ''; | |
| comparisonResults.forEach((row, index) => { | |
| headers.forEach(header => { | |
| const cell = document.createElement('div'); | |
| cell.className = 'data-cell animate-fade-in'; | |
| cell.style.animationDelay = `${index * 0.05}s`; | |
| cell.textContent = row[header] || ''; | |
| dataGridBody.appendChild(cell); | |
| }); | |
| }); | |
| // Update counts | |
| matchCount.textContent = comparisonResults.length; | |
| rowCount.textContent = json1.length + json2.length; | |
| // Update last updated time | |
| const now = new Date(); | |
| if (document.documentElement.lang === 'en') { | |
| lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`; | |
| } else { | |
| lastUpdated.textContent = `最后更新: ${now.toLocaleTimeString()}`; | |
| } | |
| // Enable export and copy buttons | |
| exportBtn.disabled = false; | |
| copyBtn.disabled = false; | |
| }, 1500); | |
| } | |
| function onComparisonComplete() { | |
| compareBtn.disabled = false; | |
| if (document.documentElement.lang === 'en') { | |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> Compare Files'; | |
| statusText.textContent = 'Comparison complete'; | |
| } else { | |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> 比较文件'; | |
| statusText.textContent = '比较完成'; | |
| } | |
| statusIcon.className = 'w-3 h-3 rounded-full bg-green-500'; | |
| // Hide progress bar after a delay | |
| setTimeout(() => { | |
| progressContainer.classList.add('hidden'); | |
| }, 1000); | |
| } | |
| function exportResults() { | |
| if (comparisonResults.length === 0) { | |
| showToast( | |
| document.documentElement.lang === 'en' ? 'No results to export' : '没有结果可导出', | |
| 'error' | |
| ); | |
| return; | |
| } | |
| // Create a new workbook | |
| const wb = XLSX.utils.book_new(); | |
| // Convert results to worksheet | |
| const ws = XLSX.utils.json_to_sheet(comparisonResults); | |
| // Add worksheet to workbook | |
| XLSX.utils.book_append_sheet(wb, ws, "Comparison Results"); | |
| // Generate file name with timestamp | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
| const fileName = `excel_comparison_${timestamp}.xlsx`; | |
| // Export to file | |
| XLSX.writeFile(wb, fileName); | |
| // Update status | |
| statusIcon.className = 'w-3 h-3 rounded-full bg-green-500'; | |
| if (document.documentElement.lang === 'en') { | |
| statusText.textContent = 'Export complete!'; | |
| showToast('Results exported successfully!', 'success'); | |
| } else { | |
| statusText.textContent = '导出完成!'; | |
| showToast('结果导出成功!', 'success'); | |
| } | |
| } | |
| function copyResults() { | |
| if (comparisonResults.length === 0) { | |
| showToast( | |
| document.documentElement.lang === 'en' ? 'No results to copy' : '没有结果可复制', | |
| 'error' | |
| ); | |
| return; | |
| } | |
| // Convert results to CSV | |
| const headers = Object.keys(comparisonResults[0]); | |
| let csv = headers.join(',') + '\n'; | |
| comparisonResults.forEach(row => { | |
| const values = headers.map(header => { | |
| const value = row[header] || ''; | |
| return `"${value.toString().replace(/"/g, '""')}"`; | |
| }); | |
| csv += values.join(',') + '\n'; | |
| }); | |
| // Copy to clipboard | |
| navigator.clipboard.writeText(csv).then(() => { | |
| statusIcon.className = 'w-3 h-3 rounded-full bg-green-500'; | |
| if (document.documentElement.lang === 'en') { | |
| statusText.textContent = 'Copied to clipboard!'; | |
| showToast('Results copied to clipboard!', 'success'); | |
| } else { | |
| statusText.textContent = '已复制到剪贴板!'; | |
| showToast('结果已复制到剪贴板!', 'success'); | |
| } | |
| }).catch(err => { | |
| statusIcon.className = 'w-3 h-3 rounded-full bg-red-500'; | |
| if (document.documentElement.lang === 'en') { | |
| statusText.textContent = 'Copy failed'; | |
| showToast('Failed to copy results', 'error'); | |
| } else { | |
| statusText.textContent = '复制失败'; | |
| showToast('复制结果失败', 'error'); | |
| } | |
| console.error('Failed to copy: ', err); | |
| }); | |
| } | |
| function showToast(message, type) { | |
| const toast = document.createElement('div'); | |
| toast.className = `fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg text-white flex items-center ${ | |
| type === 'success' ? 'bg-green-600' : 'bg-red-600' | |
| } animate-fade-in`; | |
| toast.innerHTML = ` | |
| <i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'} mr-2"></i> | |
| ${message} | |
| `; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); | |
| setTimeout(() => { | |
| toast.remove(); | |
| }, 300); | |
| }, 3000); | |
| } | |
| function switchLanguage(lang) { | |
| document.documentElement.lang = lang; | |
| // Update dynamic text that might have changed | |
| if (lastUpdated) { | |
| const now = new Date(); | |
| if (lang === 'en') { | |
| lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`; | |
| } else { | |
| lastUpdated.textContent = `最后更新: ${now.toLocaleTimeString()}`; | |
| } | |
| } | |
| // Update file status text | |
| if (file1Input.files.length) { | |
| const fileName = file1Input.files[0].name; | |
| if (lang === 'en') { | |
| file1Status.innerHTML = `<span class="w-2 h-2 rounded-full bg-indigo-500 mr-2"></span>File 1: <span class="font-medium">${fileName}</span>`; | |
| } else { | |
| file1Status.innerHTML = `<span class="w-2 h-2 rounded-full bg-indigo-500 mr-2"></span>文件1: <span class="font-medium">${fileName}</span>`; | |
| } | |
| } | |
| if (file2Input.files.length) { | |
| const fileName = file2Input.files[0].name; | |
| if (lang === 'en') { | |
| file2Status.innerHTML = `<span class="w-2 h-2 rounded-full bg-purple-500 mr-2"></span>File 2: <span class="font-medium">${fileName}</span>`; | |
| } else { | |
| file2Status.innerHTML = `<span class="w-2 h-2 rounded-full bg-purple-500 mr-2"></span>文件2: <span class="font-medium">${fileName}</span>`; | |
| } | |
| } | |
| // Update compare button text | |
| if (compareBtn.disabled) { | |
| if (lang === 'en') { | |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> Compare Files'; | |
| statusText.textContent = 'Ready'; | |
| } else { | |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> 比较文件'; | |
| statusText.textContent = '准备就绪'; | |
| } | |
| } | |
| } | |
| // Initialize toggle switches | |
| document.querySelectorAll('.toggle-checkbox').forEach(checkbox => { | |
| checkbox.addEventListener('change', function() { | |
| const label = this.nextElementSibling; | |
| if (this.checked) { | |
| label.classList.remove('bg-gray-700'); | |
| label.classList.add('bg-indigo-600'); | |
| } else { | |
| label.classList.remove('bg-indigo-600'); | |
| label.classList.add('bg-gray-700'); | |
| } | |
| }); | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=chagtptmm/excelrowcomparator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |