Files
pdf-translate-web/templates/translation.html

357 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>翻译详情 - {{ site_config.site_name }}</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">📄 {{ site_config.site_name }}</a>
<div class="navbar-nav ms-auto">
{% if user %}
<span class="nav-link text-light">👋 {{ user.username }}</span>
<a class="nav-link" href="/logout">退出</a>
{% endif %}
</div>
</div>
</nav>
<main class="container my-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">{{ translation.original_filename }}</h4>
<div>
<a href="/api/download/{{ translation.id }}" class="btn btn-success btn-sm">下载结果</a>
<button class="btn btn-outline-primary btn-sm" id="toggleCompare">切换对比</button>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted">
页数: {{ translation.page_count }} |
时间: {{ translation.created_at.strftime('%Y-%m-%d %H:%M') }} |
{% if translation.from_cache %}
<span class="text-success">来自缓存</span>
{% endif %}
</small>
</div>
<div id="resultContent" class="translation-content">
加载中...
</div>
{% if user %}
<hr>
<div class="mt-3">
<h5>重新翻译</h5>
<textarea class="form-control" id="retranslateInstruction" rows="2"
placeholder="输入新的翻译要求,如:更口语化、保留更多原文术语等"></textarea>
<button class="btn btn-primary mt-2" id="retranslateBtn">重新翻译</button>
</div>
{% endif %}
</div>
</div>
</main>
<script>
// 加载翻译结果
const translationId = {{ translation.id }};
let showCompare = false;
async function loadResult() {
try {
const response = await fetch(`/api/result/${translationId}`);
const result = await response.json();
document.getElementById('resultContent').innerHTML = renderMarkdown(result.content);
} catch (error) {
document.getElementById('resultContent').innerHTML = '加载失败: ' + error.message;
}
}
// 切换对比视图
let syncScrollEnabled = true;
let pdfDoc = null;
let pagePositions = []; // PDF各页位置
let translatedPagePositions = []; // 译文各页位置
document.getElementById('toggleCompare').addEventListener('click', async function() {
showCompare = !showCompare;
if (showCompare) {
try {
const response = await fetch(`/api/compare/${translationId}`);
const result = await response.json();
// 原文面板如果有PDF URL用PDF.js渲染
let originalHtml = '';
if (result.original_pdf_url) {
originalHtml = '<div id="pdfPages"></div>';
} else if (result.original && result.original.length > 0) {
originalHtml = `<div class="original-text" style="white-space:pre-wrap;font-family:monospace;">${escapeHtml(result.original)}</div>`;
} else {
originalHtml = '<div class="text-muted">原文内容未找到可能PDF已被删除</div>';
}
// 译文按页渲染
let translatedHtml = renderTranslatedByPage(result.translated);
document.getElementById('resultContent').innerHTML = `
<div class="compare-container">
<div class="compare-panel original" id="originalPanel">
<h5>原文 PDF <small class="text-muted">(共${result.pages.length}页)</small></h5>
<div id="pdfContainer">${originalHtml}</div>
</div>
<div class="compare-panel translated" id="translatedPanel">
<h5>译文 <small class="text-muted">(共${result.pages.length}页)</small></h5>
<div class="translated-content">${translatedHtml}</div>
</div>
</div>
<div class="text-center mt-2">
<small class="text-muted">💡 滚动任意一侧,另一侧自动同步到对应页</small>
</div>
`;
// 先计算译文位置(因为已经渲染好了)
setTimeout(calculateTranslatedPositions, 100);
// 如果有PDF用PDF.js渲染完成后计算位置并启用同步
if (result.original_pdf_url) {
renderPDF(result.original_pdf_url).then(() => {
setTimeout(() => {
calculatePdfPositions();
enableSyncScroll();
}, 200);
});
} else {
setTimeout(enableSyncScroll, 300);
}
} catch (error) {
alert('加载对比失败: ' + error.message);
}
} else {
loadResult();
}
});
// 译文按页渲染
function renderTranslatedByPage(content) {
if (!content) return '<div class="text-muted">译文加载失败</div>';
// 解析分页 - 按 "---" 分隔
const parts = content.split(/\n\n---\n\n/);
let html = '';
parts.forEach((part, index) => {
// 提取页码
const pageNumMatch = part.match(/## 第 (\d+) 页\n\n/);
const pageNum = pageNumMatch ? pageNumMatch[1] : (index + 1);
// 移除页码标题
let contentOnly = part.replace(/## 第 \d+ 页\n\n/, '').replace(/^# .*\n\n/, '').replace(/^> .*\n\n/, '');
html += `
<div class="translated-page" data-page="${pageNum}">
<div class="page-header text-center text-muted mb-3">— 第 ${pageNum} 页 —</div>
<div class="page-content">${renderMarkdown(contentOnly)}</div>
</div>
`;
});
return html;
}
// PDF.js渲染PDF返回Promise
async function renderPDF(url) {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const pdf = await pdfjsLib.getDocument(url).promise;
pdfDoc = pdf;
const container = document.getElementById('pdfPages');
container.innerHTML = '';
// 渲染所有页面
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 });
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
container.appendChild(canvas);
}
return pdf;
}
// 计算译文各页位置(相对于滚动容器顶部)
function calculateTranslatedPositions() {
translatedPagePositions = [];
const translatedPanel = document.getElementById('translatedPanel');
const translatedPages = document.querySelectorAll('.translated-page');
translatedPages.forEach(el => {
const page = parseInt(el.dataset.page);
// 使用getBoundingClientRect计算相对位置
const rect = el.getBoundingClientRect();
const panelRect = translatedPanel.getBoundingClientRect();
// 相对位置 = 元素top - 容器top + 当前scrollTop
const relativeTop = rect.top - panelRect.top + translatedPanel.scrollTop;
translatedPagePositions.push({
page: page,
top: relativeTop
});
});
console.log('译文页位置:', translatedPagePositions);
}
// 计算PDF各页位置相对于滚动容器顶部
function calculatePdfPositions() {
pagePositions = [];
const originalPanel = document.getElementById('originalPanel');
const pdfPages = document.querySelectorAll('#pdfPages .page-header');
pdfPages.forEach(el => {
const page = parseInt(el.dataset.page);
// 使用getBoundingClientRect计算相对位置
const rect = el.getBoundingClientRect();
const panelRect = originalPanel.getBoundingClientRect();
// 相对位置 = 元素top - 容器top + 当前scrollTop
const relativeTop = rect.top - panelRect.top + originalPanel.scrollTop;
pagePositions.push({
page: page,
top: relativeTop
});
});
console.log('PDF页位置:', pagePositions);
}
// 根据滚动位置找当前页
function findCurrentPage(scrollTop, positions) {
for (let i = positions.length - 1; i >= 0; i--) {
if (scrollTop >= positions[i].top - 30) {
return positions[i].page;
}
}
return 1;
}
// 滚动到指定页
function scrollToPage(panel, positions, pageNum) {
const pos = positions.find(p => p.page === pageNum);
if (pos) {
panel.scrollTop = pos.top - 10;
}
}
// 滚动同步
function enableSyncScroll() {
const originalPanel = document.getElementById('originalPanel');
const translatedPanel = document.getElementById('translatedPanel');
if (!originalPanel || !translatedPanel || pagePositions.length === 0 || translatedPagePositions.length === 0) {
console.log('滚动同步未启用: 缺少元素或位置数据');
return;
}
console.log('滚动同步已启用');
originalPanel.addEventListener('scroll', function() {
if (!syncScrollEnabled) return;
const currentPage = findCurrentPage(this.scrollTop, pagePositions);
syncScrollEnabled = false;
scrollToPage(translatedPanel, translatedPagePositions, currentPage);
setTimeout(() => syncScrollEnabled = true, 100);
});
translatedPanel.addEventListener('scroll', function() {
if (!syncScrollEnabled) return;
const currentPage = findCurrentPage(this.scrollTop, translatedPagePositions);
syncScrollEnabled = false;
scrollToPage(originalPanel, pagePositions, currentPage);
setTimeout(() => syncScrollEnabled = true, 100);
});
}
// 重新翻译
document.getElementById('retranslateBtn')?.addEventListener('click', async function() {
const instruction = document.getElementById('retranslateInstruction').value;
if (!instruction.trim()) {
alert('请输入翻译要求');
return;
}
try {
const response = await fetch(`/api/retranslate/${translationId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({instruction: instruction})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error);
}
alert('重译任务已创建,请稍后查看');
window.location.reload();
} catch (error) {
alert('重译失败: ' + error.message);
}
});
// 简单Markdown渲染
function renderMarkdown(text) {
text = text.replace(/^## (.*)$/gm, '<h2>$1</h2>');
text = text.replace(/^# (.*)$/gm, '<h1>$1</h1>');
text = text.replace(/^---$/gm, '<hr>');
text = text.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
text = text.replace(/\n\n/g, '</p><p>');
text = '<p>' + text + '</p>';
return text;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 初始化加载
loadResult();
</script>
</body>
</html>