// 图片素材库 (/image-library) // - 左:笔记列表(按入库时间倒序,封面 + N 张 + 来源链接) // - 右:选中笔记后大图 grid,可逐张删除 const ImageLibraryPage = () => { const [notes, setNotes] = React.useState([]); const [stats, setStats] = React.useState(null); const [activeId, setActiveId] = React.useState(null); const [detail, setDetail] = React.useState(null); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(""); const [busyId, setBusyId] = React.useState(""); const [batchBusy, setBatchBusy] = React.useState(false); const [batchMsg, setBatchMsg] = React.useState(""); const batchInputRef = React.useRef(null); const load = async () => { setLoading(true); setErr(""); try { const r = await API.imageLibrary.list(); setNotes(r.items || []); setStats(r.stats || null); if (!activeId && (r.items || []).length) setActiveId(r.items[0].note_id); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }; const loadDetail = async (noteId) => { if (!noteId) { setDetail(null); return; } try { setDetail(await API.imageLibrary.get(noteId)); } catch (e) { setDetail(null); setErr(String(e.message || e)); } }; React.useEffect(() => { load(); }, []); React.useEffect(() => { loadDetail(activeId); }, [activeId]); const onBatchPick = async (e) => { const files = Array.from(e.target.files || []); e.target.value = ""; if (!files.length) return; setBatchBusy(true); setBatchMsg(`上传中:0 / ${files.length}`); try { const fd = new FormData(); for (const f of files) fd.append("files", f); // 用浏览器本地时间作 fallback 标题(后端兜底了,不传也行) fd.append("title", `手动上传 · ${new Date().toLocaleString("zh-CN")}`); const r = await API.imageLibrary.uploadImages(fd); const ok = r.ok_count || (r.uploaded || []).length; const fail = r.fail_count || (r.skipped || []).length; if (ok) { setBatchMsg(`✓ 新建笔记 ${r.note_id?.slice(0,8) || ""}… · 上传 ${ok} 张 · 跳过 ${fail}`); await load(); if (r.note_id) setActiveId(r.note_id); } else { setBatchMsg(`✗ 全部失败 · ${(r.skipped?.[0]?.reason) || "未知错误"}`); } } catch (err2) { setBatchMsg(`✗ 上传失败:${err2.message || err2}`); } finally { setBatchBusy(false); setTimeout(() => setBatchMsg(""), 6000); } }; const deleteNote = async (note) => { if (busyId) return; if (!window.confirm(`删除整条笔记「${note.title}」?\n会移除 ${note.image_count} 张图片,不可恢复。`)) return; setBusyId(note.note_id); setErr(""); try { await API.imageLibrary.deleteNote(note.note_id); if (activeId === note.note_id) { setActiveId(null); setDetail(null); } await load(); } catch (e) { setErr(String(e.message || e)); } finally { setBusyId(""); } }; const deleteImage = async (name) => { if (!detail || busyId) return; if (!window.confirm(`删除这张图「${name}」?`)) return; setBusyId(`${detail.note_id}/${name}`); setErr(""); try { await API.imageLibrary.deleteImage(detail.note_id, name); await loadDetail(detail.note_id); await load(); } catch (e) { setErr(String(e.message || e)); } finally { setBusyId(""); } }; const activeNote = notes.find(n => n.note_id === activeId); return ( <> {stats?.notes || 0} 笔记 · {stats?.images || 0} 张图 } /> {batchMsg && (
{batchMsg}
)} {err && (
{err}
)}
{/* 左:笔记列表 */}
{notes.length === 0 ? (
{loading ? "加载中…" : ( <> 暂无图文笔记
在拆解爆款页贴小红书图文笔记链接,系统会自动落到这里
)}
) : (
{notes.map(n => (
setActiveId(n.note_id)} style={{ padding: 8, borderRadius: 5, cursor: "pointer", border: `1.5px solid ${activeId === n.note_id ? "var(--cyan)" : "var(--line-1)"}`, background: activeId === n.note_id ? "color-mix(in oklch, var(--cyan) 8%, var(--bg-1))" : "var(--bg-1)", }}>
{n.cover_url && ( { e.currentTarget.style.display = "none"; }}/> )}
{n.title}
{n.image_count} 张 · {n.source}
{(n.saved_at || "").slice(0, 19).replace("T", " ")}
))}
)}
{/* 右:图片网格 */}
{!activeNote ? (
选一条图文笔记看图
) : (
{activeNote.title} {activeNote.link_url && ( {activeNote.link_url} )}
{activeNote.image_count} 张
{detail?.image_urls?.length ? (
{detail.image_urls.map((url, i) => { const name = detail.image_names[i]; const isBusy = busyId === `${detail.note_id}/${name}`; return (
{i + 1}
); })}
) : (
加载中…
)}
)}
); }; Object.assign(window, { ImageLibraryPage });