<!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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }

  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>