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

252 lines
11 KiB
HTML
Raw 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;
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 = '<canvas id="pdfCanvas"></canvas><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>';
}
document.getElementById('resultContent').innerHTML = `
<div class="compare-container">
<div class="compare-panel original" id="originalPanel">
<h5>原文 PDF</h5>
<div id="pdfContainer">${originalHtml}</div>
</div>
<div class="compare-panel translated" id="translatedPanel">
<h5>译文</h5>
<div class="translated-content">${renderMarkdown(result.translated)}</div>
</div>
</div>
<div class="text-center mt-2">
<small class="text-muted">💡 左右滚动同步,方便逐页对比</small>
</div>
`;
// 如果有PDF用PDF.js渲染
if (result.original_pdf_url) {
renderPDF(result.original_pdf_url);
}
// 启用滚动同步
setTimeout(enableSyncScroll, 300);
} catch (error) {
alert('加载对比失败: ' + error.message);
}
} else {
loadResult();
}
});
// 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';
try {
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 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';
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);
// 添加页码标识
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 =
'<div class="text-danger">PDF加载失败: ' + error.message + '</div>';
}
}
// 滚动同步
function enableSyncScroll() {
const originalPanel = document.getElementById('originalPanel');
const translatedPanel = document.getElementById('translatedPanel');
if (!originalPanel || !translatedPanel) return;
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);
});
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);
});
}
// 重新翻译
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>