// Inbox.jsx — 采集箱:手机采集 → 工作台的接收口 // 与手机端共享同源 localStorage(popor.inbox.v1)。体现「双时间戳」: // · 整理于(capturedAt)= 当下用手机收下这件事的行为与心情 // · 原件(originAt) = 内容自己的过去(写在纸上 / 语音口述 / 聊天日期) const { useState: useStateIB, useEffect: useEffectIB } = React; const INBOX_KEY = 'popor.inbox.v1'; const INBOX_DONE_KEY = 'popor.inbox.done.v1'; const NOTES_KEY = 'popor.notes.v1'; // 整理进本子后写回的笔记(刷新不丢) function ibRead() { try { return JSON.parse(localStorage.getItem(INBOX_KEY) || '[]'); } catch (e) { return []; } } function ibDone() { try { return JSON.parse(localStorage.getItem(INBOX_DONE_KEY) || '[]'); } catch (e) { return []; } } function ibMarkDone(id) { const d = ibDone(); if (d.indexOf(id) < 0) d.push(id); localStorage.setItem(INBOX_DONE_KEY, JSON.stringify(d)); try { fetch((window.POPOR_API_BASE || '') + '/api/inbox/' + encodeURIComponent(id) + '/done', { method: 'POST' }).catch(() => {}); } catch (e) {} } // —— 已整理笔记的持久化(同源 localStorage)—— function savedNotesRead() { try { return JSON.parse(localStorage.getItem(NOTES_KEY) || '[]'); } catch (e) { return []; } } function savedNotesWrite(list) { try { localStorage.setItem(NOTES_KEY, JSON.stringify(list)); } catch (e) {} } function savedNoteUpsert(note) { if (!note || !note.id) return savedNotesRead(); const list = savedNotesRead(); const i = list.findIndex(n => n.id === note.id); if (i >= 0) list[i] = note; else list.unshift(note); savedNotesWrite(list); return list; } // 演示种子:即便没用手机也能看见概念(真实手机采集会叠加在最上) const INBOX_SEED = []; // 生产:数据全部来自后端 let _ibSyncTimer = null; async function syncInboxFromServer() { try { const res = await fetch((window.POPOR_API_BASE || '') + '/api/inbox'); if (!res.ok) return false; const d = await res.json(); const items = Array.isArray(d.items) ? d.items : []; const cur = localStorage.getItem(INBOX_KEY) || '[]'; const next = JSON.stringify(items); if (cur !== next) { localStorage.setItem(INBOX_KEY, next); try { window.dispatchEvent(new StorageEvent('storage', { key: INBOX_KEY })); } catch (e) { window.dispatchEvent(new Event('popor-inbox-sync')); } } return true; } catch (e) { return false; } } function startInboxSync(intervalMs) { if (_ibSyncTimer) return; syncInboxFromServer(); _ibSyncTimer = setInterval(syncInboxFromServer, intervalMs || 5000); } async function organizeInboxApi(it) { const res = await fetch((window.POPOR_API_BASE || '') + '/api/inbox/' + encodeURIComponent(it.id) + '/organize', { method: 'POST' }); if (!res.ok) throw new Error('organize ' + res.status); const d = await res.json(); return (typeof transformNote === 'function') ? transformNote(d.note) : d.note; } function getInboxItems() { const done = ibDone(); const real = ibRead(); const merged = real.concat(INBOX_SEED.filter(s => !real.some(r => r.id === s.id))); return merged.filter(x => done.indexOf(x.id) < 0); } const IB_SRC = { cam: { icon: 'camera', label: '照片' }, voice: { icon: 'mic', label: '语音' }, text: { icon: 'pencil', label: '文字' }, imp: { icon: 'download', label: '导入' }, }; // 采集箱 → 书写台:把一条采集拼成可书写的页(落在「原件」时间,保留「整理于」凭据) function inboxOriginYM(it) { const o = it.originAt; const year = o ? ((o.label.match(/\d{4}/) || ['2026'])[0]) : '2026'; const mraw = o ? ((o.label.match(/\.(\d{1,2})/) || [null, ''])[1]) : ''; return { year: year, month: mraw ? (mraw + '月') : '6月' }; } function inboxToPaperNote(it) { const ym = inboxOriginYM(it); const loc = (it.tags.find(t => t[0] === 'loc') || [, ''])[1]; const nb = (window.NOTEBOOKS || []).find(n => it.tags.some(t => t[1] === n.name || (n.name || '').indexOf(t[1]) >= 0)) || (window.NOTEBOOKS || [])[0] || { id: 'nb', name: '本子', lineIcon: 'book' }; return { id: 'inbox-' + it.id, title: it.title, en: '', date: (it.originAt ? it.originAt.label.replace(/(\d{4})\.(\d{1,2})$/, '$1.$2.01') : it.capturedAt.full), time: it.capturedAt.time || '10:00', year: ym.year, month: ym.month, city: loc || it.capturedAt.place.split(' · ')[0] || '深圳', scene: '采集', notebook: nb.id, cat: it.tags.find(t => t[0] === 'cat') ? it.tags.find(t => t[0] === 'cat')[1] : nb.name, notes: it.tags.filter(t => t[0] === 'note').map(t => t[1]), locs: loc ? [loc] : [], preview: it.text, body: it.text, _fromInbox: true, _src: it.src, _capturedAt: it.capturedAt, _originAt: it.originAt, nbName: nb.name, nbIcon: nb.lineIcon, }; } // 采集箱 → 时空轴:每条采集在轴上关联两个时间 —— 原件年(2013) + 整理于(2026) // 地点同理:原件地点(奈良/北京) + 整理地点(深圳)。让时间轴与地点轴随历史数据一起「拉长」。 function inboxToAxisNotes(items) { const out = []; (items || []).forEach(it => { const locCity = (it.tags.find(t => t[0] === 'loc') || [, null])[1] || null; const capCity = (it.capturedAt.place || '').split(' · ')[0] || '深圳'; // 整理于 2026 · 当下地点的锚点 out.push({ id: it.id + '@cap', year: '2026', month: '6月', city: capCity, scene: '采集', _inbox: true, _capture: true, _ref: it.id }); if (it.originAt) { // 原件有日期:时间轴拉到原件年,地点落在原件地点(若已知) const ym = inboxOriginYM(it); out.push({ id: it.id + '@org', year: ym.year, month: ym.month, city: locCity, scene: '采集', _inbox: true, _capture: false, _ref: it.id }); } else if (locCity) { // 原件无日期但知道地点(如语音口述的北京):只落在地点轴 out.push({ id: it.id + '@place', year: null, city: locCity, scene: '采集', _inbox: true, _capture: false, _ref: it.id }); } }); return out; } // 顶栏按钮(带计数) function InboxButton({ count, onClick }) { return ( ); } function DualStamp({ it }) { return (
整理于 {it.capturedAt.full} · {it.capturedAt.place} {it.originAt ? ( 原件 {it.originAt.label} · {it.originAt.note} ) : it.src === 'cam' ? ( + 补原件时间 ) : null}
); } function InboxPanel({ items, onClose, onOrganize, onDismiss }) { return (
e.stopPropagation()}>
采集箱
手机收进来的 · 整理进本子前,先核对时间地点
{items.length === 0 ? (
采集箱空了。
用手机拍照 / 语音 / 导入,这里会自动出现。
) : (
{items.map(it => { const s = IB_SRC[it.src] || IB_SRC.text; return (
{it.src === 'voice' ? :
} {it.proc && }
{it.title}
{it.text}
{it.tags.map((t, i) => {t[1]})}
); })}
)}
同一个本子 · 手机负责收,工作台负责写。两个时间相互独立又相互关联。
); } Object.assign(window, { getInboxItems, ibMarkDone, inboxToPaperNote, inboxToAxisNotes, InboxButton, InboxPanel, INBOX_KEY, savedNotesRead, savedNoteUpsert, NOTES_KEY, syncInboxFromServer, startInboxSync, organizeInboxApi });