import { useEffect, useRef, useState } from 'react'; import { regenerateMindSpacePageThumbnail, uploadMindSpacePageThumbnail, } from '../api/client'; import { compressImageForUpload } from '../utils/imageUpload'; import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePreviewFrame'; async function readCompressedImageAsBase64(file: File): Promise<{ base64: string; mimeType: string }> { const compressed = await compressImageForUpload(file, { maxInputBytes: 8 * 1024 * 1024, maxOutputBytes: 900 * 1024, maxDimension: 1200, }); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = String(reader.result ?? ''); const match = result.match(/^data:([^;]+);base64,(.+)$/); if (!match) { reject(new Error('无法读取图片')); return; } resolve({ mimeType: match[1], base64: match[2] }); }; reader.onerror = () => reject(new Error('无法读取图片')); reader.readAsDataURL(compressed); }); } export function MindSpacePagePreviewPanel({ pageId, title, summary, content, reloadKey, onFrameReady, onContentChange, }: { pageId: string; title: string; summary: string; content: string; reloadKey: number; onFrameReady?: (iframe: HTMLIFrameElement | null) => void; onContentChange: (html: string) => void; }) { const [thumbnailFailed, setThumbnailFailed] = useState(false); const [thumbnailVersion, setThumbnailVersion] = useState(reloadKey); const [thumbBusy, setThumbBusy] = useState<'upload' | 'ai' | null>(null); const [thumbNotice, setThumbNotice] = useState(null); const [thumbError, setThumbError] = useState(null); const uploadInputRef = useRef(null); const thumbnailRefreshTokenRef = useRef(0); const latestDraftRef = useRef({ title, summary, content }); const thumbnailUrl = `/api/mindspace/v1/pages/${pageId}/thumbnail?v=${thumbnailVersion}`; useEffect(() => { setThumbnailVersion(reloadKey); }, [reloadKey]); useEffect(() => { setThumbnailFailed(false); }, [thumbnailUrl]); useEffect(() => { latestDraftRef.current = { title, summary, content }; }, [content, summary, title]); useEffect(() => { let cancelled = false; const refreshToken = ++thumbnailRefreshTokenRef.current; const { title: draftTitle, summary: draftSummary, content: draftContent } = latestDraftRef.current; const syncThumbnail = async () => { try { const result = await regenerateMindSpacePageThumbnail(pageId, { title: draftTitle, summary: draftSummary, content: draftContent, }); if (cancelled || thumbnailRefreshTokenRef.current !== refreshToken) return; setThumbnailVersion(result.updatedAt ?? Date.now()); setThumbnailFailed(false); } catch { if (cancelled || thumbnailRefreshTokenRef.current !== refreshToken) return; setThumbnailFailed(false); } }; void syncThumbnail(); return () => { cancelled = true; }; }, [pageId, reloadKey]); const bumpThumbnail = () => { setThumbnailVersion(Date.now()); setThumbnailFailed(false); }; const handleUploadClick = () => { uploadInputRef.current?.click(); }; const handleUploadFile = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; if (!file.type.startsWith('image/')) { setThumbError('请选择图片文件'); return; } setThumbBusy('upload'); setThumbError(null); setThumbNotice(null); try { const { base64, mimeType } = await readCompressedImageAsBase64(file); await uploadMindSpacePageThumbnail(pageId, { imageBase64: base64, mimeType, title, summary, content, }); bumpThumbnail(); setThumbNotice('封面已替换,保存页面后仍会保留'); } catch (err) { setThumbError(err instanceof Error ? err.message : '上传封面失败'); } finally { setThumbBusy(null); } }; const handleAiRegenerate = async () => { setThumbBusy('ai'); setThumbError(null); setThumbNotice(null); try { const result = await regenerateMindSpacePageThumbnail(pageId, { title, summary, content, useAi: true, instruction: '根据当前页面内容与标题,生成更精美、可在信息流中直接展示的中文封面。', }); if (result.content) { onContentChange(result.content); } bumpThumbnail(); setThumbNotice('AI 已重新生成封面'); } catch (err) { setThumbError(err instanceof Error ? err.message : 'AI 封面生成失败'); } finally { setThumbBusy(null); } }; return (
event.stopPropagation()} > 页面预览编辑

微型页面,可上下滑动查看完整效果 · 虚线框:蓝=文字 · 橙=图片 · 紫=链接 · 绿=背景

卡片预览图

保存后在信息流中显示的 3:4 封面

{!thumbnailFailed ? ( {`${title setThumbnailFailed(true)} /> ) : ( 预览图生成失败 )}
void handleUploadFile(event)} />
{thumbNotice ?

{thumbNotice}

: null} {thumbError ?

{thumbError}

: null}
); }