872 lines
37 KiB
JavaScript
872 lines
37 KiB
JavaScript
// js/ui/immersive_layout_logic.js
|
||
(function ImmersiveLayoutLogic(global) {
|
||
let toggleBtn, immersiveContainer;
|
||
let mainPageContainer, tocPopupElement, chatbotModalElement, dockElement;
|
||
let immersiveTocArea, immersiveMainArea, immersiveChatbotArea, immersiveDockPlaceholderElement;
|
||
let tocVsDockResizeHandle;
|
||
let isTocDockResizing = false; // Flag to indicate dragging state
|
||
let initialTocDockMouseY, initialTocHeight, initialDockHeight;
|
||
|
||
// Original parent and next sibling of the elements to be moved
|
||
let originalMainContainerParent = null;
|
||
let originalMainContainerNextSibling = null;
|
||
let originalTocPopupParent = null;
|
||
let originalTocPopupNextSibling = null;
|
||
let originalChatbotModalParent = null;
|
||
let originalChatbotModalNextSibling = null;
|
||
let originalDockElementParent = null;
|
||
let originalDockElementNextSibling = null;
|
||
|
||
let isImmersiveActive = false;
|
||
let isSimpleImmersiveActive = false; // 新增:简单沉浸模式状态
|
||
const LS_IMMERSIVE_KEY = 'immersiveLayoutActive';
|
||
const LS_SIMPLE_IMMERSIVE_KEY = 'simpleImmersiveLayoutActive'; // 新增:简单沉浸模式存储键
|
||
const LS_PANEL_SIZES_KEY = 'immersivePanelSizes';
|
||
|
||
function initializeDomElements() {
|
||
toggleBtn = document.getElementById('toggle-immersive-btn');
|
||
immersiveContainer = document.getElementById('immersive-layout-container');
|
||
|
||
mainPageContainer = document.querySelector('.container');
|
||
tocPopupElement = document.getElementById('toc-popup');
|
||
chatbotModalElement = document.getElementById('chatbot-modal');
|
||
dockElement = document.getElementById('bottom-left-dock');
|
||
|
||
immersiveTocArea = document.getElementById('immersive-toc-area');
|
||
immersiveMainArea = document.getElementById('immersive-main-content-area');
|
||
immersiveChatbotArea = document.getElementById('immersive-chatbot-area');
|
||
|
||
// Create the dock placeholder dynamically
|
||
immersiveDockPlaceholderElement = document.createElement('div');
|
||
immersiveDockPlaceholderElement.id = 'toc-dock-placeholder';
|
||
// Basic styles can be applied here if not fully covered by CSS, or rely on CSS.
|
||
// For now, CSS will handle styling based on the ID.
|
||
|
||
return toggleBtn && immersiveContainer && mainPageContainer && tocPopupElement && immersiveTocArea && immersiveMainArea && immersiveChatbotArea && dockElement /* && immersiveDockPlaceholderElement (element itself exists, not a check if found in DOM here) */;
|
||
}
|
||
|
||
function reQueryDynamicElements() {
|
||
if (!chatbotModalElement) {
|
||
chatbotModalElement = document.getElementById('chatbot-modal');
|
||
}
|
||
if (!mainPageContainer) mainPageContainer = document.querySelector('.container');
|
||
if (!tocPopupElement) tocPopupElement = document.getElementById('toc-popup');
|
||
if (!dockElement) dockElement = document.getElementById('bottom-left-dock');
|
||
}
|
||
|
||
function storeOriginalPositions() {
|
||
if (mainPageContainer) {
|
||
originalMainContainerParent = mainPageContainer.parentNode;
|
||
originalMainContainerNextSibling = mainPageContainer.nextSibling;
|
||
}
|
||
if (tocPopupElement) {
|
||
originalTocPopupParent = tocPopupElement.parentNode;
|
||
originalTocPopupNextSibling = tocPopupElement.nextSibling;
|
||
}
|
||
if (chatbotModalElement) {
|
||
originalChatbotModalParent = chatbotModalElement.parentNode;
|
||
originalChatbotModalNextSibling = chatbotModalElement.nextSibling;
|
||
} else {
|
||
console.warn('storeOriginalPositions: chatbotModalElement not found.');
|
||
}
|
||
if (dockElement) {
|
||
originalDockElementParent = dockElement.parentNode;
|
||
originalDockElementNextSibling = dockElement.nextSibling;
|
||
} else {
|
||
console.warn('storeOriginalPositions: dockElement not found.');
|
||
}
|
||
}
|
||
|
||
function savePanelSizes() {
|
||
if (!isImmersiveActive || !immersiveContainer || immersiveContainer.style.display === 'none') return;
|
||
if (!immersiveTocArea || !immersiveMainArea || !immersiveChatbotArea) return;
|
||
|
||
const tocWidth = immersiveTocArea.style.width;
|
||
const mainWidth = immersiveMainArea.style.width;
|
||
const chatbotWidth = immersiveChatbotArea.style.width;
|
||
localStorage.setItem(LS_PANEL_SIZES_KEY, JSON.stringify({ toc: tocWidth, main: mainWidth, chatbot: chatbotWidth }));
|
||
}
|
||
|
||
function loadPanelSizes() {
|
||
if (!immersiveTocArea || !immersiveMainArea || !immersiveChatbotArea) return;
|
||
|
||
const savedSizes = localStorage.getItem(LS_PANEL_SIZES_KEY);
|
||
if (savedSizes) {
|
||
try {
|
||
const sizes = JSON.parse(savedSizes);
|
||
if (sizes.toc) immersiveTocArea.style.width = sizes.toc;
|
||
if (sizes.chatbot) immersiveChatbotArea.style.width = sizes.chatbot;
|
||
} catch (e) {
|
||
console.error('Error loading panel sizes:', e);
|
||
immersiveTocArea.style.width = '20%';
|
||
immersiveMainArea.style.flex = '1';
|
||
immersiveMainArea.style.width = '';
|
||
immersiveChatbotArea.style.width = '25%';
|
||
}
|
||
} else {
|
||
immersiveTocArea.style.width = '20%';
|
||
immersiveMainArea.style.flex = '1';
|
||
immersiveMainArea.style.width = '';
|
||
immersiveChatbotArea.style.width = '25%';
|
||
}
|
||
}
|
||
|
||
function initializeTocDockResizer() {
|
||
if (!tocVsDockResizeHandle || !tocPopupElement || !immersiveDockPlaceholderElement || !immersiveTocArea) {
|
||
console.warn("TOC vs Dock resizer: Missing one or more elements.");
|
||
return;
|
||
}
|
||
|
||
// Ensure event listeners are not duplicated if called multiple times
|
||
tocVsDockResizeHandle.removeEventListener('mousedown', handleTocDockDragStart);
|
||
tocVsDockResizeHandle.addEventListener('mousedown', handleTocDockDragStart);
|
||
}
|
||
|
||
function handleTocDockDragStart(event) {
|
||
event.preventDefault();
|
||
isTocDockResizing = true;
|
||
|
||
initialTocDockMouseY = event.clientY;
|
||
initialTocHeight = tocPopupElement.offsetHeight;
|
||
initialDockHeight = immersiveDockPlaceholderElement.offsetHeight;
|
||
|
||
// Add class to body for global cursor/selection styles
|
||
document.body.classList.add('toc-dock-resizing');
|
||
|
||
// Add temporary flex-grow overrides if needed, or rely on flex-basis
|
||
// tocPopupElement.style.flexGrow = '0';
|
||
// immersiveDockPlaceholderElement.style.flexGrow = '0';
|
||
|
||
|
||
document.addEventListener('mousemove', handleTocDockDragMove);
|
||
document.addEventListener('mouseup', handleTocDockDragEnd);
|
||
}
|
||
|
||
function handleTocDockDragMove(event) {
|
||
if (!isTocDockResizing) return;
|
||
event.preventDefault();
|
||
|
||
const deltaY = event.clientY - initialTocDockMouseY;
|
||
let newTocHeight = initialTocHeight + deltaY;
|
||
let newDockHeight = initialDockHeight - deltaY;
|
||
|
||
const minPanelHeight = 40; // Minimum height for TOC and Dock panels
|
||
|
||
// Constrain TOC height
|
||
if (newTocHeight < minPanelHeight) {
|
||
newTocHeight = minPanelHeight;
|
||
// Recalculate dock height based on constrained TOC
|
||
newDockHeight = initialTocHeight + initialDockHeight - newTocHeight;
|
||
}
|
||
|
||
// Constrain Dock height
|
||
if (newDockHeight < minPanelHeight) {
|
||
newDockHeight = minPanelHeight;
|
||
// Recalculate TOC height based on constrained Dock
|
||
newTocHeight = initialTocHeight + initialDockHeight - newDockHeight;
|
||
}
|
||
|
||
// Ensure total height of tocArea children doesn't exceed tocArea height if it's fixed
|
||
// For flexbox, adjusting flex-basis is usually better.
|
||
// The sum of newTocHeight and newDockHeight should ideally be initialTocHeight + initialDockHeight.
|
||
// The logic above ensures this if one hits minPanelHeight.
|
||
|
||
// Apply new heights using flex-basis for better control in a flex container
|
||
tocPopupElement.style.flexBasis = newTocHeight + 'px';
|
||
immersiveDockPlaceholderElement.style.flexBasis = newDockHeight + 'px';
|
||
|
||
// If not using flex-basis, and want to force height and disable grow/shrink during drag:
|
||
// tocPopupElement.style.height = newTocHeight + 'px';
|
||
// immersiveDockPlaceholderElement.style.height = newDockHeight + 'px';
|
||
// tocPopupElement.style.flexGrow = '0';
|
||
// immersiveDockPlaceholderElement.style.flexGrow = '0';
|
||
|
||
}
|
||
|
||
function handleTocDockDragEnd() {
|
||
if (!isTocDockResizing) return;
|
||
isTocDockResizing = false;
|
||
|
||
document.body.classList.remove('toc-dock-resizing');
|
||
document.removeEventListener('mousemove', handleTocDockDragMove);
|
||
document.removeEventListener('mouseup', handleTocDockDragEnd);
|
||
|
||
// Restore original flex-grow properties if they were changed during drag
|
||
// tocPopupElement.style.flexGrow = ''; // Or to its original value e.g., '10'
|
||
// immersiveDockPlaceholderElement.style.flexGrow = ''; // Or to its original value e.g., '1'
|
||
// However, since we are using flex-basis, the original flex-grow values from CSS should still apply
|
||
// once flex-basis is set, unless grow/shrink are explicitly set to 0.
|
||
// The CSS has flex-grow: 10 for toc and flex-grow: 1 for dock placeholder.
|
||
// Setting flex-basis should work fine with these grow factors if space allows.
|
||
|
||
// Optional: Save the new heights/proportions to localStorage
|
||
// const tocAreaHeight = immersiveTocArea.offsetHeight;
|
||
// if (tocAreaHeight > 0) {
|
||
// const tocRatio = tocPopupElement.offsetHeight / tocAreaHeight;
|
||
// localStorage.setItem('immersiveTocDockRatio', tocRatio.toFixed(4));
|
||
// }
|
||
}
|
||
|
||
function destroyTocDockResizer() {
|
||
if (tocVsDockResizeHandle) {
|
||
tocVsDockResizeHandle.removeEventListener('mousedown', handleTocDockDragStart);
|
||
}
|
||
// Clean up global listeners if somehow left hanging (should be removed by handleTocDockDragEnd)
|
||
document.removeEventListener('mousemove', handleTocDockDragMove);
|
||
document.removeEventListener('mouseup', handleTocDockDragEnd);
|
||
document.body.classList.remove('toc-dock-resizing');
|
||
isTocDockResizing = false; // Reset flag
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
reQueryDynamicElements();
|
||
|
||
let missingElements = [];
|
||
if (!immersiveContainer) missingElements.push('immersiveContainer');
|
||
if (!mainPageContainer) missingElements.push('mainPageContainer');
|
||
if (!tocPopupElement) missingElements.push('tocPopupElement');
|
||
if (!chatbotModalElement) missingElements.push('chatbotModalElement');
|
||
if (!dockElement) missingElements.push('dockElement');
|
||
if (!immersiveTocArea) missingElements.push('immersiveTocArea');
|
||
if (!immersiveMainArea) missingElements.push('immersiveMainArea');
|
||
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();
|
||
|
||
// 静默模式:跳过动画,直接设置状态
|
||
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) {
|
||
immersiveTocArea.appendChild(tocPopupElement);
|
||
}
|
||
|
||
// Create and insert the resize handle between TOC and Dock Placeholder
|
||
if (immersiveTocArea) {
|
||
if (!tocVsDockResizeHandle) { // Create if it doesn't exist
|
||
tocVsDockResizeHandle = document.createElement('div');
|
||
tocVsDockResizeHandle.id = 'toc-vs-dock-resize-handle';
|
||
}
|
||
if (tocPopupElement && tocPopupElement.parentNode === immersiveTocArea) {
|
||
immersiveTocArea.insertBefore(tocVsDockResizeHandle, tocPopupElement.nextSibling);
|
||
} else {
|
||
immersiveTocArea.appendChild(tocVsDockResizeHandle);
|
||
}
|
||
}
|
||
|
||
// Then append the dock placeholder to TOC area
|
||
if (immersiveDockPlaceholderElement && immersiveTocArea) {
|
||
if (tocVsDockResizeHandle && tocVsDockResizeHandle.parentNode === immersiveTocArea) {
|
||
immersiveTocArea.insertBefore(immersiveDockPlaceholderElement, tocVsDockResizeHandle.nextSibling);
|
||
} else {
|
||
immersiveTocArea.appendChild(immersiveDockPlaceholderElement);
|
||
}
|
||
}
|
||
|
||
// Move other elements
|
||
if (mainPageContainer && immersiveMainArea) {
|
||
immersiveMainArea.appendChild(mainPageContainer);
|
||
}
|
||
if (chatbotModalElement && immersiveChatbotArea) {
|
||
immersiveChatbotArea.appendChild(chatbotModalElement);
|
||
}
|
||
|
||
// Move the actual dock into its placeholder
|
||
if (dockElement && immersiveDockPlaceholderElement) {
|
||
immersiveDockPlaceholderElement.appendChild(dockElement);
|
||
|
||
// Force Dock to be expanded in immersive mode
|
||
if (dockElement.classList.contains('dock-collapsed')) {
|
||
dockElement.classList.remove('dock-collapsed');
|
||
const dockToggleBtn = document.getElementById('dock-toggle-btn');
|
||
if (dockToggleBtn) {
|
||
dockToggleBtn.innerHTML = '<i class="fa fa-chevron-down"></i>';
|
||
dockToggleBtn.title = '折叠';
|
||
}
|
||
if (typeof window !== 'undefined' && window.docIdForLocalStorage) {
|
||
localStorage.setItem(`dockCollapsed_${window.docIdForLocalStorage}`, 'false');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 静默模式下已经设置了 display 和 opacity,跳过动画
|
||
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(() => {
|
||
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>';
|
||
toggleBtn.classList.add('immersive-exit-btn-active');
|
||
toggleBtn.title = '退出沉浸模式';
|
||
}
|
||
|
||
// 其他初始化逻辑保持不变...
|
||
if (typeof window.refreshTocList === 'function') {
|
||
window.refreshTocList();
|
||
}
|
||
if (window.ChatbotUI && typeof window.ChatbotUI.updateChatbotUI === 'function') {
|
||
window.isChatbotOpen = true;
|
||
window.isChatbotFullscreen = false;
|
||
window.forceChatbotWidthReset = true;
|
||
window.ChatbotUI.updateChatbotUI();
|
||
}
|
||
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function' && window.data && window.currentVisibleTabId) {
|
||
dockElement.style.display = '';
|
||
window.DockLogic.updateStats(window.data, window.currentVisibleTabId);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
if (window.DockLogic) {
|
||
if (typeof window.DockLogic.unbindScrollForCurrentScrollable === 'function') {
|
||
console.log("[ImmersiveLayout] 进入沉浸模式前,先解绑旧的滚动事件");
|
||
window.DockLogic.unbindScrollForCurrentScrollable();
|
||
}
|
||
if (typeof window.DockLogic.forceUpdateReadingProgress === 'function') {
|
||
console.log("[ImmersiveLayout] 进入沉浸模式后,延迟调用 forceUpdateReadingProgress");
|
||
window.DockLogic.forceUpdateReadingProgress();
|
||
}
|
||
}
|
||
|
||
// 为滚动容器添加标记 class,供 JS 查询使用
|
||
if (immersiveMainArea) {
|
||
const container = immersiveMainArea.querySelector('.container');
|
||
if (container) {
|
||
const tabContent = container.querySelector('.tab-content');
|
||
if (tabContent) {
|
||
// 添加 .js-scroll-container 标记,供 DockLogic 等模块查询
|
||
// CSS 通过 body.immersive-active 选择器自动控制 overflow
|
||
tabContent.classList.add('js-scroll-container');
|
||
console.log("[ImmersiveLayout] 已为 tab-content 添加 js-scroll-container 标记");
|
||
}
|
||
}
|
||
}
|
||
}, 300);
|
||
|
||
// Force TOC to be expanded in immersive mode
|
||
if (tocPopupElement && !tocPopupElement.classList.contains('toc-expanded')) {
|
||
const tocExpandBtn = document.getElementById('toc-expand-btn');
|
||
if (tocExpandBtn) {
|
||
tocPopupElement.classList.add('toc-expanded');
|
||
const icon = tocExpandBtn.querySelector('i');
|
||
if (icon) {
|
||
icon.classList.remove('fa-angles-right');
|
||
icon.classList.add('fa-angles-left');
|
||
}
|
||
tocExpandBtn.title = '收起目录';
|
||
}
|
||
}
|
||
|
||
loadPanelSizes();
|
||
initializeTocDockResizer();
|
||
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
|
||
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
|
||
}
|
||
function exitImmersiveMode() {
|
||
reQueryDynamicElements();
|
||
isImmersiveActive = false;
|
||
|
||
// 添加退出动画
|
||
document.body.classList.add('immersive-exiting');
|
||
if (immersiveContainer) {
|
||
immersiveContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||
immersiveContainer.style.opacity = '0';
|
||
immersiveContainer.style.transform = 'scale(0.98)';
|
||
}
|
||
|
||
setTimeout(() => {
|
||
destroyTocDockResizer();
|
||
|
||
// Remove the TOC vs Dock resize handle
|
||
if (tocVsDockResizeHandle && tocVsDockResizeHandle.parentNode === immersiveTocArea) {
|
||
immersiveTocArea.removeChild(tocVsDockResizeHandle);
|
||
}
|
||
|
||
// 恢复元素位置的逻辑保持不变...
|
||
if (originalTocPopupParent && tocPopupElement && tocPopupElement.parentNode === immersiveTocArea) {
|
||
originalTocPopupParent.insertBefore(tocPopupElement, originalTocPopupNextSibling);
|
||
} else if (tocPopupElement && immersiveTocArea.contains(tocPopupElement)) {
|
||
immersiveTocArea.removeChild(tocPopupElement);
|
||
if (originalTocPopupParent) {
|
||
originalTocPopupParent.insertBefore(tocPopupElement, originalTocPopupNextSibling);
|
||
}
|
||
}
|
||
|
||
if (originalMainContainerParent && mainPageContainer && mainPageContainer.parentNode === immersiveMainArea) {
|
||
originalMainContainerParent.insertBefore(mainPageContainer, originalMainContainerNextSibling);
|
||
} else if (mainPageContainer && immersiveMainArea.contains(mainPageContainer)) {
|
||
immersiveMainArea.removeChild(mainPageContainer);
|
||
if (originalMainContainerParent) {
|
||
originalMainContainerParent.insertBefore(mainPageContainer, originalMainContainerNextSibling);
|
||
}
|
||
}
|
||
|
||
if (chatbotModalElement && originalChatbotModalParent && chatbotModalElement.parentNode === immersiveChatbotArea) {
|
||
originalChatbotModalParent.insertBefore(chatbotModalElement, originalChatbotModalNextSibling);
|
||
} else if (chatbotModalElement && immersiveChatbotArea.contains(chatbotModalElement)) {
|
||
immersiveChatbotArea.removeChild(chatbotModalElement);
|
||
if (originalChatbotModalParent) {
|
||
originalChatbotModalParent.insertBefore(chatbotModalElement, originalChatbotModalNextSibling);
|
||
}
|
||
} else if (!chatbotModalElement && originalChatbotModalParent) {
|
||
immersiveChatbotArea.innerHTML = '';
|
||
}
|
||
|
||
if (dockElement && originalDockElementParent) {
|
||
if (immersiveDockPlaceholderElement && immersiveDockPlaceholderElement.contains(dockElement)) {
|
||
immersiveDockPlaceholderElement.removeChild(dockElement);
|
||
}
|
||
originalDockElementParent.insertBefore(dockElement, originalDockElementNextSibling);
|
||
} else if (dockElement && immersiveDockPlaceholderElement && immersiveDockPlaceholderElement.contains(dockElement)){
|
||
immersiveDockPlaceholderElement.removeChild(dockElement);
|
||
}
|
||
|
||
document.body.classList.remove('immersive-active', 'no-scroll', 'immersive-exiting');
|
||
if (immersiveContainer) {
|
||
immersiveContainer.style.display = 'none';
|
||
immersiveContainer.style.transition = '';
|
||
immersiveContainer.style.opacity = '';
|
||
immersiveContainer.style.transform = '';
|
||
}
|
||
|
||
// 清理滚动容器标记 class
|
||
if (mainPageContainer) {
|
||
const tabContent = mainPageContainer.querySelector('.tab-content');
|
||
if (tabContent) {
|
||
tabContent.classList.remove('js-scroll-container');
|
||
console.log("[ImmersiveLayout] 已移除 tab-content 的 js-scroll-container 标记");
|
||
}
|
||
}
|
||
|
||
if (toggleBtn) {
|
||
toggleBtn.innerHTML = '<i class="fas fa-expand-alt"></i>';
|
||
toggleBtn.classList.remove('immersive-exit-btn-active');
|
||
toggleBtn.title = '进入沉浸式布局';
|
||
}
|
||
|
||
// 其他退出逻辑保持不变...
|
||
if (typeof window.refreshTocList === 'function') {
|
||
window.refreshTocList();
|
||
}
|
||
|
||
if (typeof window.docIdForLocalStorage !== 'undefined' && window.docIdForLocalStorage) {
|
||
const savedChatbotOpenState = localStorage.getItem(`chatbotOpenState_${window.docIdForLocalStorage}`);
|
||
if (savedChatbotOpenState === 'true') {
|
||
window.isChatbotOpen = true;
|
||
} else if (savedChatbotOpenState === 'false') {
|
||
window.isChatbotOpen = false;
|
||
}
|
||
}
|
||
|
||
if (window.ChatbotUI && typeof window.ChatbotUI.updateChatbotUI === 'function') {
|
||
window.ChatbotUI.updateChatbotUI();
|
||
}
|
||
|
||
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function' && window.data && window.currentVisibleTabId) {
|
||
dockElement.style.display = '';
|
||
window.DockLogic.updateStats(window.data, window.currentVisibleTabId);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
if (window.DockLogic) {
|
||
if (typeof window.DockLogic.unbindScrollForCurrentScrollable === 'function') {
|
||
console.log("[ImmersiveLayout] 退出沉浸模式前,先解绑旧的滚动事件");
|
||
window.DockLogic.unbindScrollForCurrentScrollable();
|
||
}
|
||
if (typeof window.DockLogic.forceUpdateReadingProgress === 'function') {
|
||
console.log("[ImmersiveLayout] 退出沉浸模式后,延迟调用 forceUpdateReadingProgress");
|
||
window.DockLogic.forceUpdateReadingProgress();
|
||
}
|
||
}
|
||
}, 300);
|
||
|
||
localStorage.setItem(LS_IMMERSIVE_KEY, 'false');
|
||
document.dispatchEvent(new CustomEvent('immersiveModeExited'));
|
||
}, 300);
|
||
}
|
||
|
||
function initResizeHandles() {
|
||
const handles = document.querySelectorAll('.immersive-resize-handle');
|
||
let activeHandle = null;
|
||
let startX, startWidthPrev, startWidthNext;
|
||
|
||
handles.forEach(handle => {
|
||
handle.addEventListener('mousedown', function(e) {
|
||
activeHandle = this;
|
||
startX = e.clientX;
|
||
const prevPanelId = activeHandle.dataset.targetPrev;
|
||
const nextPanelId = activeHandle.dataset.targetNext;
|
||
const prevPanel = document.getElementById(prevPanelId);
|
||
const nextPanel = document.getElementById(nextPanelId);
|
||
|
||
if (!prevPanel || !nextPanel) {
|
||
console.warn(`Resize panels not found: ${prevPanelId} or ${nextPanelId}`);
|
||
activeHandle = null;
|
||
return;
|
||
}
|
||
|
||
startWidthPrev = prevPanel.offsetWidth;
|
||
startWidthNext = nextPanel.offsetWidth;
|
||
|
||
document.body.classList.add('immersive-dragging');
|
||
document.body.style.userSelect = 'none';
|
||
document.body.style.cursor = 'col-resize';
|
||
|
||
e.preventDefault();
|
||
});
|
||
});
|
||
|
||
document.addEventListener('mousemove', function(e) {
|
||
if (!activeHandle) return;
|
||
|
||
const dx = e.clientX - startX;
|
||
const prevPanel = document.getElementById(activeHandle.dataset.targetPrev);
|
||
const nextPanel = document.getElementById(activeHandle.dataset.targetNext);
|
||
|
||
if (!prevPanel || !nextPanel) return;
|
||
|
||
const newWidthPrev = startWidthPrev + dx;
|
||
const newWidthNext = startWidthNext - dx;
|
||
const minPanelWidth = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--immersive-panel-min-width')) || 80;
|
||
|
||
if (newWidthPrev >= minPanelWidth && newWidthNext >= minPanelWidth) {
|
||
prevPanel.style.width = newWidthPrev + 'px';
|
||
nextPanel.style.width = newWidthNext + 'px';
|
||
|
||
// 平滑的过渡动画
|
||
prevPanel.style.transition = 'none';
|
||
nextPanel.style.transition = 'none';
|
||
}
|
||
});
|
||
|
||
document.addEventListener('mouseup', function() {
|
||
if (activeHandle) {
|
||
savePanelSizes();
|
||
|
||
// 恢复样式
|
||
document.body.classList.remove('immersive-dragging');
|
||
document.body.style.userSelect = '';
|
||
document.body.style.cursor = '';
|
||
|
||
// 恢复过渡动画
|
||
const prevPanel = document.getElementById(activeHandle.dataset.targetPrev);
|
||
const nextPanel = document.getElementById(activeHandle.dataset.targetNext);
|
||
if (prevPanel) prevPanel.style.transition = '';
|
||
if (nextPanel) nextPanel.style.transition = '';
|
||
|
||
activeHandle = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
function mainInit() {
|
||
if (!initializeDomElements()) {
|
||
console.warn('Immersive layout core static elements not found on DOMContentLoaded. Retrying shortly...');
|
||
setTimeout(mainInit, 500);
|
||
return;
|
||
}
|
||
|
||
reQueryDynamicElements();
|
||
|
||
// 标准沉浸模式切换按钮
|
||
const standardToggleBtn = document.getElementById('toggle-immersive-btn');
|
||
// 简单沉浸模式切换按钮
|
||
const simpleToggleBtn = document.getElementById('toggle-simple-immersive-btn');
|
||
|
||
// 标准沉浸模式功能
|
||
if (standardToggleBtn) {
|
||
standardToggleBtn.addEventListener('click', () => {
|
||
// 检查是否为移动端设备(屏幕宽度小于等于700px)
|
||
if (window.innerWidth <= 700) {
|
||
console.warn('沉浸式布局在移动端(≤700px)不可用');
|
||
// 可选:显示提示消息
|
||
if (window.showToast) {
|
||
window.showToast('沉浸式布局在手机端不可用', 'warning');
|
||
} else {
|
||
alert('沉浸式布局在手机端不可用,请在更大的屏幕上使用');
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (isImmersiveActive) {
|
||
exitImmersiveMode();
|
||
} else {
|
||
enterImmersiveMode();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 简单沉浸模式功能
|
||
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) {
|
||
// 如果是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-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');
|
||
}
|
||
});
|
||
}
|
||
|
||
initResizeHandles();
|
||
|
||
// 监听窗口大小变化,如果变成移动端尺寸则自动退出沉浸模式
|
||
function handleWindowResize() {
|
||
if (window.innerWidth <= 700 && isImmersiveActive) {
|
||
console.log('检测到屏幕缩小到移动端尺寸,自动退出沉浸式布局');
|
||
exitImmersiveMode();
|
||
}
|
||
}
|
||
|
||
// 添加窗口大小变化监听器
|
||
window.addEventListener('resize', handleWindowResize);
|
||
|
||
// Restore immersive state from localStorage
|
||
const savedImmersiveState = localStorage.getItem(LS_IMMERSIVE_KEY);
|
||
|
||
// 历史详情页默认进入沉浸模式,只有用户明确退出过才不进入
|
||
// 注意: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');
|
||
// 显示页面
|
||
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 {
|
||
// 不需要沉浸模式,直接显示页面
|
||
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
|
||
const savedSimpleImmersiveState = localStorage.getItem(LS_SIMPLE_IMMERSIVE_KEY);
|
||
if (savedSimpleImmersiveState === 'true') {
|
||
// 检查是否为移动端,如果是则不启用简单沉浸模式
|
||
if (window.innerWidth <= 700) {
|
||
console.log('检测到移动端设备,不启用简单沉浸式布局状态');
|
||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'false'); // 清除保存的状态
|
||
} else {
|
||
// 如果不在标准沉浸模式下,才启用简单沉浸模式
|
||
if (!isImmersiveActive) {
|
||
setTimeout(() => {
|
||
if (!isSimpleImmersiveActive) {
|
||
document.body.classList.add('simple-immersive-pdf-mode');
|
||
isSimpleImmersiveActive = true;
|
||
if (toggleBtn) {
|
||
toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>';
|
||
toggleBtn.title = '退出简单沉浸模式';
|
||
}
|
||
}
|
||
}, 200);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('Immersive layout logic initialized.');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', mainInit);
|
||
} else {
|
||
mainInit();
|
||
}
|
||
|
||
global.ImmersiveLayout = {
|
||
isActive: () => isImmersiveActive,
|
||
isSimpleActive: () => isSimpleImmersiveActive, // 新增:简单沉浸模式状态查询
|
||
enter: enterImmersiveMode,
|
||
exit: exitImmersiveMode
|
||
};
|
||
|
||
})(window);
|
||
|