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

View File

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

View File

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

View File

@ -1327,7 +1327,11 @@
<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);">
<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>
</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 clearBtn = document.getElementById('clearFilesBtn');
const processBtn = document.getElementById('processBtn');
const onlyReadBtn = document.getElementById('onlyReadBtn');
const downloadBtn = document.getElementById('downloadAllBtn');
const formatFilterContainer = document.getElementById('fileFormatFilters');
const batchToggle = document.getElementById('batchModeToggle');
@ -752,6 +771,7 @@ function setupEventListeners() {
// 处理和下载
processBtn.addEventListener('click', handleProcessClick);
onlyReadBtn.addEventListener('click', handleReadClick);
downloadBtn.addEventListener('click', handleDownloadClick);
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
}
function enterImmersiveMode() {
function enterImmersiveMode(options = {}) {
const { silent = false } = options;
// 检查是否为移动端设备
if (window.innerWidth <= 700) {
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;
}
@ -238,19 +245,41 @@
if (!immersiveChatbotArea) missingElements.push('immersiveChatbotArea');
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) {
console.warn('Immersive mode elements not found:', missingElements.join(', '));
// 元素缺失时也需要显示页面
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
return;
}
isImmersiveActive = true;
storeOriginalPositions();
// 添加进入动画类
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';
// 静默模式:跳过动画,直接设置状态
if (silent) {
document.body.classList.add('immersive-active', 'no-scroll');
immersiveContainer.style.display = 'flex';
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
if (tocPopupElement && immersiveTocArea) {
@ -305,26 +334,35 @@
}
}
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)';
// 静默模式下已经设置了 display 和 opacity跳过动画
if (!silent) {
document.body.classList.add('immersive-active', 'no-scroll');
immersiveContainer.style.display = 'flex';
// 简单的强制重新计算,修复初始化时的布局问题
setTimeout(() => {
document.body.classList.remove('immersive-entering');
immersiveContainer.style.transition = '';
}, 400);
});
if (immersiveContainer) {
immersiveContainer.offsetHeight; // 触发重新布局
}
}, 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) {
toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>';
@ -393,7 +431,6 @@
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
}
function exitImmersiveMode() {
reQueryDynamicElements();
isImmersiveActive = false;
@ -758,21 +795,36 @@
// Restore immersive state from localStorage
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) {
console.log('检测到移动端设备,不恢复沉浸式布局状态');
localStorage.setItem(LS_IMMERSIVE_KEY, 'false'); // 清除保存的状态
return;
console.log('检测到移动端设备,不进入沉浸式布局');
localStorage.setItem(LS_IMMERSIVE_KEY, 'false');
// 显示页面
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);
}
// Slight delay to ensure other initializations (like TOC, Chatbot) can occur first
// especially if they also interact with elements moved by immersive mode.
setTimeout(() => {
if (!isImmersiveActive) { // Check again in case of race conditions or manual toggle
enterImmersiveMode();
}
}, 200); // Adjust delay if needed
} else {
// 不需要沉浸模式,直接显示页面
document.documentElement.classList.remove('immersive-pending');
document.documentElement.classList.add('immersive-ready');
document.body.classList.remove('immersive-pending');
document.body.classList.add('immersive-ready');
}
// Restore simple immersive state from localStorage

View File

@ -86,10 +86,27 @@
<link rel="stylesheet" href="../../css/history_detail.css" />
<!-- 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>
<body>
<body class="immersive-pending">
<!-- 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>
<!-- Default icon for entering immersive mode -->
</button>
@ -400,9 +417,9 @@
<h2 id="fileName">历史详情</h2>
<div class="meta" id="fileMeta">
<div class="meta-info">
<span id="fileMetaTime"></span>
<!-- <span id="fileMetaTime"></span>
<span class="meta-separator">|</span>
<span id="fileMetaImages"></span>
<span id="fileMetaImages"></span> -->
</div>
<button
class="meta-export-trigger"