// app.jsx — 状态、过滤、组装 + Tweaks const { useState: useStateApp, useMemo: useMemoApp, useEffect: useEffectApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "buttonStyle": "seal", "paperTexture": "grid", "cardLayout": "list" }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [axis, setAxis] = useStateApp('time'); const [timeFilter, setTimeFilter] = useStateApp({}); const [placeFilter, setPlaceFilter] = useStateApp({}); const [tagFilter, setTagFilter] = useStateApp('all'); const [notebookFilter, setNotebookFilter] = useStateApp(null); const [stCity, setStCity] = useStateApp(null); // 时空轴城市筛选 const [openNote, setOpenNote] = useStateApp(null); // 详情 const [paperNote, setPaperNote] = useStateApp(null); // 书写视图(新建/编辑都走这里) const [metroOpen, setMetroOpen] = useStateApp(false);// 时空地图覆层 const [contactsOpen, setContactsOpen] = useStateApp(false); // 通讯录 const [skillsOpen, setSkillsOpen] = useStateApp(false); // 技能工具箱 const [shareNote, setShareNote] = useStateApp(null); // 分享面板 const [plansOpen, setPlansOpen] = useStateApp(false); // 订阅档位 const [inboxOpen, setInboxOpen] = useStateApp(false); // 采集箱 const [inboxTick, setInboxTick] = useStateApp(0); // 采集箱刷新 const [extraNotes, setExtraNotes] = useStateApp( () => (typeof savedNotesRead === 'function' ? savedNotesRead() : [])); // 整理进本子的笔记(localStorage 持久化) // 采集箱:手机端经同源 localStorage 收进来的(含演示种子) const inboxItems = useMemoApp(() => (typeof getInboxItems === 'function' ? getInboxItems() : []), [inboxTick]); // 手机端写入时(storage 事件)实时刷新计数;并在挂载后强制重算一次(规避脚本异步就绪时序) useEffectApp(() => { setInboxTick(t => t + 1); const onStore = e => { if (!e || e.key === INBOX_KEY) setInboxTick(t => t + 1); }; const onSync = () => setInboxTick(t => t + 1); window.addEventListener('storage', onStore); window.addEventListener('popor-inbox-sync', onSync); if (typeof startInboxSync === 'function') startInboxSync(5000); return () => { window.removeEventListener('storage', onStore); window.removeEventListener('popor-inbox-sync', onSync); }; }, []); function organizeInbox(it) { setInboxOpen(false); setOpenNote(null); const localFallback = () => { const note = inboxToPaperNote(it); setExtraNotes(e => e.some(x => x.id === note.id) ? e : [note].concat(e)); if (typeof savedNoteUpsert === 'function') savedNoteUpsert(note); setPaperNote(note); }; if (typeof organizeInboxApi === 'function') { organizeInboxApi(it).then(note => { setExtraNotes(e => e.some(x => x.id === note.id) ? e : [note].concat(e)); setPaperNote(note); }).catch(localFallback); } else localFallback(); ibMarkDone(it.id); setInboxTick(t => t + 1); } function dismissInbox(it) { ibMarkDone(it.id); setInboxTick(t => t + 1); } // 书写台关闭时,把编辑后的正文写回(仅持久化已整理的笔记) function persistNote(updated) { if (!updated || !updated.id) return; setExtraNotes(list => { if (!list.some(x => x.id === updated.id)) return list; // 只更新已在本地库里的 const next = list.map(x => x.id === updated.id ? updated : x); if (typeof savedNoteUpsert === 'function') savedNoteUpsert(updated); return next; }); if (/^note_/.test(updated.id)) { try { fetch((window.POPOR_API_BASE || '') + '/api/notes/' + encodeURIComponent(updated.id) + '/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: updated.title, text: updated.body }), }).catch(() => {}); } catch (e) {} } } // 全部标签(主分类 + 内容标签,去重) const workNotes = useMemoApp(() => extraNotes.concat(NOTES), [extraNotes]); const allTags = useMemoApp(() => { const s = []; workNotes.forEach(n => { [n.cat, ...n.notes].forEach(x => { if (x && !s.includes(x)) s.push(x); }); }); return s; }, [workNotes]); // 时空轴的数据 = 已写入笔记 + 采集箱(按原件/整理两个时间落点,时间轴随之拉长) const axisNotes = useMemoApp( () => workNotes.concat(typeof inboxToAxisNotes === 'function' ? inboxToAxisNotes(inboxItems) : []), [workNotes, inboxItems]); // 过滤(交叉) const filtered = useMemoApp(() => workNotes.filter(n => { if (timeFilter.year && n.year !== timeFilter.year) return false; if (timeFilter.month && n.month !== timeFilter.month) return false; if (placeFilter.city && n.city !== placeFilter.city) return false; if (placeFilter.scene && n.scene !== placeFilter.scene) return false; if (tagFilter !== 'all' && n.cat !== tagFilter && !n.notes.includes(tagFilter)) return false; if (notebookFilter && n.notebook !== notebookFilter) return false; return true; }), [workNotes, timeFilter, placeFilter, tagFilter, notebookFilter]); const counts = useMemoApp(() => { const c = {}; workNotes.forEach(n => { c[n.notebook] = (c[n.notebook] || 0) + 1; }); return c; }, [workNotes]); const noteById = id => workNotes.find(n => n.id === id); const current = openNote ? noteById(openNote) : null; // 导入文件 → 直接进书写台(文本 → 加工 → 保存 的入口) function pcPlace() { // 电脑地点:第一次导入询问一次,存本机,以后自动带上(设置里可清 localStorage 重设) let place = localStorage.getItem('popor.pc.place'); if (place === null) { place = prompt('这台电脑所在地点(如:深圳福田)?\n作为客观「录入地点」自动记录(文本里的时间地点另算),可留空:') || ''; localStorage.setItem('popor.pc.place', place); } return place; } async function importFile(file) { try { const { title, body, image, kind, lossy } = await readImportedFile(file); const nb = NOTEBOOKS[0]; setOpenNote(null); // ── 生产:导入即落库(双时间点:原件=文件修改时间,导入=现在)── const API = window.POPOR_API_BASE || ''; const contentDate = file.lastModified ? new Date(file.lastModified).toISOString().slice(0, 10) : ''; const place = pcPlace(); let backendNote = null; try { if (body && body.trim()) { const r = await fetch(API + '/api/notes/text', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: body, content_date: contentDate, import_location: place, manual_category: nb.name }) }); if (r.ok) backendNote = (await r.json()); } else if (image) { const blob = await (await fetch(image)).blob(); const fd = new FormData(); fd.append('file', new File([blob], file.name || 'import.jpg', { type: blob.type })); fd.append('content_date', contentDate); fd.append('import_location', place); fd.append('manual_category', nb.name); const r = await fetch(API + '/api/notes/upload', { method: 'POST', body: fd }); if (r.ok) backendNote = (await r.json()); } } catch (e) { backendNote = null; } if (backendNote && backendNote.note_id) { // 重新拉一次列表,让笔记进入主库与时空轴,然后打开书写台继续编辑 const full = await fetch(API + '/api/notes/' + backendNote.note_id).then(r => r.json()).catch(() => null); const proto = (full && full.note && typeof transformNote === 'function') ? transformNote(full.note) : null; if (proto) { setExtraNotes(e => e.some(x => x.id === proto.id) ? e : [proto].concat(e)); setPaperNote(proto); return; } } // ── 回落:后端不可达时保持原行为(仅本地预览)── setPaperNote({ id: 'import-' + Date.now(), title: title || '导入的内容', en: '', date: NOW.date.replace(/-/g, '.'), time: NOW.time, year: '2026', month: '6月', city: NOW.city || '上海', scene: NOW.scene || '家', gps: '', notebook: nb.id, cat: nb.name, notes: [], locs: [], body: body || '', _blank: !body && !image, _imported: true, _lossy: lossy, _kind: kind, _image: image, nbName: nb.name, nbIcon: nb.lineIcon, }); } catch (e) { alert('导入失败:' + (e.message || e)); } } // 在某个本子下新建一页 → 直接进书写视图(干净的空白页) function newNoteIn(nbId) { const nb = NOTEBOOKS.find(n => n.id === nbId) || NOTEBOOKS[0]; setOpenNote(null); setPaperNote({ id: 'new-' + Date.now(), title: '新的一页', en: '', date: '2026.06.01', time: '11:15', year: '2026', month: '6月', city: '上海', scene: '家', gps: '31.23°N 121.47°E', notebook: nb.id, cat: nb.name, notes: [], locs: [], body: '', _blank: true, nbName: nb.name, nbIcon: nb.lineIcon, }); } return (
setContactsOpen(true)} onOpenSkills={() => setSkillsOpen(true)} onOpenShare={() => setShareNote(current || filtered[0] || NOTES[0])} onOpenInbox={() => setInboxOpen(true)} inboxCount={inboxItems.length} onOpenPlans={() => setPlansOpen(true)} /> {}} />
setOpenNote(id)} onExpandMetro={() => setMetroOpen(true)} onOpenInbox={() => setInboxOpen(true)} selectedNote={openNote} stCity={stCity} setStCity={setStCity} />
setOpenNote(id)} /> {current && ( nb.id === current.notebook)} onBack={() => setOpenNote(null)} onWrite={n => setPaperNote(n)} onShare={n => setShareNote(n)} /> )}
{metroOpen && ( setMetroOpen(false)} onOpenNote={id => { setMetroOpen(false); setOpenNote(id); }} /> )} {paperNote && ( setPaperNote(null)} onPersist={persistNote} defaultTexture={t.paperTexture} onOpenStore={() => { setPaperNote(null); setSkillsOpen(true); }} /> )} {contactsOpen && ( setContactsOpen(false)} onOpenNote={c => { const hit = NOTES.find(n => (n.notes || []).concat([n.title]).some(x => c.note && c.note.indexOf(x) >= 0)) || NOTES.find(n => n.city === c.now); setContactsOpen(false); if (hit) setOpenNote(hit.id); }} /> )} {skillsOpen && setSkillsOpen(false)} />} {inboxOpen && ( setInboxOpen(false)} onOrganize={organizeInbox} onDismiss={dismissInbox} /> )} {shareNote && setShareNote(null)} />} {plansOpen && ( setPlansOpen(false)} onClose={() => setPlansOpen(false)} /> )} {/* —— Tweaks —— */} setTweak('buttonStyle', v)} /> setTweak('cardLayout', v)} /> setTweak('paperTexture', v)} />
); } // 启动:先从后端拉取真实数据,注入全局后再渲染(失败自动回落示例数据) function bootApp() { const root = ReactDOM.createRoot(document.getElementById('root')); const ready = (typeof loadNotes === 'function') ? loadNotes() : Promise.resolve({ notes: window.NOTES || [], notebooks: window.NOTEBOOKS }); ready.then(({ notes, notebooks }) => { window.NOTES = notes; if (notebooks) window.NOTEBOOKS = notebooks; root.render(); }); } bootApp();