zcbot/web/static/js/layout.js

159 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 三栏布局:pane 折叠(rail 模式 + localStorage)+ 拖拽 splitter + 手机单列视图。
// 右栏是应用内工作区:展开时在“文件 / 软件作业”之间切换,折叠时两个
// activity icon 仍可达。这一层不依赖外层 header标准 / embed 共用。
import { $ } from "./dom.js";
import { LS_LEFT_COLLAPSED, LS_RIGHT_COLLAPSED, LS_LEFT_WIDTH, LS_RIGHT_WIDTH } from "./state.js";
// ───── pane 折叠 + splitters(rail 模式 + localStorage 持久化) ─────
const PANE_W = { left: { min: 220, max: 560, def: 320 }, right: { min: 220, max: 560, def: 320 } };
function clampPaneWidth(side, value) {
const cfg = PANE_W[side];
const n = Number(value);
if (!Number.isFinite(n)) return cfg.def;
return Math.max(cfg.min, Math.min(cfg.max, Math.round(n)));
}
function applyPaneWidth(side, value) {
const width = clampPaneWidth(side, value);
$("app").style.setProperty(side === "left" ? "--left-pane-width" : "--right-pane-width", width + "px");
localStorage.setItem(side === "left" ? LS_LEFT_WIDTH : LS_RIGHT_WIDTH, String(width));
}
function restorePaneWidths() {
applyPaneWidth("left", localStorage.getItem(LS_LEFT_WIDTH) || PANE_W.left.def);
applyPaneWidth("right", localStorage.getItem(LS_RIGHT_WIDTH) || PANE_W.right.def);
}
// 折叠 = pane 收成 40px rail,只留 toggle 一直可点;按钮符号根据状态翻向
function applyLeftCollapsed(collapsed) {
document.body.classList.toggle("left-collapsed", collapsed);
const btn = $("pane-toggle-left");
btn.textContent = collapsed ? "" : "";
btn.title = collapsed ? "展开任务列表" : "折叠任务列表";
}
function applyRightCollapsed(collapsed) {
document.body.classList.toggle("right-collapsed", collapsed);
const btn = $("pane-toggle-right");
btn.textContent = collapsed ? "" : "";
btn.title = collapsed ? "展开工作区" : "折叠工作区";
}
let rightPanelTab = "files";
function syncMobileTabSelection(view) {
for (const button of document.querySelectorAll(".mobile-tabs button")) {
const sameView = button.dataset.mv === view;
const sameRightTab = view !== "mv-right"
|| (button.dataset.rightTab || "files") === rightPanelTab;
button.classList.toggle("active", sameView && sameRightTab);
}
}
export function setRightPanelTab(tab, { expand = true } = {}) {
rightPanelTab = tab === "jobs" ? "jobs" : "files";
document.body.classList.toggle("right-tab-jobs", rightPanelTab === "jobs");
$("right-files-panel").setAttribute("aria-hidden", String(rightPanelTab !== "files"));
$("right-jobs-panel").setAttribute("aria-hidden", String(rightPanelTab !== "jobs"));
for (const button of document.querySelectorAll("[data-right-panel-tab]")) {
const active = button.dataset.rightPanelTab === rightPanelTab;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
}
if (expand && document.body.classList.contains("right-collapsed")) {
localStorage.setItem(LS_RIGHT_COLLAPSED, "");
applyRightCollapsed(false);
}
const mobileView = document.body.classList.contains("mv-right") ? "mv-right"
: (document.body.classList.contains("mv-mid") ? "mv-mid" : "mv-left");
syncMobileTabSelection(mobileView);
document.dispatchEvent(new CustomEvent("right-panel-changed", {
detail: { tab: rightPanelTab },
}));
}
$("pane-toggle-left").onclick = () => {
const next = !document.body.classList.contains("left-collapsed");
localStorage.setItem(LS_LEFT_COLLAPSED, next ? "1" : "");
applyLeftCollapsed(next);
};
$("pane-toggle-right").onclick = () => {
const next = !document.body.classList.contains("right-collapsed");
localStorage.setItem(LS_RIGHT_COLLAPSED, next ? "1" : "");
applyRightCollapsed(next);
};
restorePaneWidths();
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
applyRightCollapsed(localStorage.getItem(LS_RIGHT_COLLAPSED) === "1");
function attachPaneSplitter(id, side) {
const el = $(id);
let dragging = false;
el.addEventListener("pointerdown", (ev) => {
if (mqPhone.matches) return;
ev.preventDefault();
dragging = true;
el.classList.add("active");
document.body.classList.add("resizing-panes");
el.setPointerCapture(ev.pointerId);
if (side === "left" && document.body.classList.contains("left-collapsed")) {
localStorage.setItem(LS_LEFT_COLLAPSED, "");
applyLeftCollapsed(false);
}
if (side === "right" && document.body.classList.contains("right-collapsed")) {
localStorage.setItem(LS_RIGHT_COLLAPSED, "");
applyRightCollapsed(false);
}
});
el.addEventListener("pointermove", (ev) => {
if (!dragging) return;
const rect = $("app").getBoundingClientRect();
const width = side === "left" ? ev.clientX - rect.left : rect.right - ev.clientX;
applyPaneWidth(side, width);
});
const stop = (ev) => {
if (!dragging) return;
dragging = false;
el.classList.remove("active");
document.body.classList.remove("resizing-panes");
try { el.releasePointerCapture(ev.pointerId); } catch (_) {}
};
el.addEventListener("pointerup", stop);
el.addEventListener("pointercancel", stop);
}
attachPaneSplitter("split-left", "left");
attachPaneSplitter("split-right", "right");
// ───── 手机视图切换(单列 + tab) ─────
// body.mv-{left,mid,right} 控制当前显示的 pane;桌面下三 pane 都可见,本函数仅维护 class
// 进入手机视口时清掉 collapsed(只 DOM,不动 localStorage —— 回桌面用户偏好仍生效)
export const mqPhone = window.matchMedia("(max-width: 640px)");
export function setMobileView(view, requestedRightTab = "") {
// view ∈ "mv-left" | "mv-mid" | "mv-right"
if (view === "mv-right" && requestedRightTab) {
setRightPanelTab(requestedRightTab, { expand: false });
}
document.body.classList.remove("mv-left", "mv-mid", "mv-right");
document.body.classList.add(view);
syncMobileTabSelection(view);
}
function applyMobileMode() {
if (mqPhone.matches) {
// 手机:清掉桌面 rail 状态,默认显示任务列表(若未设过)
document.body.classList.remove("left-collapsed");
document.body.classList.remove("right-collapsed");
if (!document.body.matches(".mv-left, .mv-mid, .mv-right")) {
setMobileView("mv-left");
}
} else {
// 桌面/平板:恢复 localStorage 的 collapsed 偏好(平板靠 @media 强制 rail,不需依赖 class)
applyLeftCollapsed(localStorage.getItem(LS_LEFT_COLLAPSED) === "1");
applyRightCollapsed(localStorage.getItem(LS_RIGHT_COLLAPSED) === "1");
}
}
mqPhone.addEventListener("change", applyMobileMode);
applyMobileMode();
for (const b of document.querySelectorAll(".mobile-tabs button")) {
b.onclick = () => setMobileView(b.dataset.mv, b.dataset.rightTab || "");
}
for (const button of document.querySelectorAll("[data-right-panel-tab]")) {
button.onclick = () => setRightPanelTab(button.dataset.rightPanelTab);
}
setRightPanelTab("files", { expand: false });