feat: 完成ocrTab的逻辑
This commit is contained in:
parent
95fa091565
commit
b3003c4606
|
|
@ -877,23 +877,30 @@ body.immersive-active #immersive-main-content-area .container {
|
|||
/* ==================== 16. 简单沉浸模式样式 ==================== */
|
||||
|
||||
/* 简单沉浸模式隐藏元素 */
|
||||
body.simple-immersive-mode #fileName,
|
||||
body.simple-immersive-mode .tabs-container,
|
||||
body.simple-immersive-mode #fileMeta {
|
||||
body.simple-immersive-pdf-mode #fileName,
|
||||
body.simple-immersive-pdf-mode .tabs-container,
|
||||
body.simple-immersive-pdf-mode #fileMeta {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 简单沉浸模式下PDF阅读器高度为100vh */
|
||||
body.simple-immersive-mode #pdf-viewer-iframe {
|
||||
height: 100vh !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 1000 !important;
|
||||
/* 简单沉浸模式下 container 的 padding 设为 0 */
|
||||
body.simple-immersive-pdf-mode .container {
|
||||
padding: 0 !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* 简单沉浸模式下 container 的 padding 设为 0 */
|
||||
body.simple-immersive-pdf-mode #immersive-main-content-area .container {
|
||||
padding: 0 !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* 简单沉浸模式下 container 的 padding 设为 0 */
|
||||
body.simple-immersive-pdf-mode .history-export-controls {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* ==================== 17. 打印样式 ==================== */
|
||||
|
||||
@media print {
|
||||
|
|
@ -902,9 +909,9 @@ body.simple-immersive-mode #pdf-viewer-iframe {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
body.simple-immersive-mode #fileName,
|
||||
body.simple-immersive-mode .tabs-container,
|
||||
body.simple-immersive-mode #fileMeta {
|
||||
body.simple-immersive-pdf-mode #fileName,
|
||||
body.simple-immersive-pdf-mode .tabs-container,
|
||||
body.simple-immersive-pdf-mode #fileMeta {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ body {
|
|||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 40px auto;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-base);
|
||||
border-radius: var(--radius-xl);
|
||||
/* 使用变量定义的轻量化阴影 */
|
||||
|
|
@ -86,6 +87,7 @@ body {
|
|||
.tab-content {
|
||||
background: transparent;
|
||||
padding: 8px 0;
|
||||
height: 100vh;
|
||||
min-height: 300px;
|
||||
margin-top: 0;
|
||||
/* 优化阅读体验的字体设置 */
|
||||
|
|
|
|||
53
index.html
53
index.html
|
|
@ -532,30 +532,7 @@
|
|||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen">
|
||||
<script>
|
||||
// 可选:每天一次跳转到落地页(仅在非后端模式且未强制 backend 时启用)
|
||||
(function() {
|
||||
try {
|
||||
var currentPath = window.location.pathname;
|
||||
// 如果明确进入后端模式(通过查询参数或全局变量),则不跳转落地页,避免影响使用
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var forcedMode = (params.get('mode') || '').toLowerCase();
|
||||
var envMode = (window.ENV_DEPLOYMENT_MODE || '').toLowerCase();
|
||||
var shouldSkip = (forcedMode === 'backend') || (envMode === 'backend');
|
||||
|
||||
if (shouldSkip) return;
|
||||
if (currentPath.indexOf('landing-page.html') !== -1) return;
|
||||
|
||||
var today = new Date().toDateString();
|
||||
var lastShownDate = localStorage.getItem('paperBurnerLandingLastShown');
|
||||
if (lastShownDate !== today) {
|
||||
localStorage.setItem('paperBurnerLandingLastShown', today);
|
||||
var basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1);
|
||||
window.location.href = basePath + 'views/landing/landing-page.html';
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div id="sidebarOverlay" class="sidebar-overlay md:hidden"></div>
|
||||
|
|
@ -566,9 +543,11 @@
|
|||
<!-- ===================== -->
|
||||
<aside id="appSidebar" class="app-sidebar">
|
||||
<div class="px-6 py-5 flex items-center justify-between transition-all" id="sidebarHeader">
|
||||
<a href="views/landing/landing-page.html" class="hover:opacity-80 transition-opacity" title="返回落地页">
|
||||
<!-- <a href="views/landing/landing-page.html" class="hover:opacity-80 transition-opacity" title="返回落地页">
|
||||
<img id="sidebarLogo" src="public/h_with_name.svg" class="h-8 transition-all" alt="Paper Burner X">
|
||||
</a>
|
||||
</a> -->
|
||||
<!-- 占位元素 -->
|
||||
<span></span>
|
||||
<!-- Desktop Collapse Button -->
|
||||
<button id="sidebarToggleBtn" class="hidden md:flex text-slate-400 hover:text-slate-600 p-1.5 hover:bg-slate-100 rounded-md transition-colors" title="切换侧边栏">
|
||||
<iconify-icon id="sidebarToggleIcon" icon="carbon:side-panel-close" width="20"></iconify-icon>
|
||||
|
|
@ -605,34 +584,22 @@
|
|||
</div>
|
||||
|
||||
<div class="my-4 border-t border-slate-100 mx-3 transition-all" id="sidebarDivider"></div>
|
||||
<div class="nav-section-title px-3 mb-2 text-[11px] font-bold text-slate-400 uppercase tracking-wider transition-opacity">
|
||||
<!-- <div class="nav-section-title px-3 mb-2 text-[11px] font-bold text-slate-400 uppercase tracking-wider transition-opacity">
|
||||
系统
|
||||
</div>
|
||||
<div id="sidebarSettingsBtn" class="nav-item cursor-pointer" title="全局设置">
|
||||
<iconify-icon icon="carbon:settings" class="nav-icon"></iconify-icon>
|
||||
<span class="nav-text transition-opacity">全局设置</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</nav>
|
||||
<!-- 删除界面会加载不出来 -->
|
||||
<div class="p-3 transition-all" id="sidebarFooter">
|
||||
<div class="p-4 bg-slate-50 rounded-2xl border border-slate-100 overflow-hidden">
|
||||
<a href="https://github.com/Feather-2/paper-burner" target="_blank" class="flex items-start gap-3 hover:opacity-80 transition-opacity" title="GitHub 仓库">
|
||||
<div class="w-10 h-10 rounded-xl bg-white border border-slate-200 flex items-center justify-center text-slate-400 shadow-sm shrink-0">
|
||||
<iconify-icon icon="carbon:logo-github" width="24"></iconify-icon>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 nav-text transition-opacity">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<span id="githubStars" class="inline-flex items-center gap-1 text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full">
|
||||
<iconify-icon icon="carbon:star" width="10"></iconify-icon>
|
||||
<span>加载中...</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">支持自部署,欢迎星标</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="text-center py-1 mt-1 nav-text transition-opacity">
|
||||
<span id="showCopyrightModal" class="text-[11px] text-slate-400 hover:text-slate-600 transition-colors cursor-pointer select-none">
|
||||
Paper Burner X 丨 关于
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,268 @@ function hasOriginalPdfData() {
|
|||
return window.data && window.data.metadata && window.data.metadata.originalPdfBase64;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Base64 字符串转换为 File 对象
|
||||
* @param {string} base64 - Base64 编码的字符串(可带或不带 data: 前缀)
|
||||
* @param {string} filename - 文件名
|
||||
* @returns {File} File 对象
|
||||
*/
|
||||
function base64ToFile(base64, filename) {
|
||||
// 处理 data URL 格式
|
||||
let dataUrl = base64;
|
||||
let mimeString = 'application/pdf';
|
||||
|
||||
if (base64.startsWith('data:')) {
|
||||
const matches = base64.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (matches) {
|
||||
mimeString = matches[1];
|
||||
dataUrl = matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
const byteString = atob(dataUrl);
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return new File([ab], filename || 'document.pdf', { type: mimeString });
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Toast 消息
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} type - 类型: 'info', 'success', 'error', 'warning'
|
||||
* @param {number} duration - 显示时长(毫秒)
|
||||
*/
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
// 检查是否已有 toast 容器
|
||||
let toastContainer = document.getElementById('pbx-toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'pbx-toast-container';
|
||||
toastContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
`;
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// 创建 toast 元素
|
||||
const toast = document.createElement('div');
|
||||
const colors = {
|
||||
info: '#3b82f6',
|
||||
success: '#10b981',
|
||||
error: '#ef4444',
|
||||
warning: '#f59e0b'
|
||||
};
|
||||
|
||||
toast.style.cssText = `
|
||||
padding: 12px 20px;
|
||||
background: ${colors[type] || colors.info};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
// 添加动画样式
|
||||
if (!document.getElementById('pbx-toast-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'pbx-toast-styles';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
toast.textContent = message;
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// 自动移除
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease-out forwards';
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
// 如果容器为空,移除容器
|
||||
if (toastContainer.children.length === 0) {
|
||||
toastContainer.parentNode.removeChild(toastContainer);
|
||||
}
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 OCR 处理
|
||||
* @param {File} file - 要处理的文件
|
||||
* @param {Function} onProgress - 进度回调函数 (current, total, message)
|
||||
* @returns {Promise<Object>} OCR 结果 { markdown, images, metadata }
|
||||
*/
|
||||
async function performOcr(file, onProgress) {
|
||||
// 验证 OcrManager 可用
|
||||
if (typeof OcrManager === 'undefined') {
|
||||
throw new Error('OCR 模块未加载,请刷新页面重试');
|
||||
}
|
||||
|
||||
// 创建 OcrManager 实例
|
||||
const ocrManager = new OcrManager();
|
||||
|
||||
// 执行 OCR
|
||||
const result = await ocrManager.processFile(file, onProgress);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行翻译处理
|
||||
* @param {string} markdown - OCR 后的 Markdown 文本
|
||||
* @param {Function} onProgress - 进度回调函数
|
||||
* @returns {Promise<string>} 翻译后的文本
|
||||
*/
|
||||
async function performTranslation(markdown, onProgress) {
|
||||
// 获取翻译设置
|
||||
const settings = typeof loadSettings === 'function' ? loadSettings() : {};
|
||||
|
||||
const targetLang = settings.targetLanguage || 'zh-CN';
|
||||
const targetLangName = targetLang === 'custom'
|
||||
? (settings.customTargetLanguageName || '中文')
|
||||
: { 'zh-CN': '中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어' }[targetLang] || targetLang;
|
||||
|
||||
const selectedModel = settings.selectedTranslationModel || 'mistral';
|
||||
|
||||
// 获取 API Key
|
||||
let apiKey = '';
|
||||
let modelConfig = null;
|
||||
|
||||
if (selectedModel === 'custom') {
|
||||
// 自定义模型配置
|
||||
modelConfig = settings.translationModelConfig || settings.customModelConfig || null;
|
||||
if (!modelConfig) {
|
||||
throw new Error('请先配置自定义翻译模型');
|
||||
}
|
||||
} else {
|
||||
// 预设模型 - 从 Key 管理器获取 API Key
|
||||
if (typeof loadModelKeys === 'function') {
|
||||
const keys = loadModelKeys(selectedModel);
|
||||
const validKey = keys.find(k => k && k.value && (k.status === 'valid' || k.status === 'untested'));
|
||||
if (validKey) {
|
||||
apiKey = validKey.value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到 localStorage
|
||||
if (!apiKey) {
|
||||
const legacyKey = localStorage.getItem(`${selectedModel}ApiKeys`) || localStorage.getItem('translationApiKeys');
|
||||
if (legacyKey) {
|
||||
apiKey = legacyKey.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`请先配置 ${selectedModel} 模型的 API Key`);
|
||||
}
|
||||
}
|
||||
|
||||
// 分段翻译长文本
|
||||
const chunks = splitMarkdownIntoChunks(markdown, 2000);
|
||||
let translatedText = '';
|
||||
const totalChunks = chunks.length;
|
||||
|
||||
onProgress && onProgress(0, totalChunks, '正在翻译...');
|
||||
|
||||
// 临时禁用 promptPoolUI,避免历史详情页访问不存在的 UI 元素
|
||||
const originalPromptPoolUI = window.promptPoolUI;
|
||||
window.promptPoolUI = undefined;
|
||||
|
||||
try {
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
onProgress && onProgress(i + 1, totalChunks, `翻译中 (${i + 1}/${totalChunks})`);
|
||||
|
||||
try {
|
||||
// 构建翻译选项
|
||||
const translateOptions = {};
|
||||
|
||||
// 如果是自定义模型,需要传入 modelConfig
|
||||
if (selectedModel === 'custom' && modelConfig) {
|
||||
translateOptions.modelConfig = modelConfig;
|
||||
}
|
||||
|
||||
// 调用 translateMarkdown 函数 - 不传 boundPrompt,让它使用内置提示词
|
||||
const chunkResult = await translateMarkdown(
|
||||
chunks[i],
|
||||
targetLangName,
|
||||
selectedModel,
|
||||
apiKey,
|
||||
'[历史详情页翻译]', // logContext
|
||||
'', // defaultSystemPrompt - 空值会触发内置提示词
|
||||
'', // defaultUserPromptTemplate - 空值会触发内置提示词
|
||||
false, // useCustomPrompts
|
||||
true, // processTablePlaceholders
|
||||
translateOptions
|
||||
);
|
||||
|
||||
translatedText += chunkResult + '\n\n';
|
||||
} catch (error) {
|
||||
console.error(`[performTranslation] 翻译第 ${i + 1} 块失败:`, error);
|
||||
// 如果某块翻译失败,保留原文
|
||||
translatedText += chunks[i] + '\n\n';
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 恢复 promptPoolUI
|
||||
window.promptPoolUI = originalPromptPoolUI;
|
||||
}
|
||||
|
||||
return translatedText.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 分割成小块
|
||||
* @param {string} markdown - Markdown 文本
|
||||
* @param {number} maxChars - 每块最大字符数
|
||||
* @returns {string[]} 分割后的块数组
|
||||
*/
|
||||
function splitMarkdownIntoChunks(markdown, maxChars = 2000) {
|
||||
const chunks = [];
|
||||
const lines = markdown.split('\n');
|
||||
let currentChunk = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (currentChunk.length + line.length + 1 > maxChars) {
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
currentChunk = line + '\n';
|
||||
} else {
|
||||
currentChunk += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
return chunks.length > 0 ? chunks : [markdown];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建确认对话框
|
||||
* @param {string} title - 对话框标题
|
||||
|
|
@ -167,22 +429,76 @@ async function triggerReprocess(includeTranslation) {
|
|||
const docName = window.data ? window.data.name : '未知文档';
|
||||
|
||||
if (!docId) {
|
||||
alert('无法获取文档ID,请刷新页面重试。');
|
||||
showToast('无法获取文档ID,请刷新页面重试。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存处理模式到 localStorage,以便主页面读取
|
||||
const reprocessConfig = {
|
||||
docId: docId,
|
||||
docName: docName,
|
||||
includeTranslation: includeTranslation,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem('pbx_reprocess_config', JSON.stringify(reprocessConfig));
|
||||
// 检查是否有原始 PDF 数据
|
||||
const pdfBase64 = window.data?.metadata?.originalPdfBase64;
|
||||
if (!pdfBase64) {
|
||||
showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳转到主页面
|
||||
const baseUrl = window.location.pathname.replace('/views/history/history_detail.html', '/');
|
||||
window.location.href = baseUrl + '?reprocess=1';
|
||||
// 显示处理中 Toast
|
||||
showToast('正在准备文档...', 'info');
|
||||
|
||||
try {
|
||||
// 将 Base64 转为 File 对象
|
||||
const file = base64ToFile(pdfBase64, docName);
|
||||
|
||||
// 执行 OCR
|
||||
showToast('正在进行 OCR 识别...', 'info');
|
||||
|
||||
const ocrResult = await performOcr(file, (current, total, msg) => {
|
||||
showToast(`${msg || 'OCR 处理中'} (${current}/${total})`, 'info', 5000);
|
||||
});
|
||||
|
||||
// 更新 window.data
|
||||
window.data.ocr = ocrResult.markdown;
|
||||
if (ocrResult.images && ocrResult.images.length > 0) {
|
||||
window.data.images = ocrResult.images;
|
||||
}
|
||||
if (ocrResult.metadata) {
|
||||
window.data.metadata = window.data.metadata || {};
|
||||
window.data.metadata.ocrEngine = ocrResult.metadata.engine;
|
||||
window.data.metadata.pageCount = ocrResult.metadata.pageCount;
|
||||
}
|
||||
|
||||
// 如果需要翻译,执行翻译
|
||||
if (includeTranslation) {
|
||||
showToast('正在进行翻译...', 'info');
|
||||
|
||||
try {
|
||||
const translationResult = await performTranslation(ocrResult.markdown, (current, total, msg) => {
|
||||
showToast(`${msg || '翻译中'} (${current}/${total})`, 'info', 5000);
|
||||
});
|
||||
window.data.translation = translationResult;
|
||||
} catch (translationError) {
|
||||
console.error('[triggerReprocess] 翻译失败:', translationError);
|
||||
showToast(`翻译失败: ${translationError.message},但 OCR 已完成`, 'warning');
|
||||
// 继续保存 OCR 结果,即使翻译失败
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到 IndexedDB
|
||||
showToast('正在保存...', 'info');
|
||||
await saveResultToDB(window.data);
|
||||
|
||||
// 刷新页面显示
|
||||
if (typeof renderDetail === 'function') {
|
||||
renderDetail();
|
||||
}
|
||||
if (typeof showTab === 'function') {
|
||||
showTab(includeTranslation ? 'translation' : 'ocr');
|
||||
}
|
||||
|
||||
showToast('处理完成!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[triggerReprocess] 处理失败:', error);
|
||||
showToast(`处理失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -198,7 +514,7 @@ async function handleOcrTabClick() {
|
|||
|
||||
// 检查是否有原始PDF
|
||||
if (!hasOriginalPdfData()) {
|
||||
alert('当前记录没有保存原始PDF数据,无法重新处理。');
|
||||
showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +544,7 @@ async function handleTranslationTabClick() {
|
|||
|
||||
// 检查是否有原始PDF
|
||||
if (!hasOriginalPdfData()) {
|
||||
alert('当前记录没有保存原始PDF数据,无法重新处理。');
|
||||
showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,19 +213,17 @@ function showTabImmediate(tab) {
|
|||
// 构建 iframe 容器
|
||||
document.getElementById('tabContent').innerHTML = `
|
||||
<div id="pdf-iframe-wrapper" style="
|
||||
width: 100%; height: calc(100vh - 180px);
|
||||
width: 100%; height: 100%;
|
||||
min-height: 500px; position: relative;
|
||||
background: #525659;
|
||||
">
|
||||
<div id="pdf-viewer-loading" style="
|
||||
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
||||
color:#ccc;font-size:16px;text-align:center;
|
||||
">
|
||||
<i class="fa fa-spinner fa-spin" style="font-size:32px;display:block;margin-bottom:12px;"></i>
|
||||
正在加载 PDF 查看器...
|
||||
</div>
|
||||
<iframe id="pdf-viewer-iframe" style="
|
||||
width:100%; height:100%; border:none; display:none;
|
||||
width:100%; height:100%; min-height:500px; border:none; display:none;
|
||||
" allowfullscreen></iframe>
|
||||
</div>`;
|
||||
|
||||
|
|
|
|||
|
|
@ -632,13 +632,110 @@
|
|||
if (simpleToggleBtn) {
|
||||
simpleToggleBtn.addEventListener('click', () => {
|
||||
isSimpleImmersiveActive = !isSimpleImmersiveActive;
|
||||
|
||||
// 检查是否在"原始文件"标签页(PDF查看器)
|
||||
const isOriginalFileTab = window.currentVisibleTabId === 'original-file';
|
||||
const pdfIframeWrapper = document.getElementById('pdf-iframe-wrapper');
|
||||
const pdfIframe = document.getElementById('pdf-viewer-iframe');
|
||||
const mainContainer = document.querySelector('.container');
|
||||
|
||||
if (isSimpleImmersiveActive) {
|
||||
document.body.classList.add('simple-immersive-mode');
|
||||
// 如果是PDF查看器标签页,进行特殊处理
|
||||
if (isOriginalFileTab && pdfIframeWrapper && mainContainer) {
|
||||
// 添加特殊类用于PDF全屏模式
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
|
||||
// 移除container的padding
|
||||
mainContainer.dataset.originalPadding = mainContainer.style.padding || '';
|
||||
mainContainer.style.padding = '0 !important';
|
||||
|
||||
// 让iframe wrapper充满container(接近全屏)
|
||||
pdfIframeWrapper.style.cssText = `
|
||||
width: 100%;
|
||||
height: calc(100vh - 20px);
|
||||
min-height: calc(100vh - 20px);
|
||||
position: relative;
|
||||
background: #525659;
|
||||
`;
|
||||
|
||||
if (pdfIframe) {
|
||||
pdfIframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
`;
|
||||
}
|
||||
|
||||
// 隐藏其他UI元素
|
||||
const elementsToHide = [
|
||||
{ selector: '.tabs-container', el: document.querySelector('.tabs-container') },
|
||||
{ selector: '.history-export-controls', el: document.querySelector('.history-export-controls') },
|
||||
{ selector: '#bottom-left-dock', el: document.getElementById('bottom-left-dock') },
|
||||
{ selector: '#toc-popup', el: document.getElementById('toc-popup') },
|
||||
{ selector: '#fileName', el: document.getElementById('fileName') },
|
||||
{ selector: '#fileMeta', el: document.getElementById('fileMeta') }
|
||||
];
|
||||
|
||||
elementsToHide.forEach(item => {
|
||||
if (item.el) {
|
||||
item.el.dataset.originalDisplay = item.el.style.display || '';
|
||||
item.el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏loading指示器
|
||||
const loading = document.getElementById('pdf-viewer-loading');
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
// 确保iframe显示
|
||||
if (pdfIframe) pdfIframe.style.display = 'block';
|
||||
} else {
|
||||
// 非PDF查看器标签页,使用原有的简单沉浸模式
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
}
|
||||
|
||||
simpleToggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i>';
|
||||
simpleToggleBtn.title = '退出简单沉浸模式';
|
||||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'true');
|
||||
} else {
|
||||
document.body.classList.remove('simple-immersive-mode');
|
||||
// 退出简单沉浸模式
|
||||
document.body.classList.remove('simple-immersive-pdf-mode');
|
||||
document.body.classList.remove('simple-immersive-pdf-mode');
|
||||
|
||||
// 恢复container的padding
|
||||
if (mainContainer && mainContainer.dataset.originalPadding !== undefined) {
|
||||
mainContainer.style.padding = mainContainer.dataset.originalPadding;
|
||||
delete mainContainer.dataset.originalPadding;
|
||||
}
|
||||
|
||||
// 恢复所有被隐藏的元素
|
||||
const elementsToRestore = [
|
||||
{ selector: '.tabs-container', el: document.querySelector('.tabs-container') },
|
||||
{ selector: '.history-export-controls', el: document.querySelector('.history-export-controls') },
|
||||
{ selector: '#bottom-left-dock', el: document.getElementById('bottom-left-dock') },
|
||||
{ selector: '#toc-popup', el: document.getElementById('toc-popup') },
|
||||
{ selector: '#fileName', el: document.getElementById('fileName') },
|
||||
{ selector: '#fileMeta', el: document.getElementById('fileMeta') }
|
||||
];
|
||||
|
||||
elementsToRestore.forEach(item => {
|
||||
if (item.el && item.el.dataset.originalDisplay !== undefined) {
|
||||
item.el.style.display = item.el.dataset.originalDisplay;
|
||||
delete item.el.dataset.originalDisplay;
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复iframe wrapper的原始样式
|
||||
if (pdfIframeWrapper) {
|
||||
pdfIframeWrapper.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
position: relative;
|
||||
background: #525659;
|
||||
`;
|
||||
}
|
||||
|
||||
simpleToggleBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
||||
simpleToggleBtn.title = '进入简单沉浸模式';
|
||||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'false');
|
||||
|
|
@ -690,7 +787,7 @@
|
|||
if (!isImmersiveActive) {
|
||||
setTimeout(() => {
|
||||
if (!isSimpleImmersiveActive) {
|
||||
document.body.classList.add('simple-immersive-mode');
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
isSimpleImmersiveActive = true;
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>';
|
||||
|
|
|
|||
|
|
@ -173,12 +173,14 @@
|
|||
<aside class="app-sidebar" id="appSidebar">
|
||||
<!-- 侧边栏头部 -->
|
||||
<div class="sidebar-header">
|
||||
<img
|
||||
<!-- <img
|
||||
src="../../public/h_with_name.svg"
|
||||
alt="Paper Burner X"
|
||||
class="sidebar-logo"
|
||||
id="sidebarLogo"
|
||||
/>
|
||||
/> -->
|
||||
<!-- 占位元素 -->
|
||||
<span></span>
|
||||
<!-- 桌面端收起按钮 -->
|
||||
<button
|
||||
class="sidebar-toggle-btn"
|
||||
|
|
@ -812,6 +814,18 @@
|
|||
<script src="../../js/api/api.js"></script>
|
||||
<script src="../../js/process/translation.js"></script>
|
||||
<script src="../../js/process/mineru-structured-translation.js"></script>
|
||||
|
||||
<!-- OCR Manager 及适配器 -->
|
||||
<script src="../../js/process/ocr-manager.js"></script>
|
||||
<script src="../../js/process/ocr-adapters/base-adapter.js"></script>
|
||||
<script src="../../js/process/ocr-adapters/mistral-adapter.js"></script>
|
||||
<script src="../../js/process/ocr-adapters/mineru-adapter.js"></script>
|
||||
<script src="../../js/process/ocr-adapters/doc2x-adapter.js"></script>
|
||||
<script src="../../js/process/ocr-adapters/local-adapter.js"></script>
|
||||
<script src="../../js/process/ocr.js"></script>
|
||||
|
||||
<!-- OCR 设置管理器 -->
|
||||
<script src="../../js/ui/ocr-settings.js"></script>
|
||||
<script src="../../js/annotations/annotation_logic.js"></script>
|
||||
<!-- Defines helpers like checkIfTextIsHighlighted -->
|
||||
<script src="../../js/annotations/custom_markdown_renderer.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue