From 48c7ae0e9819b97d41d0d8b0d8ba93ff7972e88e Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 4 Nov 2026 17:02:14 +0800 Subject: [PATCH] Show recent canvas edits on the workspace landing page --- .../src/pages/infinite_canvas/index.tsx | 2 +- apps/waterfall-wiki/src/pages/workspace.tsx | 44 ++++++++++++++++--- .../src/styles/workspace.module.css | 19 ++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/apps/waterfall-wiki/src/pages/infinite_canvas/index.tsx b/apps/waterfall-wiki/src/pages/infinite_canvas/index.tsx index 7aa05ec..e60d5a0 100644 --- a/apps/waterfall-wiki/src/pages/infinite_canvas/index.tsx +++ b/apps/waterfall-wiki/src/pages/infinite_canvas/index.tsx @@ -17,7 +17,7 @@ export default function InfiniteCanvas(){ const pickerSequence=useRef(0),agent=useRef(null); const active=doc.cards.find(c=>c.id===selected); useEffect(()=>{try{const raw=localStorage.getItem(key);if(raw){const d=parseCanvasDocument(raw);setDoc(d);setStatus('已恢复本机草稿')}}catch{setStatus('草稿读取失败,可导入 JSON 恢复')}try{const v=JSON.parse(localStorage.getItem(key+'.view')||'null');if(v&&Number.isFinite(v.zoom)&&v.zoom>=.2&&v.zoom<=2&&Number.isFinite(v.x)&&Number.isFinite(v.y)){setZoom(v.zoom);setPan({x:v.x,y:v.y})}}catch{}setReady(true);fetch('/api/account/session').then(r=>r.ok?r.json():null).then(d=>setEmail(d?.user?.email||'')).catch(()=>{});},[]); - useEffect(()=>{if(!ready)return;try{localStorage.setItem(key,JSON.stringify(doc));setStatus('已保存到此浏览器')}catch{setStatus('本机空间不足,请导出项目备份')}},[doc,ready]); + useEffect(()=>{if(!ready)return;try{const serialized=JSON.stringify(doc);const previous=localStorage.getItem(key);if(previous!==serialized){localStorage.setItem(key,serialized);if(previous||doc.cards.length)localStorage.setItem(key+'.editedAt',new Date().toISOString());}setStatus('已保存到此浏览器')}catch{setStatus('本机空间不足,请导出项目备份')}},[doc,ready]); useEffect(()=>{if(ready)try{localStorage.setItem(key+'.view',JSON.stringify({...pan,zoom}))}catch{}},[pan,zoom,ready]); useEffect(()=>{const el=viewport.current;if(!el)return;const block=(e:WheelEvent)=>{if(!(e.target as HTMLElement).closest('textarea,input,select'))e.preventDefault()};el.addEventListener('wheel',block,{passive:false});return()=>el.removeEventListener('wheel',block)},[]); useEffect(()=>{const keys=(e:KeyboardEvent)=>{if(e.key==='Escape'){setMenu(null);setRefMenu('');setEditing('');return}if((e.target as HTMLElement).closest('input,textarea,select,[contenteditable]'))return;if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)redo();else undo()}if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='d'){e.preventDefault();duplicate()}};window.addEventListener('keydown',keys);return()=>window.removeEventListener('keydown',keys)}); diff --git a/apps/waterfall-wiki/src/pages/workspace.tsx b/apps/waterfall-wiki/src/pages/workspace.tsx index b2d5df4..572400d 100644 --- a/apps/waterfall-wiki/src/pages/workspace.tsx +++ b/apps/waterfall-wiki/src/pages/workspace.tsx @@ -18,7 +18,7 @@ const purposes = [ {label: '学习与研究', detail: '梳理资料,记录研究过程。', icon: BookOpen}, ]; type Space = {name: string; purpose: string}; -type Subcanvas = {id: string; name: string; nodes: string[]}; +type Subcanvas = {id: string; name: string; nodes: string[]; editedAt?: string}; const personal = {name: '个人空间', purpose: ''}; export default function Workspace() { @@ -35,7 +35,8 @@ export default function Workspace() { const [notice, setNotice] = useState(''); const [nodes, setNodes] = useState([]); const [subcanvases, setSubcanvases] = useState([]); - const [selectedCanvas, setSelectedCanvas] = useState(''); + const [selectedCanvas, setSelectedCanvas] = useState('history'); + const [draftEditedAt, setDraftEditedAt] = useState(); const [expanded, setExpanded] = useState(true); const [subsetName, setSubsetName] = useState(''); const [subsetNodes, setSubsetNodes] = useState([]); @@ -85,10 +86,11 @@ export default function Workspace() { const subsetKey = 'studio.subcanvases:' + email + ':' + active.name; useEffect(() => { if (!email) return; - setSelectedCanvas(''); + setSelectedCanvas('history'); setQuery(''); try { const draft = localStorage.getItem('studio.infinite-canvas.v1'); + setDraftEditedAt(localStorage.getItem('studio.infinite-canvas.v1.editedAt') || undefined); setNodes(draft ? parseCanvasDocument(draft).cards : []); } catch { setNodes([]); @@ -113,7 +115,7 @@ export default function Workspace() { const name = subsetName.trim(); if (!name || !subsetNodes.length) return setError('请输入名称并选择至少一个节点。'); const id = selectedCanvas || window.crypto?.randomUUID?.() || String(Date.now()); - const subset = {id, name, nodes: subsetNodes}; + const subset = {id, name, nodes: subsetNodes, editedAt: new Date().toISOString()}; const next = [...subcanvases.filter(item => item.id !== id), subset]; try { localStorage.setItem(subsetKey, JSON.stringify(next)); @@ -163,6 +165,21 @@ export default function Workspace() { const visibleNodes = subset ? nodes.filter(node => subset.nodes.includes(node.id)) : nodes; const visibleSubsets = subcanvases.filter(item => item.name.toLowerCase().includes(query.trim().toLowerCase())); const canvasName = '无限画布'; + const showHistory = selectedCanvas === 'history'; + const dates = new Intl.DateTimeFormat('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'}); + const times = new Intl.DateTimeFormat('zh-CN', {hour: '2-digit', minute: '2-digit', hour12: false}); + const entries = [ + ...(nodes.length || draftEditedAt ? [{id: '', name: canvasName, editedAt: draftEditedAt, summary: nodes[0]?.text || nodes[0]?.title || '本机草稿'}] : []), + ...subcanvases.map(item => ({...item, summary: nodes.find(node => item.nodes.includes(node.id))?.text || '子画布'})), + ].filter(item => item.name.toLowerCase().includes(query.trim().toLowerCase())); + entries.sort((a, b) => (b.editedAt ?? '').localeCompare(a.editedAt ?? '')); + const history = new Map(); + for (const entry of entries) { + const day = entry.editedAt && Number.isFinite(Date.parse(entry.editedAt)) ? dates.format(new Date(entry.editedAt)) : '编辑时间未记录'; + const group = history.get(day) ?? []; + group.push(entry); + history.set(day, group); + } return
工作台 · 啡跃