// 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 (
{axis === 'spacetime' ? ( ) : ( {filterText && (
{filterText} axis === 'time' ? setTimeFilter({}) : setPlaceFilter({})}>
)}
{axis === 'time' ? timeTree.map(y => (
toggleY(y.year)} style={{ display: 'inline-grid' }}> setTimeFilter(timeFilter.year === y.year && !timeFilter.month ? {} : { year: y.year })}> {y.year} {y.total > 0 && {y.total}} {y.inbox > 0 && ( { e.stopPropagation(); onOpenInbox && onOpenInbox(); }}> {y.inbox} )}
{y.months.map(m => (
{ const same = timeFilter.year === y.year && timeFilter.month === m.month; setTimeFilter(same ? {} : { year: y.year, month: m.month }); }}> {m.month} {m.count > 0 && {m.count}} {m.inbox > 0 && {m.inbox}}
))}
)) : placeTree.map(c => (
toggleC(c.city)} style={{ display: 'inline-grid' }}> setPlaceFilter(placeFilter.city === c.city && !placeFilter.scene ? {} : { city: c.city })}> {c.city} {c.total > 0 && {c.total}} {c.inbox > 0 && ( { e.stopPropagation(); onOpenInbox && onOpenInbox(); }}> {c.inbox} )}
{c.scenes.map(s => (
{ const same = placeFilter.city === c.city && placeFilter.scene === s.scene; setPlaceFilter(same ? {} : { city: c.city, scene: s.scene }); }}> {s.scene} {s.count}
))}
))}
)}
); } // ============ 标签 ============ function Tag({ kind, children, icon }) { return {icon && }{children}; } // ============ 中栏:笔记列表 ============ function NoteList({ notes, allTags, tagFilter, setTagFilter, selected, onOpen, cardLayout }) { return (
{allTags.map(t => ( ))}
{notes.length} 则笔记{cardLayout === 'stream' ? ' · 时间流' : ''}
{notes.length === 0 ? (
这里还什么都没有。
你的笔记,会随阅读慢慢聚拢。
) : (
{notes.map(n => (
onOpen(n.id)}>
{n.title} {n.en}
{n.date}
{n.preview}
{n.cat} {n.notes.map(t => {t})} {n.locs.map(t => {t})}
))}
)}
); } // ============ 右栏:本子系列 ============ function NotebookRail({ notebooks, counts, active, onPick, iconStyle, onAdd }) { return (
Notebooks本子
{notebooks.map(nb => (
onPick(active === nb.id ? null : nb.id)}>
{counts[nb.id] || 0}
{nb.name}
{nb.desc}
))}
新建本子
); } Object.assign(window, { LeftAxis, NoteList, NotebookRail, Tag, SCENE_ICON });