<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>截图转表格 - 图片文字识别导出</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:#f0f2f5;color:#1f2937;min-height:100vh;padding:12px;overflow-x:hidden}
.app{max-width:960px;margin:0 auto}
header{text-align:center;padding:16px 0 12px}
header h1{font-size:22px;font-weight:700;color:#111827;letter-spacing:.5px}
header p{font-size:13px;color:#6b7280;margin-top:6px}
.card{background:#fff;border-radius:14px;box-shadow:0 2px 12px rgba(0,0,0,.06);padding:16px;margin-bottom:14px}
.dropzone{border:2px dashed #c7d2fe;border-radius:12px;background:#f8faff;padding:28px 16px;text-align:center;cursor:pointer;transition:.2s;position:relative}
.dropzone:hover,.dropzone.drag{border-color:#6366f1;background:#eef2ff}
.dropzone .icon{font-size:40px;line-height:1}
.dropzone .t1{font-size:15px;font-weight:600;color:#374151;margin-top:10px}
.dropzone .t2{font-size:12px;color:#9ca3af;margin-top:6px}
.dropzone input{display:none}
.thumbs{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px}
.thumb{position:relative;width:96px;height:96px;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb;background:#f9fafb}
.thumb img{width:100%;height:100%;object-fit:cover;display:block}
.thumb .del{position:absolute;top:3px;right:3px;width:22px;height:22px;border-radius:50%;background:rgba(0,0,0,.6);color:#fff;border:none;font-size:13px;line-height:22px;text-align:center;cursor:pointer;padding:0}
.thumb .name{position:absolute;bottom:0;left:0;right:0;background:rgba(0,0,0,.55);color:#fff;font-size:10px;padding:2px 4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.btnrow{display:flex;gap:10px;margin-top:14px}
.btn{flex:1;min-height:44px;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;text-align:center;transition:.15s;padding:0 8px;font-family:inherit}
.btn:active{transform:scale(.98)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn-primary{background:linear-gradient(135deg,#6366f1,#4f46e5);color:#fff}
.btn-primary:hover:not(:disabled){background:linear-gradient(135deg,#4f46e5,#4338ca)}
.btn-ghost{background:#f3f4f6;color:#374151;border:1px solid #e5e7eb}
.btn-ghost:hover:not(:disabled){background:#e5e7eb}
.btn-green{background:linear-gradient(135deg,#10b981,#059669);color:#fff}
.btn-green:hover:not(:disabled){background:linear-gradient(135deg,#059669,#047857)}
.progress{height:8px;background:#e5e7eb;border-radius:4px;overflow:hidden;margin-top:14px;display:none}
.progress.show{display:block}
.progress .bar{height:100%;width:0;background:linear-gradient(90deg,#6366f1,#8b5cf6);transition:width .3s;border-radius:4px}
.status{font-size:13px;color:#6b7280;text-align:center;margin-top:10px;min-height:18px}
.status.err{color:#dc2626}
.status.ok{color:#059669}
.result-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px;flex-wrap:wrap}
.result-head h2{font-size:16px;font-weight:700;color:#111827}
.result-head .count{font-size:12px;color:#6b7280;background:#f3f4f6;padding:3px 10px;border-radius:20px}
.tabs{display:flex;gap:6px;margin-bottom:12px;background:#f3f4f6;padding:4px;border-radius:10px}
.tab{flex:1;min-height:38px;border:none;background:transparent;border-radius:8px;font-size:14px;font-weight:600;color:#6b7280;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center}
.tab.active{background:#fff;color:#4f46e5;box-shadow:0 1px 4px rgba(0,0,0,.08)}
.tablewrap{overflow:auto;max-height:420px;border:1px solid #e5e7eb;border-radius:10px;-webkit-overflow-scrolling:touch}
table{border-collapse:collapse;width:100%;font-size:13px;min-width:100%}
table th,table td{border:1px solid #e5e7eb;padding:8px 10px;text-align:left;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
table th{background:#f9fafb;font-weight:600;color:#374151;position:sticky;top:0;z-index:1}
table td[contenteditable]:focus{outline:2px solid #6366f1;outline-offset:-2px;background:#eef2ff}
table tr:nth-child(even) td{background:#fcfcfd}
table tr:nth-child(even) td[contenteditable]:focus{background:#eef2ff}
.rawtext{width:100%;min-height:200px;border:1px solid #e5e7eb;border-radius:10px;padding:12px;font-size:13px;font-family:ui-monospace,Menlo,Consolas,monospace;resize:vertical;line-height:1.6;color:#374151}
.rawtext:focus{outline:2px solid #6366f1;outline-offset:-1px}
.empty{text-align:center;padding:36px 16px;color:#9ca3af}
.empty .icon{font-size:44px;margin-bottom:10px}
.empty p{font-size:14px}
.tips{font-size:12px;color:#9ca3af;line-height:1.7;margin-top:10px}
.tips b{color:#6b7280}
.toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(20px);background:#1f2937;color:#fff;padding:11px 20px;border-radius:10px;font-size:14px;opacity:0;pointer-events:none;transition:.25s;z-index:99;max-width:90vw;text-align:center}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
.hidden{display:none!important}
@media(max-width:520px){
body{padding:8px}
header h1{font-size:19px}
.card{padding:13px;border-radius:12px}
.dropzone{padding:22px 12px}
.thumb{width:76px;height:76px}
.btn{font-size:14px;min-height:44px}
table th,table td{padding:7px 8px;font-size:12px}
}
</style>
</head>
<body>
<div class="app">
<header>
<h1>📸 截图转表格</h1>
<p>粘贴或上传截图,自动识别文字并保留表格结构,一键导出 Excel</p>
</header>
<div class="card">
<div class="dropzone" id="dropzone">
<div class="icon">🖼️</div>
<div class="t1">点击选择图片,或直接 Ctrl+V 粘贴截图</div>
<div class="t2">支持 PNG / JPG / WEBP,可一次选多张批量识别</div>
<input type="file" id="fileInput" accept="image/*" multiple>
</div>
<div class="thumbs" id="thumbs"></div>
<div class="btnrow">
<button class="btn btn-primary" id="btnRecognize" disabled>🔍 开始识别</button>
<button class="btn btn-ghost" id="btnClear" disabled>🗑️ 清空</button>
</div>
<div class="progress" id="progress"><div class="bar" id="bar"></div></div>
<div class="status" id="status">等待添加图片…</div>
</div>
<div class="card" id="resultCard">
<div class="result-head">
<h2>识别结果</h2>
<span class="count" id="cellCount">0 个单元格</span>
</div>
<div class="tabs">
<button class="tab active" data-tab="table">📊 表格视图</button>
<button class="tab" data-tab="raw">📝 纯文本</button>
</div>
<div id="tabTable">
<div class="tablewrap" id="tablewrap">
<div class="empty"><div class="icon">📋</div><p>识别完成后,表格会显示在这里<br>单元格可直接点击修改</p></div>
</div>
</div>
<div id="tabRaw" class="hidden">
<textarea class="rawtext" id="rawtext" placeholder="识别出的纯文本会显示在这里,可自由编辑…"></textarea>
</div>
<div class="btnrow">
<button class="btn btn-green" id="btnExcel" disabled>📥 导出 Excel</button>
<button class="btn btn-ghost" id="btnCsv" disabled>📄 导出 CSV</button>
<button class="btn btn-ghost" id="btnCopy" disabled>📋 复制表格</button>
</div>
<div class="tips">
<b>小提示:</b>识别结果基于浏览器内置文字识别能力,识别后请核对关键数据;表格单元格支持直接双击编辑,改完再导出即可。
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
(function(){
'use strict';
var images = []; // {id, name, dataUrl}
var rows = []; // 二维数组
var uid = 0;
var ocrSupported = ('TextDetector' in window);
var $ = function(id){ return document.getElementById(id); };
var dropzone = $('dropzone'), fileInput = $('fileInput'), thumbs = $('thumbs');
var btnRecognize = $('btnRecognize'), btnClear = $('btnClear');
var progress = $('progress'), bar = $('bar'), statusEl = $('status');
var tablewrap = $('tablewrap'), rawtext = $('rawtext'), cellCount = $('cellCount');
var btnExcel = $('btnExcel'), btnCsv = $('btnCsv'), btnCopy = $('btnCopy');
var toastEl = $('toast');
function toast(msg){
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(toastEl._t);
toastEl._t = setTimeout(function(){ toastEl.classList.remove('show'); }, 2200);
}
function setStatus(msg, type){
statusEl.textContent = msg;
statusEl.className = 'status' + (type ? ' ' + type : '');
}
function updateButtons(){
var has = images.length > 0;
btnRecognize.disabled = !has;
btnClear.disabled = !has;
}
function renderThumbs(){
thumbs.innerHTML = '';
images.forEach(function(img){
var d = document.createElement('div');
d.className = 'thumb';
var im = document.createElement('img');
im.src = img.dataUrl; im.alt = img.name;
var del = document.createElement('button');
del.className = 'del'; del.textContent = '×'; del.title = '移除';
del.onclick = function(e){ e.stopPropagation(); removeImage(img.id); };
var nm = document.createElement('div');
nm.className = 'name'; nm.textContent = img.name;
d.appendChild(im); d.appendChild(del); d.appendChild(nm);
thumbs.appendChild(d);
});
updateButtons();
}
function removeImage(id){
images = images.filter(function(i){ return i.id !== id; });
renderThumbs();
setStatus(images.length ? ('已添加 ' + images.length + ' 张图片,可开始识别') : '等待添加图片…');
}
function addFiles(files){
var list = Array.prototype.slice.call(files).filter(function(f){
return f && f.type && f.type.indexOf('image/') === 0;
});
if(!list.length){ toast('请选择图片文件'); return; }
var loaded = 0;
list.forEach(function(f){
var reader = new FileReader();
reader.onload = function(e){
images.push({ id: ++uid, name: f.name || ('截图' + uid), dataUrl: e.target.result });
loaded++;
if(loaded === list.length){
renderThumbs();
setStatus('已添加 ' + images.length + ' 张图片,可开始识别');
}
};
reader.onerror = function(){ toast('读取图片失败:' + (f.name || '')); };
reader.readAsDataURL(f);
});
}
dropzone.addEventListener('click', function(){ fileInput.click(); });
fileInput.addEventListener('change', function(e){ addFiles(e.target.files); fileInput.value = ''; });
['dragenter','dragover'].forEach(function(ev){
dropzone.addEventListener(ev, function(e){ e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag'); });
});
['dragleave','drop'].forEach(function(ev){
dropzone.addEventListener(ev, function(e){ e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag'); });
});
dropzone.addEventListener('drop', function(e){
if(e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files);
});
document.addEventListener('paste', function(e){
var items = (e.clipboardData && e.clipboardData.items) || [];
var files = [];
for(var i=0;i<items.length;i++){
if(items[i].type && items[i].type.indexOf('image/') === 0){
var f = items[i].getAsFile();
if(f) files.push(f);
}
}
if(files.length){
e.preventDefault();
addFiles(files);
toast('已粘贴 ' + files.length + ' 张截图');
}
});
btnClear.addEventListener('click', function(){
images = []; rows = [];
renderThumbs(); renderTable(); rawtext.value = '';
setStatus('等待添加图片…');
progress.classList.remove('show'); bar.style.width = '0';
updateExportButtons();
});
// ---------- 识别 ----------
function loadImage(dataUrl){
return new Promise(function(resolve, reject){
var im = new Image();
im.onload = function(){ resolve(im); };
im.onerror = function(){ reject(new Error('图片加载失败')); };
im.src = dataUrl;
});
}
function detectBlocks(img){
var detector = new window.TextDetector();
return detector.detect(img).then(function(list){
return list.map(function(b){
var r = b.boundingBox;
return { text: (b.rawValue || '').trim(), x: r.x, y: r.y, w: r.width, h: r.height };
}).filter(function(b){ return b.text; });
});
}
function groupIntoRows(blocks){
if(!blocks.length) return [];
var sorted = blocks.slice().sort(function(a,b){ return a.y - b.y; });
var heights = sorted.map(function(b){ return b.h; }).sort(function(a,b){ return a-b; });
var medH = heights[Math.floor(heights.length/2)] || 16;
var tol = Math.max(8, medH * 0.6);
var lines = [];
sorted.forEach(function(b){
var cy = b.y + b.h/2;
var line = null;
for(var i=0;i<lines.length;i++){
if(Math.abs(lines[i].cy - cy) <= tol){ line = lines[i]; break; }
}
if(!line){ line = { cy: cy, items: [] }; lines.push(line); }
line.items.push(b);
line.cy = (line.cy * (line.items.length-1) + cy) / line.items.length;
});
lines.sort(function(a,b){ return a.cy - b.cy; });
lines.forEach(function(l){ l.items.sort(function(a,b){ return a.x - b.x; }); });
return lines;
}
function buildGrid(lines){
if(!lines.length) return [];
var all = [];
lines.forEach(function(l){ l.items.forEach(function(b){ all.push(b); }); });
var widths = all.map(function(b){ return b.w; }).sort(function(a,b){ return a-b; });
var medW = widths[Math.floor(widths.length/2)] || 60;
var gapTol = Math.max(12, medW * 0.9);
var maxCols = 1;
lines.forEach(function(l){
var cols = 1;
for(var i=1;i<l.items.length;i++){
var prev = l.items[i-1];
var gap = l.items[i].x - (prev.x + prev.w);
if(gap > gapTol) cols++;
}
l.cols = cols;
if(cols > maxCols) maxCols = cols;
});
var grid = lines.map(function(l){
var cells = [];
var cur = l.items[0].text;
for(var i=1;i<l.items.length;i++){
var prev = l.items[i-1];
var gap = l.items[i].x - (prev.x + prev.w);
if(gap > gapTol){ cells.push(cur); cur = l.items[i].text; }
else { cur += ' ' + l.items[i].text; }
}
cells.push(cur);
while(cells.length < maxCols) cells.push('');
return cells;
});
return grid;
}
function blocksToGrid(blocks){
var lines = groupIntoRows(blocks);
return buildGrid(lines);
}
function recognizeOne(dataUrl){
return loadImage(dataUrl).then(function(img){
if(ocrSupported){
return detectBlocks(img).then(function(blocks){
if(blocks.length) return blocksToGrid(blocks);
return null;
});
}
return null;
});
}
function mergeGrids(grids){
var out = [];
grids.forEach(function(g){
if(!g || !g.length) return;
if(out.length) out.push([]);
g.forEach(function(r){ out.push(r.slice()); });
});
return out;
}
btnRecognize.addEventListener('click', function(){
if(!images.length) return;
btnRecognize.disabled = true;
btnClear.disabled = true;
progress.classList.add('show');
bar.style.width = '0';
setStatus('正在识别…');
var results = [];
var done = 0;
var failed = 0;
function step(){
if(done >= images.length){
finish();
return;
}
var img = images[done];
setStatus('正在识别第 ' + (done+1) + ' / ' + images.length + ' 张…');
recognizeOne(img.dataUrl).then(function(grid){
if(grid && grid.length) results.push(grid);
else failed++;
}).catch(function(){
failed++;
}).then(function(){
done++;
bar.style.width = Math.round(done / images.length * 100) + '%';
setTimeout(step, 60);
});
}
function finish(){
progress.classList.remove('show');
updateButtons();
if(!results.length){
setStatus('未能识别出文字,请换一张更清晰的截图重试', 'err');
toast('识别失败,请尝试更清晰的截图');
return;
}
rows = mergeGrids(results);
renderTable();
renderRaw();
updateExportButtons();
var msg = '识别完成,共 ' + rows.length + ' 行';
if(failed) msg += '(' + failed + ' 张未识别出内容)';
setStatus(msg, 'ok');
toast('识别完成');
}
step();
});
// ---------- 渲染 ----------
function renderTable(){
if(!rows.length){
tablewrap.innerHTML = '<div class="empty"><div class="icon">📋</div><p>识别完成后,表格会显示在这里<br>单元格可直接点击修改</p></div>';
cellCount.textContent = '0 个单元格';
return;
}
var maxCols = 0;
rows.forEach(function(r){ if(r.length > maxCols) maxCols = r.length; });
var html = '<table><thead><tr>';
for(var c=0;c<maxCols;c++) html += '<th>列' + (c+1) + '</th>';
html += '</tr></thead><tbody>';
rows.forEach(function(r, ri){
html += '<tr>';
for(var c=0;c<maxCols;c++){
var v = r[c] == null ? '' : r[c];
html += '<td contenteditable="true" data-r="' + ri + '" data-c="' + c + '">' + escapeHtml(v) + '</td>';
}
html += '</tr>';
});
html += '</tbody></table>';
tablewrap.innerHTML = html;
cellCount.textContent = (rows.length * maxCols) + ' 个单元格';
tablewrap.querySelectorAll('td[contenteditable]').forEach(function(td){
td.addEventListener('blur', function(){
var r = +td.getAttribute('data-r'), c = +td.getAttribute('data-c');
if(rows[r]) rows[r][c] = td.textContent.trim();
renderRaw();
});
td.addEventListener('keydown', function(e){
if(e.key === 'Enter'){ e.preventDefault(); td.blur(); }
});
});
}
function escapeHtml(s){
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function renderRaw(){
rawtext.value = rows.map(function(r){ return r.join('\t'); }).join('\n');
}
function updateExportButtons(){
var has = rows.length > 0;
btnExcel.disabled = !has;
btnCsv.disabled = !has;
btnCopy.disabled = !has;
}
// ---------- 标签切换 ----------
document.querySelectorAll('.tab').forEach(function(tab){
tab.addEventListener('click', function(){
document.querySelectorAll('.tab').forEach(function(t){ t.classList.remove('active'); });
tab.classList.add('active');
var name = tab.getAttribute('data-tab');
$('tabTable').classList.toggle('hidden', name !== 'table');
$('tabRaw').classList.toggle('hidden', name !== 'raw');
});
});
// ---------- 导出 ----------
function downloadBlob(blob, filename){
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click();
setTimeout(function(){ document.body.removeChild(a); URL.revokeObjectURL(url); }, 200);
}
function csvEscape(v){
v = v == null ? '' : String(v);
if(/[",\n\r\t]/.test(v)) return '"' + v.replace(/"/g,'""') + '"';
return v;
}
btnCsv.addEventListener('click', function(){
if(!rows.length) return;
var csv = rows.map(function(r){ return r.map(csvEscape).join(','); }).join('\r\n');
var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' });
downloadBlob(blob, '识别表格.csv');
toast('CSV 已导出');
});
btnExcel.addEventListener('click', function(){
if(!rows.length) return;
var maxCols = 0;
rows.forEach(function(r){ if(r.length > maxCols) maxCols = r.length; });
var html = '<html xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8">';
html += '<!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>';
html += '<x:Name>识别结果</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions>';
html += '</x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->';
html += '<style>td{border:1px solid #ccc;padding:4px;mso-number-format:"\\@";}</style></head><body><table>';
rows.forEach(function(r){
html += '<tr>';
for(var c=0;c<maxCols;c++){
var v = r[c] == null ? '' : r[c];
html += '<td>' + escapeHtml(v) + '</td>';
}
html += '</tr>';
});
html += '</table></body></html>';
var blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel;charset=utf-8' });
downloadBlob(blob, '识别表格.xls');
toast('Excel 已导出');
});
btnCopy.addEventListener('click', function(){
if(!rows.length) return;
var text = rows.map(function(r){ return r.join('\t'); }).join('\n');
function fallback(){
var ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try{ document.execCommand('copy'); toast('已复制到剪贴板'); }
catch(err){ toast('复制失败,请手动选择文本'); }
document.body.removeChild(ta);
}
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(text).then(function(){ toast('已复制到剪贴板'); }).catch(fallback);
} else fallback();
});
// 初始化
if(!ocrSupported){
setStatus('当前浏览器不支持内置文字识别,建议使用最新版 Chrome / Edge', 'err');
} else {
setStatus('等待添加图片…');
}
renderThumbs();
updateExportButtons();
})();
</script>
</body>
</html>