// Columns.jsx — 左栏(时间/地点轴)、中栏(笔记列表)、右栏(本子系列) const { useState: useStateCol } = React; // ---- 由笔记聚合时间树 / 地点树(区分已写入 vs 采集箱)---- function buildTimeTree(notes) { const years = {}; notes.forEach(n => { if (!n.year) return; // 只有地点、没有时间的采集(如口述地点)不进时间轴 const y = years[n.year] = years[n.year] || { months: {}, total: 0, inbox: 0 }; const m = y.months[n.month] = y.months[n.month] || { count: 0, inbox: 0 }; if (n._inbox) { y.inbox++; m.inbox++; } else { y.total++; m.count++; } }); return Object.keys(years).sort((a, b) => b - a).map(y => ({ year: y, total: years[y].total, inbox: years[y].inbox, months: Object.keys(years[y].months).sort((a, b) => parseInt(b) - parseInt(a)) .map(m => ({ month: m, count: years[y].months[m].count, inbox: years[y].months[m].inbox })), })); } function buildPlaceTree(notes) { const cities = {}; notes.forEach(n => { if (!n.city) return; const c = cities[n.city] = cities[n.city] || { scenes: {}, total: 0, inbox: 0 }; const s = c.scenes[n.scene] = c.scenes[n.scene] || { count: 0, inbox: 0 }; if (n._inbox) { c.inbox++; s.inbox++; } else { c.total++; s.count++; } }); return Object.keys(cities).map(c => ({ city: c, total: cities[c].total, inbox: cities[c].inbox, scenes: Object.keys(cities[c].scenes).map(s => ({ scene: s, count: cities[c].scenes[s].count, inbox: cities[c].scenes[s].inbox })), })); } const SCENE_ICON = { '家': 'home', '咖啡馆': 'coffee', '旅途中': 'mapPin', '图书馆': 'book', '户外': 'globe', '办公室': 'layers' }; // ============ 左栏 ============ function LeftAxis({ axis, setAxis, allNotes, timeFilter, setTimeFilter, placeFilter, setPlaceFilter, onOpenNote, onExpandMetro, selectedNote, stCity, setStCity, onOpenInbox }) { const [openY, setOpenY] = useStateCol(() => new Set(['2026', '2021'])); const [openC, setOpenC] = useStateCol(() => new Set(['上海', '京都'])); const timeTree = buildTimeTree(allNotes); const placeTree = buildPlaceTree(allNotes); const filterText = axis === 'time' ? (timeFilter.year ? timeFilter.year + (timeFilter.month ? ' · ' + timeFilter.month : '年') : null) : (placeFilter.city ? placeFilter.city + (placeFilter.scene ? ' · ' + placeFilter.scene : '') : null); function toggleY(y) { const s = new Set(openY); s.has(y) ? s.delete(y) : s.add(y); setOpenY(s); } function toggleC(c) { const s = new Set(openC); s.has(c) ? s.delete(c) : s.add(c); setOpenC(s); } return (