245 lines
7.6 KiB
JavaScript
245 lines
7.6 KiB
JavaScript
// layout.js -- 侧栏 / 背景 / 标题栏 / 响应式
|
|
|
|
import { $ } from './utils.js';
|
|
import { state, STORAGE_DOF, STORAGE_VIG, STORAGE_HEADER_COLLAPSED } from './state.js';
|
|
|
|
const appRoot = $("app");
|
|
const mainPanel = $("mainPanel");
|
|
const menuBtn = $("menuBtn");
|
|
const drawerBackdrop = $("drawerBackdrop");
|
|
const bgImageLayer = $("bgImageLayer");
|
|
const headerToggleBtn = $("headerToggleBtn");
|
|
const dofRange = $("dofRange");
|
|
const dofValue = $("dofValue");
|
|
const vignetteRange = $("vignetteRange");
|
|
const vignetteValue = $("vignetteValue");
|
|
|
|
const STORAGE_SIDEBAR_W = "minor_sidebar_width";
|
|
const SIDEBAR_MIN_W = 150;
|
|
const SIDEBAR_MAX_W = 420;
|
|
|
|
function setSidebarCollapsed(collapsed) {
|
|
state.sidebarCollapsed = collapsed;
|
|
if (!appRoot) return;
|
|
appRoot.classList.toggle("sidebar-collapsed", collapsed);
|
|
}
|
|
|
|
function setDrawerOpen(open) {
|
|
if (!appRoot) return;
|
|
appRoot.classList.toggle("drawer-open", open);
|
|
if (menuBtn) menuBtn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
if (drawerBackdrop) drawerBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
|
|
}
|
|
|
|
function updateMobileLayout() {
|
|
if (!appRoot) return;
|
|
const mobile = window.matchMedia("(max-width: 768px)").matches;
|
|
appRoot.classList.toggle("is-mobile", mobile);
|
|
if (!mobile) setDrawerOpen(false);
|
|
}
|
|
|
|
function closeDrawerIfMobile() {
|
|
if (!appRoot) return;
|
|
if (appRoot.classList.contains("is-mobile")) setDrawerOpen(false);
|
|
}
|
|
|
|
// ===== 标题栏 =====
|
|
function setHeaderCollapsed(collapsed) {
|
|
if (!mainPanel || !headerToggleBtn) return;
|
|
mainPanel.classList.toggle("header-collapsed", collapsed);
|
|
headerToggleBtn.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
headerToggleBtn.title = collapsed ? "展开标题栏" : "收起标题栏";
|
|
headerToggleBtn.textContent = collapsed ? "\u25BE" : "\u25B4";
|
|
try {
|
|
localStorage.setItem(STORAGE_HEADER_COLLAPSED, collapsed ? "1" : "0");
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
function loadHeaderPref() {
|
|
if (!mainPanel || !headerToggleBtn) return;
|
|
try {
|
|
if (localStorage.getItem(STORAGE_HEADER_COLLAPSED) === "1") setHeaderCollapsed(true);
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
// ===== 背景控制 =====
|
|
function applyBgVars() {
|
|
const dofPx = dofRange ? String(dofRange.value) : "0";
|
|
const vig = vignetteRange ? Number(vignetteRange.value) / 100 : 0.35;
|
|
document.documentElement.style.setProperty("--bg-dof", `${dofPx}px`);
|
|
document.documentElement.style.setProperty("--bg-vignette", String(Math.max(0, Math.min(1, vig))));
|
|
if (dofValue) dofValue.textContent = `${dofPx} px`;
|
|
if (vignetteValue) vignetteValue.textContent = `${vignetteRange ? vignetteRange.value : 35} %`;
|
|
applyBackgroundFit();
|
|
}
|
|
|
|
function loadBgPrefs() {
|
|
try {
|
|
const d = localStorage.getItem(STORAGE_DOF);
|
|
if (d != null && dofRange) dofRange.value = d;
|
|
const v = localStorage.getItem(STORAGE_VIG);
|
|
if (v != null && vignetteRange) vignetteRange.value = v;
|
|
} catch { /* ignore */ }
|
|
applyBgVars();
|
|
}
|
|
|
|
let bgNaturalW = 0;
|
|
let bgNaturalH = 0;
|
|
let bgFitTimer = null;
|
|
|
|
function backgroundImageHref() {
|
|
try {
|
|
return new URL("image/background.jpg", window.location.href).href;
|
|
} catch {
|
|
return "image/background.jpg";
|
|
}
|
|
}
|
|
|
|
function readDofPx() {
|
|
const raw = getComputedStyle(document.documentElement).getPropertyValue("--bg-dof").trim();
|
|
const n = parseFloat(raw);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
function applyBackgroundFit() {
|
|
if (!bgImageLayer || !bgNaturalW || !bgNaturalH) return;
|
|
const vw = window.innerWidth;
|
|
const vh = window.innerHeight;
|
|
const s = Math.max(vw / bgNaturalW, vh / bgNaturalH);
|
|
const dof = readDofPx();
|
|
const bleed = 1 + Math.min(Math.max(dof, 0), 24) * 0.018;
|
|
const bw = bgNaturalW * s * bleed;
|
|
const bh = bgNaturalH * s * bleed;
|
|
bgImageLayer.style.backgroundSize = `${bw}px ${bh}px`;
|
|
bgImageLayer.style.backgroundPosition = "center center";
|
|
}
|
|
|
|
function scheduleBackgroundFit() {
|
|
if (bgFitTimer) clearTimeout(bgFitTimer);
|
|
bgFitTimer = setTimeout(() => {
|
|
bgFitTimer = null;
|
|
applyBackgroundFit();
|
|
}, 100);
|
|
}
|
|
|
|
function wireBackgroundImage() {
|
|
if (!bgImageLayer) return;
|
|
const href = backgroundImageHref();
|
|
bgImageLayer.style.backgroundImage = `url(${JSON.stringify(href)})`;
|
|
const probe = new Image();
|
|
probe.onload = () => {
|
|
bgNaturalW = probe.naturalWidth;
|
|
bgNaturalH = probe.naturalHeight;
|
|
if (bgNaturalW > 0 && bgNaturalH > 0) applyBackgroundFit();
|
|
};
|
|
probe.onerror = () => {
|
|
bgNaturalW = 0;
|
|
bgNaturalH = 0;
|
|
bgImageLayer.style.backgroundSize = "cover";
|
|
};
|
|
probe.src = href;
|
|
}
|
|
|
|
function onResizeLayout() {
|
|
updateMobileLayout();
|
|
scheduleBackgroundFit();
|
|
}
|
|
|
|
// 初始化背景与标题栏
|
|
loadBgPrefs();
|
|
loadHeaderPref();
|
|
|
|
if (dofRange) {
|
|
dofRange.addEventListener("input", () => {
|
|
applyBgVars();
|
|
applyBackgroundFit();
|
|
try { localStorage.setItem(STORAGE_DOF, dofRange.value); } catch { /* ignore */ }
|
|
});
|
|
}
|
|
if (vignetteRange) {
|
|
vignetteRange.addEventListener("input", () => {
|
|
applyBgVars();
|
|
try { localStorage.setItem(STORAGE_VIG, vignetteRange.value); } catch { /* ignore */ }
|
|
});
|
|
}
|
|
|
|
window.addEventListener("resize", onResizeLayout);
|
|
updateMobileLayout();
|
|
wireBackgroundImage();
|
|
|
|
// ===== 侧栏拖动调整宽度 =====
|
|
function initSidebarResize() {
|
|
const sidebar = document.getElementById("sidebar");
|
|
const handle = document.getElementById("sidebarResizeHandle");
|
|
if (!sidebar || !handle) return;
|
|
|
|
// 恢复保存的宽度
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_SIDEBAR_W);
|
|
if (saved) {
|
|
const w = parseInt(saved, 10);
|
|
if (w >= SIDEBAR_MIN_W && w <= SIDEBAR_MAX_W) {
|
|
document.documentElement.style.setProperty("--sidebar-w", w + "px");
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
let dragging = false;
|
|
let startX = 0;
|
|
let startW = 0;
|
|
|
|
/** 结束拖拽并复位全部副作用(鼠标在窗口外松开 / 失焦时也会被调用) */
|
|
function endDrag() {
|
|
if (!dragging) return;
|
|
dragging = false;
|
|
// 恢复 transition
|
|
sidebar.style.transition = "";
|
|
document.body.style.cursor = "";
|
|
document.body.style.userSelect = "";
|
|
// 保存宽度
|
|
const finalW = sidebar.getBoundingClientRect().width;
|
|
try { localStorage.setItem(STORAGE_SIDEBAR_W, String(Math.round(finalW))); } catch { /* ignore */ }
|
|
}
|
|
|
|
// Pointer Events + setPointerCapture:拖拽期间事件由 handle 捕获,
|
|
// 鼠标移到窗口外/焦点丢失时仍能收到 pointerup,不会残留拖拽状态
|
|
handle.addEventListener("pointerdown", (e) => {
|
|
if (e.button !== 0) return; // 仅主键
|
|
if (appRoot && appRoot.classList.contains("sidebar-collapsed")) return;
|
|
e.preventDefault();
|
|
dragging = true;
|
|
startX = e.clientX;
|
|
startW = sidebar.getBoundingClientRect().width;
|
|
try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }
|
|
// 拖动时禁用 transition 以保证流畅
|
|
sidebar.style.transition = "none";
|
|
document.body.style.cursor = "col-resize";
|
|
document.body.style.userSelect = "none";
|
|
});
|
|
|
|
handle.addEventListener("pointermove", (e) => {
|
|
if (!dragging) return;
|
|
const dx = e.clientX - startX;
|
|
let newW = startW + dx;
|
|
newW = Math.max(SIDEBAR_MIN_W, Math.min(SIDEBAR_MAX_W, newW));
|
|
document.documentElement.style.setProperty("--sidebar-w", newW + "px");
|
|
});
|
|
|
|
handle.addEventListener("pointerup", endDrag);
|
|
handle.addEventListener("pointercancel", endDrag);
|
|
// 兜底:失焦(如拖拽中点开其他窗口)时复位,避免 cursor/user-select 残留
|
|
window.addEventListener("blur", endDrag);
|
|
}
|
|
|
|
initSidebarResize();
|
|
|
|
if (headerToggleBtn) {
|
|
headerToggleBtn.addEventListener("click", () => {
|
|
setHeaderCollapsed(!mainPanel.classList.contains("header-collapsed"));
|
|
});
|
|
}
|
|
|
|
export {
|
|
setSidebarCollapsed, setDrawerOpen, closeDrawerIfMobile,
|
|
};
|