excelrow / index.html
chagtptmm's picture
Add 2 files
7da885e verified
<!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 -->
<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>
<!-- 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>
<!-- Key Column Selection -->
<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>
<!-- 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="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>
<!-- 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">Find Matching Rows</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">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>
<!-- 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 "Find Matching Rows"</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 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">
<!-- 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 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');
// Store comparison results
let comparisonResults = [];
let file1Data = null;
let file2Data = null;
let headers = [];
// 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);
// Sheet change events
file1Sheet.addEventListener('change', updateKeyColumnSelect);
file2Sheet.addEventListener('change', updateKeyColumnSelect);
// 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 updateKeyColumnSelect() {
// Only update if both sheets are selected
if (!file1Sheet.value || !file2Sheet.value || !file1Data || !file2Data) {
keyColumnSelect.disabled = true;
return;
}
// Get the first sheet to determine columns
const worksheet1 = file1Data.Sheets[file1Sheet.value];
const json1 = XLSX.utils.sheet_to_json(worksheet1, {header: 1});
// Get headers from first row
headers = json1.length > 0 ? json1[0] : [];
// Populate key column select
keyColumnSelect.innerHTML = '';
keyColumnSelect.disabled = false;
// Add default option
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = document.documentElement.lang === 'en' ? 'All columns (default)' : '所有列(默认)';
keyColumnSelect.appendChild(defaultOption);
// Add column options
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() {
// 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 if not already set
if (headers.length === 0) {
headers = Object.keys(json1[0] || json2[0] || {});
}
// Get comparison options
const keyColumn = keyColumnSelect.value;
const ignoreCase = ignoreCaseCheckbox.checked;
const trimWhitespace = trimWhitespaceCheckbox.checked;
const showOnlyMatches = showOnlyMatchesCheckbox.checked;
// 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);
}
}, 200);
// Perform comparison
setTimeout(() => {
// Store comparison results
comparisonResults = [];
// Create a map of rows from file2 for quick lookup
const file2Map = new Map();
// Process file2 first to create a lookup 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);
});
// Compare file1 rows against file2
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)) {
// Found matching rows in file2
const matchingRows = file2Map.get(key);
matchingRows.forEach(row2 => {
// Create a combined result showing both rows
const resultRow = {};
headers.forEach(header => {
resultRow[`${header} (File 1)`] = row1[header];
resultRow[`${header} (File 2)`] = row2[header];
});
comparisonResults.push(resultRow);
});
} else if (!showOnlyMatches) {
// No match found, but we're showing all rows
const resultRow = {};
headers.forEach(header => {
resultRow[`${header} (File 1)`] = row1[header];
resultRow[`${header} (File 2)`] = '';
});
comparisonResults.push(resultRow);
}
});
// Add unmatched rows from file2 if we're showing all rows
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);
}
});
}
// Create display headers (combining both files)
const displayHeaders = [];
headers.forEach(header => {
displayHeaders.push(`${header} (File 1)`);
displayHeaders.push(`${header} (File 2)`);
});
// Populate data grid headers
dataGridHeaders.innerHTML = '';
displayHeaders.forEach(header => {
const headerCell = document.createElement('div');
headerCell.className = 'data-cell data-header';
headerCell.textContent = header;
// Highlight key column if specified
if (keyColumn && header.includes(keyColumn)) {
headerCell.classList.add('key-column');
}
dataGridHeaders.appendChild(headerCell);
});
// Set grid template columns based on number of headers
dataGridHeaders.style.gridTemplateColumns = `repeat(${displayHeaders.length}, minmax(150px, 1fr))`;
dataGridBody.style.gridTemplateColumns = `repeat(${displayHeaders.length}, minmax(150px, 1fr))`;
// Populate data grid body
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`;
// Check if this is a matching row (both files have data)
const isMatch = row[header] !== '' &&
row[header.replace(' (File 1)', ' (File 2)')] !== undefined;
// Set cell content
cell.textContent = row[header] !== undefined ? row[header] : '';
// Highlight key column if specified
if (keyColumn && header.includes(keyColumn)) {
cell.classList.add('key-column');
}
// Highlight matching cells
if (isMatch) {
cell.style.backgroundColor = 'rgba(16, 185, 129, 0.1)';
}
dataGridBody.appendChild(cell);
});
});
// Update counts
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;
// 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;
// Complete progress
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';
// 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, "Matching Rows");
// Generate file name with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `excel_matching_rows_${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 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>