| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ExcelRowComparator - Find Matching Rows</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; |
| } |
| |
| .key-column { |
| background-color: rgba(99, 102, 241, 0.15); |
| border-left: 3px solid var(--primary); |
| } |
| </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 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">Find matching rows between two Excel files with identical columns</span> |
| <span lang="zh">在具有相同列的两个Excel文件中查找匹配行</span> |
| </p> |
| </header> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <div class="mb-8"> |
| <h3 class="text-sm font-medium text-gray-300 mb-3 flex items-center"> |
| <i class="fas fa-key text-gray-500 mr-2"></i> |
| <span lang="en">Key Column (Optional)</span> |
| <span lang="zh">关键列(可选)</span> |
| </h3> |
| <select id="key-column-select" class="border border-gray-700 rounded-lg px-3 py-2 text-sm w-full bg-gray-800 text-white shadow-sm" disabled> |
| <option value="">All columns (default)</option> |
| </select> |
| <p class="text-xs text-gray-500 mt-1"> |
| <span lang="en">Select a column to use as matching key (leave blank to match all columns)</span> |
| <span lang="zh">选择作为匹配关键字的列(留空则匹配所有列)</span> |
| </p> |
| </div> |
| |
| |
| <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="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" checked/> |
| <label for="trimWhitespace" 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">Trim whitespace</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="showOnlyMatches" id="showOnlyMatches" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer" checked/> |
| <label for="showOnlyMatches" 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">Show only matching rows</span> |
| <span lang="zh">仅显示匹配行</span> |
| </span> |
| </label> |
| </div> |
| </div> |
| |
| |
| <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">Find Matching Rows</span> |
| <span lang="zh">查找匹配行</span> |
| </button> |
| </div> |
| |
| |
| <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">Matching Rows</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> |
| |
| |
| <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> |
| |
| |
| <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 "Find Matching Rows"</span> |
| <span lang="zh">选择两个Excel文件并点击"查找匹配行"</span> |
| </p> |
| </div> |
| |
| |
| <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 found</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"> |
| |
| </div> |
| <div class="data-grid" id="data-grid-body" style="max-height: 400px; overflow-y: auto;"> |
| |
| </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> |
| |
| |
| <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() { |
| |
| 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 keyColumnSelect = document.getElementById('key-column-select'); |
| const ignoreCaseCheckbox = document.getElementById('ignoreCase'); |
| const trimWhitespaceCheckbox = document.getElementById('trimWhitespace'); |
| const showOnlyMatchesCheckbox = document.getElementById('showOnlyMatches'); |
| |
| 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'); |
| |
| |
| let comparisonResults = []; |
| let file1Data = null; |
| let file2Data = null; |
| let headers = []; |
| |
| |
| 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'); |
| } |
| }); |
| |
| |
| 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'); |
| } |
| }); |
| |
| |
| compareBtn.addEventListener('click', compareFiles); |
| |
| |
| exportBtn.addEventListener('click', exportResults); |
| |
| |
| copyBtn.addEventListener('click', copyResults); |
| |
| |
| file1Sheet.addEventListener('change', updateKeyColumnSelect); |
| file2Sheet.addEventListener('change', updateKeyColumnSelect); |
| |
| |
| 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>`; |
| } |
| |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, {type: 'array'}); |
| |
| |
| if (fileLabel === 'File 1') { |
| file1Data = workbook; |
| } else { |
| file2Data = workbook; |
| } |
| |
| |
| sheetElement.disabled = false; |
| sheetElement.innerHTML = ''; |
| |
| |
| const defaultOption = document.createElement('option'); |
| defaultOption.value = ''; |
| if (document.documentElement.lang === 'en') { |
| defaultOption.textContent = 'Select a sheet'; |
| } else { |
| defaultOption.textContent = '选择工作表'; |
| } |
| sheetElement.appendChild(defaultOption); |
| |
| |
| workbook.SheetNames.forEach(sheet => { |
| const option = document.createElement('option'); |
| option.value = sheet; |
| option.textContent = sheet; |
| sheetElement.appendChild(option); |
| }); |
| |
| |
| if (workbook.SheetNames.length > 0) { |
| sheetElement.value = workbook.SheetNames[0]; |
| } |
| |
| updateCompareButtonState(); |
| }; |
| reader.readAsArrayBuffer(file); |
| } |
| } |
| |
| function updateKeyColumnSelect() { |
| |
| if (!file1Sheet.value || !file2Sheet.value || !file1Data || !file2Data) { |
| keyColumnSelect.disabled = true; |
| return; |
| } |
| |
| |
| const worksheet1 = file1Data.Sheets[file1Sheet.value]; |
| const json1 = XLSX.utils.sheet_to_json(worksheet1, {header: 1}); |
| |
| |
| headers = json1.length > 0 ? json1[0] : []; |
| |
| |
| keyColumnSelect.innerHTML = ''; |
| keyColumnSelect.disabled = false; |
| |
| |
| const defaultOption = document.createElement('option'); |
| defaultOption.value = ''; |
| defaultOption.textContent = document.documentElement.lang === 'en' ? 'All columns (default)' : '所有列(默认)'; |
| keyColumnSelect.appendChild(defaultOption); |
| |
| |
| headers.forEach(header => { |
| const option = document.createElement('option'); |
| option.value = header; |
| option.textContent = header; |
| keyColumnSelect.appendChild(option); |
| }); |
| } |
| |
| 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() { |
| |
| 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'); |
| |
| |
| resultsPlaceholder.classList.add('hidden'); |
| resultsContainer.classList.remove('hidden'); |
| |
| |
| const sheet1Name = file1Sheet.value; |
| const sheet2Name = file2Sheet.value; |
| |
| |
| 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); |
| |
| |
| if (headers.length === 0) { |
| headers = Object.keys(json1[0] || json2[0] || {}); |
| } |
| |
| |
| const keyColumn = keyColumnSelect.value; |
| const ignoreCase = ignoreCaseCheckbox.checked; |
| const trimWhitespace = trimWhitespaceCheckbox.checked; |
| const showOnlyMatches = showOnlyMatchesCheckbox.checked; |
| |
| |
| 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); |
| } |
| }, 200); |
| |
| |
| setTimeout(() => { |
| |
| comparisonResults = []; |
| |
| |
| const file2Map = new Map(); |
| |
| |
| json2.forEach(row => { |
| const key = keyColumn ? |
| (ignoreCase ? String(row[keyColumn]).toLowerCase() : String(row[keyColumn])) : |
| JSON.stringify(normalizeRow(row, headers, ignoreCase, trimWhitespace)); |
| |
| if (!file2Map.has(key)) { |
| file2Map.set(key, []); |
| } |
| file2Map.get(key).push(row); |
| }); |
| |
| |
| json1.forEach(row1 => { |
| const key = keyColumn ? |
| (ignoreCase ? String(row1[keyColumn]).toLowerCase() : String(row1[keyColumn])) : |
| JSON.stringify(normalizeRow(row1, headers, ignoreCase, trimWhitespace)); |
| |
| if (file2Map.has(key)) { |
| |
| const matchingRows = file2Map.get(key); |
| matchingRows.forEach(row2 => { |
| |
| const resultRow = {}; |
| headers.forEach(header => { |
| resultRow[`${header} (File 1)`] = row1[header]; |
| resultRow[`${header} (File 2)`] = row2[header]; |
| }); |
| comparisonResults.push(resultRow); |
| }); |
| } else if (!showOnlyMatches) { |
| |
| const resultRow = {}; |
| headers.forEach(header => { |
| resultRow[`${header} (File 1)`] = row1[header]; |
| resultRow[`${header} (File 2)`] = ''; |
| }); |
| comparisonResults.push(resultRow); |
| } |
| }); |
| |
| |
| if (!showOnlyMatches) { |
| const matchedKeys = new Set(); |
| json1.forEach(row1 => { |
| const key = keyColumn ? |
| (ignoreCase ? String(row1[keyColumn]).toLowerCase() : String(row1[keyColumn])) : |
| JSON.stringify(normalizeRow(row1, headers, ignoreCase, trimWhitespace)); |
| matchedKeys.add(key); |
| }); |
| |
| json2.forEach(row2 => { |
| const key = keyColumn ? |
| (ignoreCase ? String(row2[keyColumn]).toLowerCase() : String(row2[keyColumn])) : |
| JSON.stringify(normalizeRow(row2, headers, ignoreCase, trimWhitespace)); |
| |
| if (!matchedKeys.has(key)) { |
| const resultRow = {}; |
| headers.forEach(header => { |
| resultRow[`${header} (File 1)`] = ''; |
| resultRow[`${header} (File 2)`] = row2[header]; |
| }); |
| comparisonResults.push(resultRow); |
| } |
| }); |
| } |
| |
| |
| const displayHeaders = []; |
| headers.forEach(header => { |
| displayHeaders.push(`${header} (File 1)`); |
| displayHeaders.push(`${header} (File 2)`); |
| }); |
| |
| |
| dataGridHeaders.innerHTML = ''; |
| displayHeaders.forEach(header => { |
| const headerCell = document.createElement('div'); |
| headerCell.className = 'data-cell data-header'; |
| headerCell.textContent = header; |
| |
| |
| if (keyColumn && header.includes(keyColumn)) { |
| headerCell.classList.add('key-column'); |
| } |
| |
| dataGridHeaders.appendChild(headerCell); |
| }); |
| |
| |
| dataGridHeaders.style.gridTemplateColumns = `repeat(${displayHeaders.length}, minmax(150px, 1fr))`; |
| dataGridBody.style.gridTemplateColumns = `repeat(${displayHeaders.length}, minmax(150px, 1fr))`; |
| |
| |
| dataGridBody.innerHTML = ''; |
| comparisonResults.forEach((row, index) => { |
| displayHeaders.forEach(header => { |
| const cell = document.createElement('div'); |
| cell.className = 'data-cell animate-fade-in'; |
| cell.style.animationDelay = `${index * 0.05}s`; |
| |
| |
| const isMatch = row[header] !== '' && |
| row[header.replace(' (File 1)', ' (File 2)')] !== undefined; |
| |
| |
| cell.textContent = row[header] !== undefined ? row[header] : ''; |
| |
| |
| if (keyColumn && header.includes(keyColumn)) { |
| cell.classList.add('key-column'); |
| } |
| |
| |
| if (isMatch) { |
| cell.style.backgroundColor = 'rgba(16, 185, 129, 0.1)'; |
| } |
| |
| dataGridBody.appendChild(cell); |
| }); |
| }); |
| |
| |
| const actualMatches = comparisonResults.filter(row => { |
| return headers.some(header => row[`${header} (File 1)`] && row[`${header} (File 2)`]); |
| }).length; |
| |
| matchCount.textContent = actualMatches; |
| rowCount.textContent = json1.length + json2.length; |
| |
| |
| const now = new Date(); |
| if (document.documentElement.lang === 'en') { |
| lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`; |
| } else { |
| lastUpdated.textContent = `最后更新: ${now.toLocaleTimeString()}`; |
| } |
| |
| |
| exportBtn.disabled = false; |
| copyBtn.disabled = false; |
| |
| |
| onComparisonComplete(); |
| }, 1000); |
| } |
| |
| function normalizeRow(row, headers, ignoreCase, trimWhitespace) { |
| const normalized = {}; |
| headers.forEach(header => { |
| let value = row[header] !== undefined ? String(row[header]) : ''; |
| if (trimWhitespace) value = value.trim(); |
| if (ignoreCase) value = value.toLowerCase(); |
| normalized[header] = value; |
| }); |
| return normalized; |
| } |
| |
| function onComparisonComplete() { |
| compareBtn.disabled = false; |
| if (document.documentElement.lang === 'en') { |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> Find Matching Rows'; |
| 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'; |
| |
| |
| setTimeout(() => { |
| progressContainer.classList.add('hidden'); |
| }, 1000); |
| } |
| |
| function exportResults() { |
| if (comparisonResults.length === 0) { |
| showToast( |
| document.documentElement.lang === 'en' ? 'No results to export' : '没有结果可导出', |
| 'error' |
| ); |
| return; |
| } |
| |
| |
| const wb = XLSX.utils.book_new(); |
| |
| |
| const ws = XLSX.utils.json_to_sheet(comparisonResults); |
| |
| |
| XLSX.utils.book_append_sheet(wb, ws, "Matching Rows"); |
| |
| |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
| const fileName = `excel_matching_rows_${timestamp}.xlsx`; |
| |
| |
| XLSX.writeFile(wb, fileName); |
| |
| |
| 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; |
| } |
| |
| |
| const displayHeaders = []; |
| headers.forEach(header => { |
| displayHeaders.push(`${header} (File 1)`); |
| displayHeaders.push(`${header} (File 2)`); |
| }); |
| |
| let csv = displayHeaders.join(',') + '\n'; |
| |
| comparisonResults.forEach(row => { |
| const values = displayHeaders.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> Find Matching Rows'; |
| statusText.textContent = 'Ready'; |
| } else { |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> 查找匹配行'; |
| statusText.textContent = '准备就绪'; |
| } |
| } |
| |
| // Update key column select default option |
| if (keyColumnSelect.options.length > 0) { |
| keyColumnSelect.options[0].textContent = lang === 'en' ? 'All columns (default)' : '所有列(默认)'; |
| } |
| } |
| |
| // 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/excelrow" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |