import React, { useMemo } from 'react'; import { useApp } from '../context/AppContext'; import { NODE_TYPES } from '../utils/fileUtils'; import './Sidebar.css'; function pathHash32(path) { let h = 0; for (let i = 0; i < path.length; i += 1) { h = Math.imul(31, h) + path.charCodeAt(i); } return h >>> 0; } /** 可见顺序:红→橙→黄→绿→青→蓝→紫→浅黑→浅白,循环;均为浅色粉彩 */ const FOLDER_PALETTE_LIGHT = [ { bar: 'hsl(355 58% 64%)', bg: 'hsl(355 48% 97.4%)', border: 'hsl(355 38% 90%)', iconTop: 'hsl(355 62% 88%)', iconBottom: 'hsl(355 58% 72%)', wash: 'hsl(355 42% 95%)', washStrong: 'hsl(355 38% 92%)', muted: 'hsl(350 12% 46%)', }, { bar: 'hsl(28 78% 62%)', bg: 'hsl(32 62% 97.2%)', border: 'hsl(30 48% 89%)', iconTop: 'hsl(32 72% 87%)', iconBottom: 'hsl(28 75% 70%)', wash: 'hsl(32 50% 94.5%)', washStrong: 'hsl(30 45% 91.5%)', muted: 'hsl(28 14% 45%)', }, { bar: 'hsl(48 72% 58%)', bg: 'hsl(50 58% 97.6%)', border: 'hsl(48 42% 89%)', iconTop: 'hsl(52 68% 88%)', iconBottom: 'hsl(48 70% 68%)', wash: 'hsl(50 48% 95%)', washStrong: 'hsl(48 40% 92%)', muted: 'hsl(45 12% 44%)', }, { bar: 'hsl(145 52% 52%)', bg: 'hsl(142 45% 97.3%)', border: 'hsl(145 36% 88%)', iconTop: 'hsl(142 48% 86%)', iconBottom: 'hsl(145 50% 66%)', wash: 'hsl(142 38% 94.5%)', washStrong: 'hsl(145 32% 91%)', muted: 'hsl(145 10% 43%)', }, { bar: 'hsl(178 55% 48%)', bg: 'hsl(175 42% 97.4%)', border: 'hsl(178 34% 88%)', iconTop: 'hsl(175 46% 85%)', iconBottom: 'hsl(178 50% 62%)', wash: 'hsl(175 36% 94.5%)', washStrong: 'hsl(178 30% 91%)', muted: 'hsl(178 10% 42%)', }, { bar: 'hsl(218 62% 62%)', bg: 'hsl(215 50% 97.5%)', border: 'hsl(218 40% 89%)', iconTop: 'hsl(215 58% 87%)', iconBottom: 'hsl(218 62% 68%)', wash: 'hsl(215 42% 95%)', washStrong: 'hsl(218 36% 92%)', muted: 'hsl(218 12% 44%)', }, { bar: 'hsl(268 52% 62%)', bg: 'hsl(265 45% 97.6%)', border: 'hsl(268 36% 90%)', iconTop: 'hsl(265 50% 88%)', iconBottom: 'hsl(268 55% 70%)', wash: 'hsl(265 38% 95%)', washStrong: 'hsl(268 32% 92%)', muted: 'hsl(265 12% 44%)', }, { bar: 'hsl(220 14% 46%)', bg: 'hsl(220 10% 95.8%)', border: 'hsl(220 8% 87%)', iconTop: 'hsl(220 12% 84%)', iconBottom: 'hsl(220 14% 60%)', wash: 'hsl(220 8% 93%)', washStrong: 'hsl(220 6% 90%)', muted: 'hsl(220 10% 42%)', }, { bar: 'hsl(210 12% 74%)', bg: 'hsl(42 28% 98.4%)', border: 'hsl(40 18% 91%)', iconTop: 'hsl(45 22% 94%)', iconBottom: 'hsl(210 10% 78%)', wash: 'hsl(42 22% 96.2%)', washStrong: 'hsl(40 18% 94%)', muted: 'hsl(220 8% 46%)', }, ]; const PALETTE_LEN = FOLDER_PALETTE_LIGHT.length; function folderThemeForSlot(slot) { return FOLDER_PALETTE_LIGHT[((slot % PALETTE_LEN) + PALETTE_LEN) % PALETTE_LEN]; } /** * 按侧边栏当前可见顺序(深度优先:列出文件夹 → 若展开则进入子级)分配 0,1,2… 再对 9 取模。 */ function buildVisibleFolderSlotMap(nodes) { const map = new Map(); let seq = 0; function walk(list) { if (!list) return; for (const n of list) { if (n.type !== NODE_TYPES.FOLDER) continue; map.set(n.path, seq % PALETTE_LEN); seq += 1; if (n.isExpanded && n.children?.length) { walk(n.children); } } } walk(nodes); return map; } function folderGradientId(path) { return `fol-${pathHash32(path).toString(36)}`; } /** 填充式文件夹 SVG,渐变填充;展开时使用略开的轮廓 */ function FolderGlyph({ path, expanded }) { const gid = folderGradientId(path); /* Material Design Icons 风格,viewBox 0 0 24 24 */ const closedPath = 'M10 4H4C2.89 4 2 4.89 2 6v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8c0-1.11-.9-2-2-2h-8l-2-2z'; const openPath = 'M19 20H4C2.89 20 2 19.1 2 18V8C2 6.89 2.89 6 4 6H10L12 4H19A2 2 0 0 1 21 6V18C21 19.1 20.1 20 19 20Z'; return ( ); } function FileGlyph() { return ( ); } function TreeNode({ node, level = 0, folderSlotByPath }) { const { selectFile, toggleNodeExpansion, currentFile } = useApp(); // 计算当前目录下的直接文章数量 const articleCount = useMemo(() => { if (node.type !== NODE_TYPES.FOLDER || !node.children) { return 0; } return node.children.filter(child => child.type === NODE_TYPES.FILE && child.name.endsWith('.md') ).length; }, [node]); const handleClick = () => { if (node.type === NODE_TYPES.FOLDER) { toggleNodeExpansion(node.path); } else { selectFile(node.path); } }; const isSelected = currentFile === node.path; const hasChildren = node.children && node.children.length > 0; const paddingLeft = `${level * 10 + 10}px`; const contentClass = node.type === NODE_TYPES.FOLDER ? `tree-node-content tree-node-content--folder${isSelected ? ' selected' : ''}` : `tree-node-content tree-node-content--file${isSelected ? ' selected' : ''}`; const folderSlot = node.type === NODE_TYPES.FOLDER ? folderSlotByPath.get(node.path) ?? 0 : null; const folderTheme = node.type === NODE_TYPES.FOLDER ? folderThemeForSlot(folderSlot) : null; const folderShellStyle = folderTheme && ({ '--folder-bar': folderTheme.bar, '--folder-bg': folderTheme.bg, '--folder-border': folderTheme.border, '--folder-icon-top': folderTheme.iconTop, '--folder-icon-bottom': folderTheme.iconBottom, '--folder-wash': folderTheme.wash, '--folder-wash-strong': folderTheme.washStrong, '--folder-muted': folderTheme.muted, }); const fileRow = (