From f9c7caae2688290ba1acd777b39dfad254606449 Mon Sep 17 00:00:00 2001 From: coder Date: Fri, 17 Apr 2026 10:12:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20PDF=E5=AF=B9=E6=AF=94=E6=8C=89=E9=A1=B5?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=BB=9A=E5=8A=A8+=E8=AF=91=E6=96=87?= =?UTF-8?q?=E6=8C=89=E9=A1=B5=E5=88=86=E9=9A=94=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/style.css | 21 ++++++ templates/translation.html | 128 +++++++++++++++++++++++++++++++------ 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 7a0a115..ad41fe7 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -121,6 +121,27 @@ body { background: white; } +.translated-page { + padding-bottom: 30px; + border-bottom: 2px dashed #ddd; + margin-bottom: 20px; +} + +.translated-page:last-child { + border-bottom: none; +} + +.page-header { + font-size: 0.9em; + padding: 5px 10px; + background: #f0f0f0; + border-radius: 3px; +} + +.page-content { + padding: 10px; +} + /* 会员卡片 */ .pricing-card { transition: transform 0.3s ease; diff --git a/templates/translation.html b/templates/translation.html index 25a1b85..246aec8 100644 --- a/templates/translation.html +++ b/templates/translation.html @@ -79,6 +79,8 @@ // 切换对比视图 let syncScrollEnabled = true; let pdfDoc = null; + let pagePositions = []; // PDF各页位置 + let translatedPagePositions = []; // 译文各页位置 document.getElementById('toggleCompare').addEventListener('click', async function() { showCompare = !showCompare; @@ -88,29 +90,32 @@ const response = await fetch(`/api/compare/${translationId}`); const result = await response.json(); - // 原文面板:如果有PDF URL用PDF.js渲染,否则显示提取的文本 + // 原文面板:如果有PDF URL用PDF.js渲染 let originalHtml = ''; if (result.original_pdf_url) { - originalHtml = '
'; + originalHtml = '
'; } else if (result.original && result.original.length > 0) { originalHtml = `
${escapeHtml(result.original)}
`; } else { originalHtml = '
原文内容未找到(可能PDF已被删除)
'; } + // 译文按页渲染 + let translatedHtml = renderTranslatedByPage(result.translated); + document.getElementById('resultContent').innerHTML = `
-
原文 PDF
+
原文 PDF (共${result.pages.length}页)
${originalHtml}
-
译文
-
${renderMarkdown(result.translated)}
+
译文 (共${result.pages.length}页)
+
${translatedHtml}
- 💡 左右滚动同步,方便逐页对比 + 💡 滚动任意一侧,另一侧自动同步到对应页
`; @@ -119,8 +124,11 @@ renderPDF(result.original_pdf_url); } - // 启用滚动同步 - setTimeout(enableSyncScroll, 300); + // 计算各页位置并启用滚动同步 + setTimeout(() => { + calculatePagePositions(); + enableSyncScroll(); + }, 500); } catch (error) { alert('加载对比失败: ' + error.message); @@ -130,6 +138,33 @@ } }); + // 译文按页渲染 + function renderTranslatedByPage(content) { + if (!content) return '
译文加载失败
'; + + // 解析分页 + const pages = content.split(/\n\n---\n\n/); + let html = ''; + + pages.forEach((pageContent, index) => { + // 提取页码(如果有) + const pageNumMatch = pageContent.match(/## 第 (\d+) 页\n\n/); + const pageNum = pageNumMatch ? pageNumMatch[1] : (index + 1); + + // 移除页码标题,保留内容 + let contentOnly = pageContent.replace(/## 第 \d+ 页\n\n/, ''); + + html += ` +
+ +
${renderMarkdown(contentOnly)}
+
+ `; + }); + + return html; + } + // PDF.js渲染PDF async function renderPDF(url) { pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; @@ -143,12 +178,20 @@ // 渲染所有页面 for (let i = 1; i <= pdf.numPages; i++) { + // 页码标识 + const pageNumDiv = document.createElement('div'); + pageNumDiv.className = 'page-header text-center text-muted mb-2'; + pageNumDiv.textContent = `— 第 ${i} 页 —`; + pageNumDiv.dataset.page = i; + container.appendChild(pageNumDiv); + const page = await pdf.getPage(i); const canvas = document.createElement('canvas'); canvas.className = 'pdf-page-canvas'; canvas.style.width = '100%'; canvas.style.display = 'block'; canvas.style.marginBottom = '20px'; + canvas.dataset.page = i; const context = canvas.getContext('2d'); const viewport = page.getViewport({ scale: 1.5 }); @@ -162,12 +205,6 @@ }).promise; container.appendChild(canvas); - - // 添加页码标识 - const pageNum = document.createElement('div'); - pageNum.className = 'text-center text-muted mb-3'; - pageNum.textContent = `— 第 ${i} 页 —`; - container.appendChild(pageNum); } } catch (error) { document.getElementById('pdfContainer').innerHTML = @@ -175,6 +212,59 @@ } } + // 计算各页位置 + function calculatePagePositions() { + const originalPanel = document.getElementById('originalPanel'); + const translatedPanel = document.getElementById('translatedPanel'); + + // PDF各页位置 + pagePositions = []; + const pdfPages = document.querySelectorAll('#pdfPages .page-header, #pdfPages .pdf-page-canvas'); + pdfPages.forEach(el => { + if (el.dataset.page) { + const rect = el.getBoundingClientRect(); + const panelRect = originalPanel.getBoundingClientRect(); + pagePositions.push({ + page: parseInt(el.dataset.page), + top: rect.top - panelRect.top + originalPanel.scrollTop + }); + } + }); + + // 译文各页位置 + translatedPagePositions = []; + const translatedPages = document.querySelectorAll('.translated-page'); + translatedPages.forEach(el => { + const page = parseInt(el.dataset.page); + const rect = el.getBoundingClientRect(); + const panelRect = translatedPanel.getBoundingClientRect(); + translatedPagePositions.push({ + page: page, + top: rect.top - panelRect.top + translatedPanel.scrollTop + }); + }); + } + + // 按页滚动同步 + function syncToPage(sourcePanel, targetPanel, sourcePositions, targetPositions) { + const scrollTop = sourcePanel.scrollTop; + + // 找当前页 + let currentPage = 1; + for (let i = sourcePositions.length - 1; i >= 0; i--) { + if (scrollTop >= sourcePositions[i].top - 50) { + currentPage = sourcePositions[i].page; + break; + } + } + + // 找目标页位置 + const targetPos = targetPositions.find(p => p.page === currentPage); + if (targetPos) { + targetPanel.scrollTop = targetPos.top - 20; + } + } + // 滚动同步 function enableSyncScroll() { const originalPanel = document.getElementById('originalPanel'); @@ -185,17 +275,15 @@ originalPanel.addEventListener('scroll', function() { if (!syncScrollEnabled) return; syncScrollEnabled = false; - const ratio = this.scrollTop / (this.scrollHeight - this.clientHeight); - translatedPanel.scrollTop = ratio * (translatedPanel.scrollHeight - translatedPanel.clientHeight); - setTimeout(() => syncScrollEnabled = true, 50); + syncToPage(originalPanel, translatedPanel, pagePositions, translatedPagePositions); + setTimeout(() => syncScrollEnabled = true, 100); }); translatedPanel.addEventListener('scroll', function() { if (!syncScrollEnabled) return; syncScrollEnabled = false; - const ratio = this.scrollTop / (this.scrollHeight - this.clientHeight); - originalPanel.scrollTop = ratio * (originalPanel.scrollHeight - originalPanel.clientHeight); - setTimeout(() => syncScrollEnabled = true, 50); + syncToPage(translatedPanel, originalPanel, translatedPagePositions, pagePositions); + setTimeout(() => syncScrollEnabled = true, 100); }); }