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'; /* ============================================================ * useWorkbenchLayout:VSCode 风格工作台布局状态(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/tab(float 为保留值,不持久化激活) */ 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, }; }