feat: 添加仅阅读按钮;默认进入沉浸模式;关闭沉浸模式的出口;隐藏沉浸模式侧边栏。

This commit is contained in:
肖应宇 2026-03-11 09:55:46 +08:00
parent b646d9ef39
commit 56cd3108ed
7 changed files with 225 additions and 44 deletions

View File

@ -56,6 +56,8 @@
#immersive-toc-area.immersive-panel { #immersive-toc-area.immersive-panel {
padding: var(--spacing-xs) var(--spacing-sm) 0 var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm) 0 var(--spacing-sm);
border-right: 1px solid var(--color-border-light); border-right: 1px solid var(--color-border-light);
/* 隐藏沉浸模式侧边栏 */
display: none;
} }
#immersive-chatbot-area.immersive-panel { #immersive-chatbot-area.immersive-panel {
@ -788,6 +790,7 @@ body.immersive-active #immersive-main-content-area .container {
#immersive-toc-area { #immersive-toc-area {
min-width: 200px; min-width: 200px;
} }
#immersive-chatbot-area { #immersive-chatbot-area {

View File

@ -48,7 +48,8 @@
color: #94a3b8 !important; /* slate-400 */ color: #94a3b8 !important; /* slate-400 */
border: 1px solid #e2e8f0 !important; border: 1px solid #e2e8f0 !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
display: flex !important; /* 隐藏沉浸模式的出口 */
display: flex;
align-items: center !important; align-items: center !important;
justify-content: center !important; justify-content: center !important;
cursor: pointer !important; cursor: pointer !important;

View File

@ -986,6 +986,7 @@ body.immersive-dragging .immersive-resize-handle {
#immersive-toc-area { #immersive-toc-area {
min-width: 200px; min-width: 200px;
} }
#immersive-chatbot-area { #immersive-chatbot-area {

View File

@ -1327,7 +1327,11 @@
<div class="flex justify-center mt-8 mb-12"> <div class="flex justify-center mt-8 mb-12">
<button id="processBtn" class="btn-primary-large px-10 py-4 text-lg font-semibold text-white rounded-2xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-3 hover:scale-[1.02] active:scale-[0.98]" style="background: var(--color-primary);"> <button id="processBtn" class="btn-primary-large px-10 py-4 text-lg font-semibold text-white rounded-2xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-3 hover:scale-[1.02] active:scale-[0.98]" style="background: var(--color-primary);">
<iconify-icon icon="carbon:rocket" width="24"></iconify-icon> <iconify-icon icon="carbon:rocket" width="24"></iconify-icon>
<span>开始智能处理</span> <span>开始处理</span>
</button>
<button id="onlyReadBtn" class="btn-primary-large px-10 py-4 text-lg font-semibold text-white rounded-2xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-3 hover:scale-[1.02] active:scale-[0.98]" style="background: var(--color-primary);">
<iconify-icon icon="carbon:rocket" width="24"></iconify-icon>
<span>仅阅读</span>
</button> </button>
</div> </div>
</div> </div>

103
js/app.js
View File

