webui.html 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>OnnxOCR Web UI</title>
  6. <link rel="stylesheet" href="/static/webui.css">
  7. </head>
  8. <body>
  9. <div class="container">
  10. <h2>OnnxOCR Web UI</h2>
  11. <form id="ocrForm">
  12. <div class="form-row" style="justify-content: flex-start;">
  13. <label for="modelSelect">选择模型:</label>
  14. <select id="modelSelect" name="model_name">
  15. <option value="PP-OCRv5">PP-OCRv5</option>
  16. <option value="PP-OCRv4">PP-OCRv4</option>
  17. <option value="ch_ppocr_server_v2.0">ch_ppocr_server_v2.0</option>
  18. </select>
  19. <button type="button" id="clearBtn">清除</button>
  20. </div>
  21. <div class="form-row">
  22. <div id="dropZone" class="drop-zone">点击或拖拽图片/PDF文件到此处(可多选)</div>
  23. <input id="fileInput" type="file" name="files" multiple accept="image/*,.pdf" />
  24. </div>
  25. <div class="form-row">
  26. <ul id="fileList"></ul>
  27. </div>
  28. <div class="form-row button-row">
  29. <button type="submit">开始识别</button>
  30. </div>
  31. </form>
  32. <div id="previewArea"></div>
  33. <div id="progressArea">
  34. <div id="progressText"></div>
  35. <div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
  36. <div id="elapsedTime"></div>
  37. </div>
  38. <div class="download-row">
  39. <button id="downloadBtn">下载全部TXT(压缩包)</button>
  40. </div>
  41. <div id="loading">正在识别,请稍候...</div>
  42. <div id="resultArea"></div>
  43. </div>
  44. <div id="globalTip" style="display:none;position:fixed;top:32px;right:32px;z-index:9999;min-width:120px;padding:12px 28px;background:#7b8cff;color:#fff;border-radius:8px;box-shadow:0 2px 8px #7b8cff22;font-size:1.08rem;transition:opacity 0.3s;opacity:0;"></div>
  45. <script>
  46. const dropZone = document.getElementById('dropZone');
  47. const fileInput = document.getElementById('fileInput');
  48. const ocrForm = document.getElementById('ocrForm');
  49. const resultArea = document.getElementById('resultArea');
  50. const downloadBtn = document.getElementById('downloadBtn');
  51. const loading = document.getElementById('loading');
  52. const fileList = document.getElementById('fileList');
  53. const clearBtn = document.getElementById('clearBtn');
  54. const progressArea = document.getElementById('progressArea');
  55. const progressBar = document.getElementById('progressBar');
  56. const progressText = document.getElementById('progressText');
  57. const elapsedTime = document.getElementById('elapsedTime');
  58. let lastZipUrl = null;
  59. function updateFileList() {
  60. fileList.innerHTML = '';
  61. if (fileInput.files.length === 0) {
  62. fileList.innerHTML = '<li style="color:#888;">未选择文件</li>';
  63. return;
  64. }
  65. for (const file of fileInput.files) {
  66. const li = document.createElement('li');
  67. li.textContent = file.name;
  68. fileList.appendChild(li);
  69. }
  70. }
  71. // 拖拽上传
  72. ['dragenter','dragover'].forEach(evt => dropZone.addEventListener(evt, e => {
  73. e.preventDefault();
  74. dropZone.classList.add('dragover');
  75. }));
  76. ['dragleave','drop'].forEach(evt => dropZone.addEventListener(evt, e => {
  77. e.preventDefault();
  78. dropZone.classList.remove('dragover');
  79. }));
  80. dropZone.addEventListener('click', () => fileInput.click());
  81. dropZone.addEventListener('drop', e => {
  82. fileInput.files = e.dataTransfer.files;
  83. updateFileList();
  84. });
  85. fileInput.addEventListener('change', updateFileList);
  86. ocrForm.addEventListener('submit', async function(e) {
  87. e.preventDefault();
  88. if (!fileInput.files.length) {
  89. alert('请先选择图片或PDF文件');
  90. return;
  91. }
  92. loading.style.display = 'none'; // 立即隐藏 loading
  93. resultArea.innerHTML = '';
  94. downloadBtn.style.display = 'none';
  95. fileList.innerHTML = '';
  96. progressArea.style.display = 'flex';
  97. progressBar.style.width = '0%';
  98. progressText.textContent = '正在准备识别...';
  99. elapsedTime.textContent = '';
  100. const startTime = Date.now();
  101. const formData = new FormData();
  102. for (const file of fileInput.files) {
  103. formData.append('files', file);
  104. }
  105. formData.append('model_name', document.getElementById('modelSelect').value);
  106. // 保存所有图片文件的base名和File对象
  107. const imageFileMap = {};
  108. for (const file of fileInput.files) {
  109. if (file.type.startsWith('image/')) {
  110. const base = file.name.replace(/\.[^.]+$/, '');
  111. imageFileMap[base] = file;
  112. }
  113. }
  114. try {
  115. let fakeProgress = 0;
  116. const fakeTimer = setInterval(() => {
  117. if (fakeProgress < 80) {
  118. fakeProgress += Math.random() * 8 + 2;
  119. progressBar.style.width = Math.min(fakeProgress, 80) + '%';
  120. progressText.textContent = '正在识别文件...';
  121. }
  122. }, 300);
  123. const resp = await fetch('/ocr', { method: 'POST', body: formData });
  124. clearInterval(fakeTimer);
  125. progressBar.style.width = '100%';
  126. progressText.textContent = '识别完成';
  127. const data = await resp.json();
  128. if (!data.success) {
  129. progressArea.style.display = 'none';
  130. resultArea.innerHTML = `<div style='color:red;'>识别失败:${data.msg || ''}</div>`;
  131. lastZipUrl = null;
  132. return;
  133. }
  134. // 多图片多结果展示
  135. previewArea.innerHTML = '';
  136. resultArea.innerHTML = '';
  137. // 构建图片base名到File对象的映射
  138. const imgMap = {};
  139. for (const file of fileInput.files) {
  140. if (file.type.startsWith('image/')) {
  141. const base = file.name.replace(/\.[^.]+$/, '');
  142. imgMap[base] = file;
  143. }
  144. }
  145. data.results.forEach(r => {
  146. const txtBase = r.filename.replace(/\.txt$/, '');
  147. const div = document.createElement('div');
  148. div.className = 'result-block';
  149. let imgHtml = '';
  150. if (imgMap[txtBase]) {
  151. const url = URL.createObjectURL(imgMap[txtBase]);
  152. imgHtml = `<img class=\"ocr-image-preview\" src=\"${url}\" alt=\"${r.filename}\">`;
  153. }
  154. // 新增复制按钮
  155. const copyBtnHtml = `<button class=\"copy-btn\" title=\"复制文本\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"5\" y=\"5\" width=\"10\" height=\"12\" rx=\"2\" fill=\"#7b8cff\"/><rect x=\"3\" y=\"3\" width=\"10\" height=\"12\" rx=\"2\" stroke=\"#7b8cff\" stroke-width=\"1.5\" fill=\"none\"/></svg></button>`;
  156. div.innerHTML = `${imgHtml}<div class=\"ocr-text-content\"><div style=\"display:flex;justify-content:space-between;align-items:flex-start;\"><b>${r.filename}</b>${copyBtnHtml}</div><pre>${r.content}</pre></div>`;
  157. resultArea.appendChild(div);
  158. });
  159. // 事件委托绑定复制按钮点击事件
  160. resultArea.addEventListener('click', function(e) {
  161. if (e.target.closest('.copy-btn')) {
  162. const btn = e.target.closest('.copy-btn');
  163. const pre = btn.closest('.ocr-text-content').querySelector('pre');
  164. if (pre) {
  165. navigator.clipboard.writeText(pre.textContent).then(() => {
  166. showTip('复制成功');
  167. }).catch(() => {
  168. showTip('复制失败,请手动复制', '#f87171');
  169. });
  170. }
  171. }
  172. });
  173. const used = ((Date.now() - startTime) / 1000).toFixed(2);
  174. elapsedTime.textContent = `识别总耗时:${used} 秒`;
  175. downloadBtn.style.display = 'inline-block';
  176. lastZipUrl = data.zip_url || null;
  177. } catch (err) {
  178. progressArea.style.display = 'none';
  179. resultArea.innerHTML = `<div style='color:red;'>请求失败:${err}</div>`;
  180. }
  181. });
  182. downloadBtn.addEventListener('click', function() {
  183. if (lastZipUrl) {
  184. // 采用 a 标签下载,兼容所有浏览器
  185. const a = document.createElement('a');
  186. a.href = lastZipUrl;
  187. a.download = '';
  188. document.body.appendChild(a);
  189. a.click();
  190. document.body.removeChild(a);
  191. } else {
  192. alert('未找到可下载的压缩包');
  193. }
  194. });
  195. clearBtn.addEventListener('click', function() {
  196. fileInput.value = '';
  197. updateFileList();
  198. resultArea.innerHTML = '';
  199. downloadBtn.style.display = 'none';
  200. previewArea.innerHTML = '';
  201. progressArea.style.display = 'none';
  202. progressBar.style.width = '0%';
  203. progressText.textContent = '';
  204. elapsedTime.textContent = '';
  205. });
  206. function showTip(msg, color = '#7b8cff') {
  207. const tip = document.getElementById('globalTip');
  208. tip.textContent = msg;
  209. tip.style.background = color;
  210. tip.style.display = 'block';
  211. tip.style.opacity = '1';
  212. clearTimeout(tip._timer);
  213. tip._timer = setTimeout(() => {
  214. tip.style.opacity = '0';
  215. setTimeout(() => { tip.style.display = 'none'; }, 400);
  216. }, 1500);
  217. }
  218. // 复制按钮事件绑定
  219. setTimeout(() => {
  220. document.querySelectorAll('.copy-btn').forEach(btn => {
  221. btn.onclick = function(e) {
  222. const pre = btn.closest('.ocr-text-content').querySelector('pre');
  223. if (pre) {
  224. navigator.clipboard.writeText(pre.textContent).then(() => {
  225. btn.title = '已复制!';
  226. btn.style.background = '#e0eaff';
  227. setTimeout(() => {
  228. btn.title = '复制文本';
  229. btn.style.background = '';
  230. }, 1200);
  231. });
  232. }
  233. };
  234. });
  235. }, 100);
  236. </script>
  237. </body>
  238. </html>