// Overview.jsx — 二维俯瞰网格(纵轴:时间尺 / 地点序 · 横轴:本子) // 由 Claude Design「坐标模型共识」原型重建到生产数据模型。覆层叠加,不改现有流程。 // 列归属用现有 noteInNotebook(含子本子下钻);纵轴时间用 import_ts/date 真实刻度。 const { useState: useStateOv } = React; // 时间值(毫秒):date "2021.10.14" / "2026.6.9"(容许不补零)+ time "16:00" function ovTval(n) { const p = String(n.date || '').split('.').map(x => parseInt(x, 10)); if (!p[0]) return 0; const tm = /^(\d{1,2}):(\d{2})/.exec(n.time || ''); return new Date(p[0], (p[1] || 1) - 1, p[2] || 1, tm ? +tm[1] : 12, tm ? +tm[2] : 0).getTime(); } const OV_GUTTER = 60; // 左侧时间/地点标尺宽 const OV_COLW = 152; // 每个本子列宽 // 一张笔记卡(列内) function OvChip({ note, dim, onOpen, style }) { const color = note._capLanding ? 'var(--flame)' : ((window.CITY_LINE && window.CITY_LINE[note.city]) || 'var(--ink-3)'); return ( ); } function OverviewGrid({ axis, notes, notebooks, timeFilter, placeFilter, notebookFilter, setNotebookFilter, onOpenNote, onClose }) { const [scale, setScale] = useStateOv(1); const rowMode = axis === 'place' ? 'place' : 'time'; // 时间/时空 → 时间尺;地点 → 地点序 const NOW = window.NOW || { date: '2026.06.01', time: '11:15', city: '上海', scene: '家' }; const cols = (notebooks || []).filter(nb => !nb.parent); // 横轴 = 顶级本子 const colIndex = {}; cols.forEach((c, i) => colIndex[c.id] = i); // 每条笔记归到哪个顶级本子列(用现有 noteInNotebook,含子本子下钻) const colCache = new Map(); const colOf = n => { if (colCache.has(n.id)) return colCache.get(n.id); let id = null; for (const c of cols) { if (window.noteInNotebook(n, notebooks, c.id)) { id = c.id; break; } } colCache.set(n.id, id); return id; }; // 行方向收窄(共享过滤器:时间 年/月 / 地点 城/年)。本子方向是横轴,不在此过滤。 const rows = (notes || []).filter(n => { if (rowMode === 'time' && timeFilter.year && n.year !== timeFilter.year) return false; if (rowMode === 'time' && timeFilter.month && n.month !== timeFilter.month) return false; if (rowMode === 'place' && placeFilter.city && n.city !== placeFilter.city) return false; if (rowMode === 'place' && placeFilter.year && n.year !== placeFilter.year) return false; return colOf(n); // 没有本子归属的(极少)不进网格 }); const colHas = {}; rows.forEach(n => { const c = colOf(n); if (c) colHas[c] = (colHas[c] || 0) + 1; }); const totalW = OV_GUTTER + cols.length * OV_COLW; const ctx = rowMode === 'place' ? (placeFilter.city ? '地点 · ' + placeFilter.city + (placeFilter.year ? ' · ' + placeFilter.year : '') : '地点 · 全部') : (timeFilter.year ? '时间 · ' + timeFilter.year + (timeFilter.month ? ' · ' + timeFilter.month : '') : (axis === 'spacetime' ? '时空 · 全部' : '时间 · 全部')); // ---------- 列头(横轴本子,点击=锁定/解锁该列) ---------- const colHeader = (
{rowMode === 'place' ? '地点' : '时间'}
{cols.map(c => { const empty = !colHas[c.id]; const locked = notebookFilter === c.id; return ( ); })}
); // ============ 时间尺:按真实刻度铺(近→远,往下) ============ function renderTime() { const times = rows.map(ovTval).filter(Boolean); const nowMs = ovTval(NOW); const tMax = Math.max(nowMs, ...(times.length ? times : [nowMs])); const tMin = Math.min(nowMs, ...(times.length ? times : [nowMs])); const span = Math.max(864e5, tMax - tMin); const yrs = span / (365.25 * 864e5); const H = Math.max(560, Math.round(yrs * 200 * scale)) + 80; const yOf = ms => 36 + (tMax - ms) / span * (H - 96); const y0 = new Date(tMax).getFullYear(), y1 = new Date(tMin).getFullYear(); const years = []; for (let y = y0; y >= y1; y--) years.push(y); // 每列内防重叠:同列按 y 排序,过近则下推 const placed = {}; cols.forEach(c => placed[c.id] = []); [...rows].sort((a, b) => ovTval(b) - ovTval(a)).forEach(n => { const cid = colOf(n); const arr = placed[cid]; if (!arr) return; let y = yOf(ovTval(n)); if (arr.length && y - arr[arr.length - 1].y < 40) y = arr[arr.length - 1].y + 40; arr.push({ note: n, y }); }); const nowY = yOf(nowMs); return (
{years.map(y => { const yy = yOf(Date.parse(y + '-01-01T00:00')); if (yy < 20 || yy > H - 10) return null; return (
{y}
); })} {cols.map(c => (
))}
现在 · {NOW.city}
{cols.map(c => placed[c.id].map(p => ( )))}
); } // ============ 地点序:城市分组(行),组内按时间等间距排 ============ function renderPlace() { const cityList = [...new Set(rows.map(n => n.city).filter(c => c && c !== '未标注'))] .sort((a, b) => Math.max(...rows.filter(n => n.city === b).map(ovTval)) - Math.max(...rows.filter(n => n.city === a).map(ovTval))); return (
{cityList.map(city => { const cityNotes = rows.filter(n => n.city === city); const color = (window.CITY_LINE && window.CITY_LINE[city]) || 'var(--ink-3)'; return (
{city} {cityNotes.length}
{cols.map(c => { const cell = cityNotes.filter(n => colOf(n) === c.id).sort((a, b) => ovTval(a) - ovTval(b)); return (
{cell.map(n => ( ))}
); })}
); })} {cityList.length === 0 &&
这段还没有地点信息
}
); } return (
{ if (e.target === e.currentTarget) onClose(); }}>
俯瞰 · 时空 × 本子
{ctx} · 纵轴{rowMode === 'place' ? '地点(按先后)' : '时间(按刻度)'} · 横轴本子 · {rows.length} 则
{rowMode === 'time' && (
时间疏密
)}
{colHeader} {rowMode === 'place' ? renderPlace() : renderTime()}
{rowMode === 'place' ? '城市为行、本子为列;同城内按先后等距排 — 点本子列只看那一本,点卡片回到那一刻' : '时间自上而下按真实刻度,本子分列;「现在」线之上是尚未到来的计划 — 点卡片回到那一刻'}
); } Object.assign(window, { OverviewGrid });