Files
rainblogweb/frontend/src/tools/workbench/hooks/useWorkbenchLayout.js
T

188 lines
7.4 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.
import { useCallback, useEffect, useMemo, useState } from 'react';
import WidgetsIcon from '@mui/icons-material/Widgets';
import NoteAltIcon from '@mui/icons-material/NoteAlt';
import KeyIcon from '@mui/icons-material/Key';
/* ============================================================
* useWorkbenchLayoutVSCode 风格工作台布局状态(Lane A 新增)
*
* - 视图注册表 VIEWS:左侧主控(面板)+ 右侧功能区(记事本/密码箱)
* - order{ left: [viewId], right: [viewId] } —— 视图在左右活动栏的归属
* (dnd-kit 拖拽换边后持久化,key: workbench.viewOrder
* - active{ left: viewId, right: viewId } —— 每侧当前显示的视图
* - mode{ [viewId]: 'sidebar' | 'tab' | 'float' } —— 展示模式
* sidebar 默认;tab 进主区标签页;float 为 P2 占位,本期不实现)
* - terminalOpen:底部终端面板是否展开
* ============================================================ */
/** 视图注册表:id → 元信息(新增视图只需在此登记 + Workbench 提供渲染) */
export const VIEWS = {
panels: { id: 'panels', label: '面板', Icon: WidgetsIcon, hint: '面板工作台' },
notes: { id: 'notes', label: '记事本', Icon: NoteAltIcon, hint: '笔记与文件' },
vault: { id: 'vault', label: '密码箱', Icon: KeyIcon, hint: '密码管理' },
};
export const VIEW_IDS = Object.keys(VIEWS);
export const VIEW_MODE_SIDEBAR = 'sidebar';
export const VIEW_MODE_TAB = 'tab';
export const VIEW_MODE_FLOAT = 'float'; // P2:本期仅占位
const STORE = {
order: 'workbench.viewOrder', // { left: [], right: [] }
active: 'workbench.viewActive', // { left: id, right: id }
mode: 'workbench.viewMode', // { id: 'sidebar'|'tab' }
terminal: 'workbench.terminalOpen',
};
const DEFAULT_ORDER = { left: ['panels'], right: ['notes', 'vault'] };
const DEFAULT_ACTIVE = { left: 'panels', right: 'notes' };
const DEFAULT_MODE = { panels: VIEW_MODE_SIDEBAR, notes: VIEW_MODE_SIDEBAR, vault: VIEW_MODE_SIDEBAR };
function read(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw === null ? fallback : JSON.parse(raw);
} catch { return fallback; }
}
function write(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* 隐私模式忽略 */ }
}
/** 校验 order 合法性:过滤未知/重复视图,遗漏的补到右侧,且保证每侧至少一个视图 */
function sanitizeOrder(saved) {
if (!saved || !Array.isArray(saved.left) || !Array.isArray(saved.right)) return DEFAULT_ORDER;
const seen = new Set();
const clean = (arr) => {
const out = [];
arr.forEach((id) => {
if (!VIEW_IDS.includes(id) || seen.has(id)) return; // 先查重,后登记
seen.add(id);
out.push(id);
});
return out;
};
const left = clean(saved.left);
const right = clean(saved.right);
VIEW_IDS.forEach((id) => { if (!seen.has(id)) right.push(id); });
// 兜底:持久化可能把视图全挪到一侧(或空数组),刷新后另一侧空白打不开。
// 保证每侧至少一个视图(panels 优先左侧,notes 次之)。
if (left.length === 0 && right.length > 0) {
const preferred = ['panels', 'notes'].find((id) => right.includes(id));
const move = preferred || right[0];
left.push(move);
right.splice(right.indexOf(move), 1);
}
if (right.length === 0 && left.length > 0) {
const preferred = ['notes', 'vault'].find((id) => left.includes(id));
const move = preferred || left[0];
right.push(move);
left.splice(left.indexOf(move), 1);
}
return { left, right };
}
/** 校验 active:指向的视图必须仍在本侧,否则回退到该侧第一个 */
function sanitizeActive(saved, order) {
if (!saved || typeof saved !== 'object') return { ...DEFAULT_ACTIVE };
const pick = (side, fallback) => (
order[side].includes(saved[side]) ? saved[side] : (order[side][0] || fallback)
);
return { left: pick('left', 'panels'), right: pick('right', 'notes') };
}
/** 校验 mode:只接受 sidebar/tabfloat 为保留值,不持久化激活) */
function sanitizeMode(saved) {
const out = { ...DEFAULT_MODE };
if (saved && typeof saved === 'object') {
VIEW_IDS.forEach((id) => {
if (saved[id] === VIEW_MODE_SIDEBAR || saved[id] === VIEW_MODE_TAB) out[id] = saved[id];
});
}
return out;
}
export default function useWorkbenchLayout() {
const [order, setOrder] = useState(() => sanitizeOrder(read(STORE.order, null)));
const [active, setActive] = useState(() => sanitizeActive(read(STORE.active, null), order));
const [mode, setMode] = useState(() => sanitizeMode(read(STORE.mode, null)));
const [terminalOpen, setTerminalOpen] = useState(() => read(STORE.terminal, false));
useEffect(() => write(STORE.order, order), [order]);
useEffect(() => write(STORE.active, active), [active]);
useEffect(() => write(STORE.mode, mode), [mode]);
useEffect(() => write(STORE.terminal, terminalOpen), [terminalOpen]);
/** 视图 id → 所在侧 */
const sideOf = useMemo(() => {
const m = {};
order.left.forEach((id) => { m[id] = 'left'; });
order.right.forEach((id) => { m[id] = 'right'; });
return m;
}, [order]);
const viewsOf = useCallback((side) => [...order[side]], [order]);
/** 每侧有效激活视图(active 指向已移走的视图时回退到第一个) */
const activeViewOf = useCallback(
(side) => (order[side].includes(active[side]) ? active[side] : order[side][0] || null),
[order, active]
);
/** 激活某视图(无论在哪侧) */
const activateView = useCallback((id) => {
const side = sideOf[id];
if (!side) return;
setActive((prev) => ({ ...prev, [side]: id }));
}, [sideOf]);
const setViewMode = useCallback((id, m) => {
if (!VIEW_IDS.includes(id)) return;
setMode((prev) => ({ ...prev, [id]: m }));
}, []);
/** 拖拽换边:视图从一侧移到另一侧,并激活目标侧 */
const moveView = useCallback((id, toSide) => {
if (toSide !== 'left' && toSide !== 'right') return;
setOrder((prev) => {
const fromSide = (['left', 'right']).find((s) => prev[s].includes(id));
if (!fromSide || fromSide === toSide) return prev;
const next = {
left: [...prev.left.filter((x) => x !== id)],
right: [...prev.right.filter((x) => x !== id)],
};
next[toSide].push(id);
return next;
});
setActive((prev) => ({ ...prev, [toSide]: id }));
}, []);
/** 同侧排序(dnd-kit arrayMove 语义) */
const reorderView = useCallback((id, overId) => {
setOrder((prev) => {
const side = (['left', 'right']).find((s) => prev[s].includes(id));
const overSide = (['left', 'right']).find((s) => prev[s].includes(overId));
if (!side || side !== overSide) return prev;
const list = prev[side];
const from = list.indexOf(id);
const to = list.indexOf(overId);
if (from < 0 || to < 0 || from === to) return prev;
const next = [...list];
next.splice(from, 1);
next.splice(to, 0, id);
return { ...prev, [side]: next };
});
}, []);
const toggleTerminal = useCallback(() => setTerminalOpen((v) => !v), []);
const openTerminal = useCallback(() => setTerminalOpen(true), []);
return {
order, sideOf, viewsOf,
active, activeViewOf, activateView,
mode, setViewMode,
moveView, reorderView,
terminalOpen, toggleTerminal, openTerminal,
};
}