/* Custom scrollbar */
textarea::-webkit-scrollbar { width: 6px; }
textarea::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
textarea::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
textarea::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
#output::-webkit-scrollbar { width: 6px; }
#output::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
#output::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
textarea {
font-family: ‘Menlo’, ‘Monaco’, ‘Consolas’, monospace;
font-size: 13px;
resize: none;
}
#output {
font-family: ‘Menlo’, ‘Monaco’, ‘Consolas’, monospace;
font-size: 13px;
}
.na-line { color: #ef4444; }
.match-line { color: #1e293b; }
.badge-privacy {
background: linear-gradient(135deg, #10b981, #059669);
}
/* Pulse on result update */
@keyframes fadeIn {
from { opacity: 0.4; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.result-anim { animation: fadeIn 0.2s ease-out; }
/* Glow on copy success */
@keyframes glowPop {
0% { box-shadow: 0 0 0 0 rgba(16,185,129,0.6); }
50% { box-shadow: 0 0 0 8px rgba(16,185,129,0); }
100% { box-shadow: none; }
}
.copy-success { animation: glowPop 0.5s ease-out; }
🔍
Excel VLOOKUP 本地比對器
等同於 VLOOKUP(Value, Table, 2, FALSE) — 打字即時比對
🔒 資料僅在瀏覽器記憶體中處理,從不上傳
|
✅ 匹配:0 筆
|
❌ 未找到:0 筆
|
📐 對照表:0 筆
🔒 Zero-server · 100% In-Browser · No data ever leaves your device
// ── State ────────────────────────────────────────────────────────────────
let lookupMap = new Map();
let resultLines = [];
// ── Helpers ──────────────────────────────────────────────────────────────
/**
* Parse textarea value into an array of non-empty trimmed lines.
* Handles \r\n, \r, \n line endings.
*/
function parseLines(raw) {
return raw
.split(/\r\n|\r|\n/)
.map(l => l.trim())
.filter(l => l.length > 0);
}
/**
* Build a Map from the table textarea.
* Each row is “Key\tValue”; if Tab is missing, the whole row is treated as
* the Key and value is empty string.
*/
function buildMap(raw) {
const map = new Map();
const lines = parseLines(raw);
for (const line of lines) {
const tabIdx = line.indexOf(‘\t’);
if (tabIdx !== -1) {
const key = line.substring(0, tabIdx).trim();
const val = line.substring(tabIdx + 1).trim();
if (key) map.set(key, val);
} else {
// Single-column row: key only, no value
const key = line.trim();
if (key && !map.has(key)) map.set(key, ”);
}
}
return map;
}
/**
* Update the lookup counter badge.
*/
function updateCount(textareaId, countId) {
const raw = document.getElementById(textareaId).value;
const n = parseLines(raw).length;
document.getElementById(countId).textContent = n + ‘ 筆’;
return n;
}
// ── Core: VLOOKUP ────────────────────────────────────────────────────────
function runVlookup() {
const rawA = document.getElementById(‘colA’).value;
const rawB = document.getElementById(‘colB’).value;
// Rebuild the map every time B changes (O(n) once)
lookupMap = buildMap(rawB);
// Update counters
const nA = updateCount(‘colA’, ‘countA’);
const nB = updateCount(‘colB’, ‘countB’);
document.getElementById(‘statTable’).textContent = lookupMap.size;
const lookupValues = parseLines(rawA);
resultLines = [];
let matchCount = 0;
let naCount = 0;
for (const val of lookupValues) {
if (lookupMap.has(val)) {
resultLines.push({ text: lookupMap.get(val), na: false });
matchCount++;
} else {
resultLines.push({ text: ‘#N/A’, na: true });
naCount++;
}
}
// Render output
renderOutput(lookupValues.length === 0);
// Update stats bar
const statsBar = document.getElementById(‘statsBar’);
if (lookupValues.length > 0 || lookupMap.size > 0) {
statsBar.classList.remove(‘hidden’);
statsBar.classList.add(‘flex’);
} else {
statsBar.classList.add(‘hidden’);
statsBar.classList.remove(‘flex’);
}
document.getElementById(‘statTotal’).textContent = lookupValues.length;
document.getElementById(‘statMatch’).textContent = matchCount;
document.getElementById(‘statNA’).textContent = naCount;
}
function renderOutput(isEmpty) {
const output = document.getElementById(‘output’);
if (isEmpty) {
output.innerHTML = ‘在上方貼入資料後,結果將即時顯示於此…‘;
return;
}
const fragment = document.createDocumentFragment();
for (const line of resultLines) {
const div = document.createElement(‘div’);
div.className = line.na ? ‘na-line’ : ‘match-line’;
div.textContent = line.text;
fragment.appendChild(div);
}
output.innerHTML = ”;
output.appendChild(fragment);
output.classList.remove(‘result-anim’);
// Force reflow to restart animation
void output.offsetWidth;
output.classList.add(‘result-anim’);
}
// ── Copy ─────────────────────────────────────────────────────────────────
async function copyResult() {
if (resultLines.length === 0) return;
const text = resultLines.map(l => l.text).join(‘\n’);
try {
await navigator.clipboard.writeText(text);
} catch {
// Fallback for environments without clipboard API
const ta = document.createElement(‘textarea’);
ta.value = text;
ta.style.cssText = ‘position:fixed;opacity:0;top:0;left:0’;
document.body.appendChild(ta);
ta.select();
document.execCommand(‘copy’);
document.body.removeChild(ta);
}
const btn = document.getElementById(‘copyBtn’);
const originalHTML = btn.innerHTML;
btn.innerHTML = ‘✅ 已複製!’;
btn.classList.add(‘copy-success’, ‘bg-emerald-600’);
btn.classList.remove(‘bg-emerald-500’);
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove(‘copy-success’, ‘bg-emerald-600’);
btn.classList.add(‘bg-emerald-500’);
}, 1800);
}
// ── Clear helpers ────────────────────────────────────────────────────────
function clearField(textareaId, countId) {
document.getElementById(textareaId).value = ”;
document.getElementById(countId).textContent = ‘0 筆’;
runVlookup();
}
function clearAll() {
[‘colA’, ‘colB’].forEach(id => document.getElementById(id).value = ”);
[‘countA’, ‘countB’].forEach(id => document.getElementById(id).textContent = ‘0 筆’);
lookupMap.clear();
resultLines = [];
document.getElementById(‘output’).innerHTML =
‘在上方貼入資料後,結果將即時顯示於此…‘;
document.getElementById(‘statsBar’).classList.add(‘hidden’);
document.getElementById(‘statsBar’).classList.remove(‘flex’);
}
// ── Event listeners (real-time: “打字即顯示”) ──────────────────────────
document.getElementById(‘colA’).addEventListener(‘input’, runVlookup);
document.getElementById(‘colB’).addEventListener(‘input’, runVlookup);
// Also handle paste events explicitly for instant response
document.getElementById(‘colA’).addEventListener(‘paste’, () => setTimeout(runVlookup, 0));
document.getElementById(‘colB’).addEventListener(‘paste’, () => setTimeout(runVlookup, 0));


Comments