@ -43,6 +43,24 @@ function escapeHtml(str) {
}); });
} }
/**
* ArrayBuffer 转换为 Base64 字符串
* @param {ArrayBuffer} buffer - 需要转换的 ArrayBuffer
* @returns {string} Base64 编码的字符串
*/
function arrayBufferToBase64(buffer) {
if (!buffer) return null;
const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : new Uint8Array(buffer.buffer || []);
if (!bytes.length) return null;
let binary = '';
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
// ===================== // =====================
// 全局状态变量 // 全局状态变量
// ===================== // =====================
@ -523,6 +541,7 @@ function setupEventListeners() {
const githubImportBtn = document.getElementById('githubImportBtn'); const githubImportBtn = document.getElementById('githubImportBtn');
const clearBtn = document.getElementById('clearFilesBtn'); const clearBtn = document.getElementById('clearFilesBtn');
const processBtn = document.getElementById('processBtn'); const processBtn = document.getElementById('processBtn');
const onlyReadBtn = document.getElementById('onlyReadBtn');
const downloadBtn = document.getElementById('downloadAllBtn'); const downloadBtn = document.getElementById('downloadAllBtn');
const formatFilterContainer = document.getElementById('fileFormatFilters'); const formatFilterContainer = document.getElementById('fileFormatFilters');
const batchToggle = document.getElementById('batchModeToggle'); const batchToggle = document.getElementById('batchModeToggle');
@ -752,6 +771,7 @@ function setupEventListeners() {
// 处理和下载 // 处理和下载
processBtn.addEventListener('click', handleProcessClick); processBtn.addEventListener('click', handleProcessClick);
onlyReadBtn.addEventListener('click', handleReadClick);
downloadBtn.addEventListener('click', handleDownloadClick); downloadBtn.addEventListener('click', handleDownloadClick);
if (typeof window.updateDeeplxTargetLangHint === 'function') { if (typeof window.updateDeeplxTargetLangHint === 'function') {
@ -2164,6 +2184,89 @@ function handleDownloadClick() {
} }
} }
// =====================
// 仅阅读(不做处理直接跳转)
// =====================
/**
* 处理点击"仅阅读"按钮的事件
* 如果已有处理结果直接跳转历史详情
* 如果没有处理结果但有上传文件则将文件保存到历史记录后跳转
*/
async function handleReadClick() {
// 1. 优先检查是否有已处理的结果
if (allResults.length > 0) {
const successfulResult = allResults.find(r => r && r.file && !r.error && !r.skipped);
if (successfulResult && successfulResult.file) {
const recordId = `${successfulResult.file.name}_${successfulResult.file.size}`;
window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
return;
}
}
// 2. 检查是否有上传的文件,直接保存到历史记录
if (pdfFiles.length > 0) {
const file = pdfFiles[0]; // 只处理第一个文件
const recordId = `${file.name}_${file.size}`;
try {
// 读取文件内容为 base64
const arrayBuffer = await file.arrayBuffer();
const base64Content = arrayBufferToBase64(arrayBuffer);
// 获取文件扩展名和类型
const ext = file.name.split('.').pop().toLowerCase();
const fileType = ext;
// 创建历史记录对象
const record = {
id: recordId,
name: file.name,
size: file.size,
time: new Date().toISOString(),
ocr: '',
translation: '',
images: [],
ocrChunks: [],
translatedChunks: [],
fileType: fileType,
targetLanguage: '',
originalEncoding: 'binary',
originalBinary: base64Content,
originalExtension: ext,
ocrEngine: null,
ocrSource: null,
translationModelName: 'none',
batchId: null,
batchOrder: null,
batchTotal: null,
batchTemplate: null,
batchFormats: null,
batchStartedAt: null
};
// 保存到数据库
if (typeof window.storageAdapter !== 'undefined' && typeof window.storageAdapter.saveResultToDB === 'function') {
await window.storageAdapter.saveResultToDB(record);
} else if (typeof saveResultToDB === 'function') {
await saveResultToDB(record);
} else {
showNotification('存储功能不可用', 'error');
return;
}
// 跳转到历史详情页面
window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
} catch (error) {
console.error('保存文件到历史记录失败:', error);
showNotification(`保存失败: ${error.message}`, 'error');
}
} else {
showNotification('请先上传文件', 'warning');
}
}
// ===================== // =====================
// 内置提示模板获取 // 内置提示模板获取
// ===================== // =====================

View File

@ -218,10 +218,17 @@
isTocDockResizing = false; // Reset flag isTocDockResizing = false; // Reset flag
} }
function enterImmersiveMode() { function enterImmersiveMode(options = {}) {
const { silent = false } = options;
// 检查是否为移动端设备 // 检查是否为移动端设备
if (window.innerWidth <= 700) { if (window.innerWidth <= 700) {
console.warn('拒绝在移动端≤700px进入沉浸式布局'); console.warn('拒绝在移动端≤700px进入沉浸式布局');
// 即使不进入沉浸模式,也需要显示页面
document.documentElement.classList.remove('immersive-pending');
document.documentElement.classList.add('immersive-ready');
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
return; return;
} }
@ -238,19 +245,41 @@
if (!immersiveChatbotArea) missingElements.push('immersiveChatbotArea'); if (!immersiveChatbotArea) missingElements.push('immersiveChatbotArea');
if (!immersiveDockPlaceholderElement) missingElements.push('immersiveDockPlaceholderElement (logic error if this happens)'); if (!immersiveDockPlaceholderElement) missingElements.push('immersiveDockPlaceholderElement (logic error if this happens)');
console.log('[enterImmersiveMode] 元素检查:', {
immersiveContainer: !!immersiveContainer,
mainPageContainer: !!mainPageContainer,
tocPopupElement: !!tocPopupElement,
chatbotModalElement: !!chatbotModalElement,
dockElement: !!dockElement,
immersiveTocArea: !!immersiveTocArea,
immersiveMainArea: !!immersiveMainArea,
immersiveChatbotArea: !!immersiveChatbotArea
});
if (missingElements.length > 0) { if (missingElements.length > 0) {
console.warn('Immersive mode elements not found:', missingElements.join(', ')); console.warn('Immersive mode elements not found:', missingElements.join(', '));
// 元素缺失时也需要显示页面
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
return; return;
} }
isImmersiveActive = true; isImmersiveActive = true;
storeOriginalPositions(); storeOriginalPositions();
// 添加进入动画类 // 静默模式:跳过动画,直接设置状态
document.body.classList.add('immersive-entering'); if (silent) {
immersiveContainer.style.opacity = '0'; document.body.classList.add('immersive-active', 'no-scroll');
immersiveContainer.style.transform = 'scale(0.95)'; immersiveContainer.style.display = 'flex';
immersiveContainer.style.transition = 'opacity 0.4s ease, transform 0.4s ease'; immersiveContainer.style.opacity = '1';
immersiveContainer.style.transform = 'scale(1)';
} else {
// 添加进入动画类
document.body.classList.add('immersive-entering');
immersiveContainer.style.opacity = '0';
immersiveContainer.style.transform = 'scale(0.95)';
immersiveContainer.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
}
// Append TOC content first // Append TOC content first
if (tocPopupElement && immersiveTocArea) { if (tocPopupElement && immersiveTocArea) {
@ -305,26 +334,35 @@
} }
} }
document.body.classList.add('immersive-active', 'no-scroll'); // 静默模式下已经设置了 display 和 opacity跳过动画
immersiveContainer.style.display = 'flex'; if (!silent) {
document.body.classList.add('immersive-active', 'no-scroll');
// 简单的强制重新计算,修复初始化时的布局问题 immersiveContainer.style.display = 'flex';
setTimeout(() => {
if (immersiveContainer) {
immersiveContainer.offsetHeight; // 触发重新布局
}
}, 0);
// 动画进入效果
requestAnimationFrame(() => {
immersiveContainer.style.opacity = '1';
immersiveContainer.style.transform = 'scale(1)';
// 简单的强制重新计算,修复初始化时的布局问题
setTimeout(() => { setTimeout(() => {
document.body.classList.remove('immersive-entering'); if (immersiveContainer) {
immersiveContainer.style.transition = ''; immersiveContainer.offsetHeight; // 触发重新布局
}, 400); }
}); }, 0);
// 动画进入效果
requestAnimationFrame(() => {
immersiveContainer.style.opacity = '1';
immersiveContainer.style.transform = 'scale(1)';
setTimeout(() => {
document.body.classList.remove('immersive-entering');
immersiveContainer.style.transition = '';
}, 400);
});
}
// 显示页面(移除 pending 状态)
document.documentElement.classList.remove('immersive-pending');
document.documentElement.classList.add('immersive-ready');
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
if (toggleBtn) { if (toggleBtn) {
toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>'; toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>';
@ -393,7 +431,6 @@
localStorage.setItem(LS_IMMERSIVE_KEY, 'true'); localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
document.dispatchEvent(new CustomEvent('immersiveModeEntered')); document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
} }
function exitImmersiveMode() { function exitImmersiveMode() {
reQueryDynamicElements(); reQueryDynamicElements();
isImmersiveActive = false; isImmersiveActive = false;
@ -758,21 +795,36 @@
// Restore immersive state from localStorage // Restore immersive state from localStorage
const savedImmersiveState = localStorage.getItem(LS_IMMERSIVE_KEY); const savedImmersiveState = localStorage.getItem(LS_IMMERSIVE_KEY);
if (savedImmersiveState === 'true') {
// 历史详情页默认进入沉浸模式,只有用户明确退出过才不进入
// 注意null 表示首次访问,'true' 表示用户之前处于沉浸模式
// 只有 'false' 才表示用户主动退出过沉浸模式
const shouldEnterImmersive = savedImmersiveState !== 'false';
console.log('[ImmersiveLayout] savedImmersiveState:', savedImmersiveState, 'shouldEnterImmersive:', shouldEnterImmersive);
if (shouldEnterImmersive) {
// 检查是否为移动端,如果是则不恢复沉浸模式 // 检查是否为移动端,如果是则不恢复沉浸模式
if (window.innerWidth <= 700) { if (window.innerWidth <= 700) {
console.log('检测到移动端设备,不恢复沉浸式布局状态'); console.log('检测到移动端设备,不进入沉浸式布局');
localStorage.setItem(LS_IMMERSIVE_KEY, 'false'); // 清除保存的状态 localStorage.setItem(LS_IMMERSIVE_KEY, 'false');
return; // 显示页面
document.documentElement.classList.remove('immersive-pending');
document.documentElement.classList.add('immersive-ready');
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
} else {
// 立即进入沉浸模式(静默模式,无动画)
console.log('[ImmersiveLayout] 准备进入沉浸模式, isImmersiveActive:', isImmersiveActive);
enterImmersiveMode({ silent: true });
console.log('[ImmersiveLayout] enterImmersiveMode 调用完成, isImmersiveActive:', isImmersiveActive);
} }
} else {
// Slight delay to ensure other initializations (like TOC, Chatbot) can occur first // 不需要沉浸模式,直接显示页面
// especially if they also interact with elements moved by immersive mode. document.documentElement.classList.remove('immersive-pending');
setTimeout(() => { document.documentElement.classList.add('immersive-ready');
if (!isImmersiveActive) { // Check again in case of race conditions or manual toggle document.body.classList.remove('immersive-pending');
enterImmersiveMode(); document.body.classList.add('immersive-ready');
}
}, 200); // Adjust delay if needed
} }
// Restore simple immersive state from localStorage // Restore simple immersive state from localStorage

View File

@ -86,10 +86,27 @@
<link rel="stylesheet" href="../../css/history_detail.css" /> <link rel="stylesheet" href="../../css/history_detail.css" />
<!-- All CSS now imported via history_detail.css (Phase 9 完成) --> <!-- All CSS now imported via history_detail.css (Phase 9 完成) -->
<!-- 页面初始隐藏,等待沉浸模式初始化 -->
<style>
html.immersive-pending body,
body.immersive-pending {
opacity: 0;
}
html.immersive-ready body,
body.immersive-ready {
opacity: 1;
transition: opacity 0.2s ease;
}
</style>
<script>
// 在 DOM 解析前就添加类,避免闪烁
document.documentElement.classList.add('immersive-pending');
</script>
</head> </head>
<body> <body class="immersive-pending">
<!-- Standard Immersive Mode Toggle Button --> <!-- Standard Immersive Mode Toggle Button -->
<button id="toggle-immersive-btn" class="tiny-round-btn" title="进入沉浸式布局"> <button id="toggle-immersive-btn" class="tiny-round-btn hidden" style="display: none;" title="进入沉浸式布局">
<i class="fas fa-expand-alt"></i> <i class="fas fa-expand-alt"></i>
<!-- Default icon for entering immersive mode --> <!-- Default icon for entering immersive mode -->
</button> </button>
@ -400,9 +417,9 @@
<h2 id="fileName">历史详情</h2> <h2 id="fileName">历史详情</h2>
<div class="meta" id="fileMeta"> <div class="meta" id="fileMeta">
<div class="meta-info"> <div class="meta-info">
<span id="fileMetaTime"></span> <!-- <span id="fileMetaTime"></span>
<span class="meta-separator">|</span> <span class="meta-separator">|</span>
<span id="fileMetaImages"></span> <span id="fileMetaImages"></span> -->
</div> </div>
<button <button
class="meta-export-trigger" class="meta-export-trigger"