跳到主要内容

Upload 面板(上传)

内置的 UploadPanel 能用但默认实现太简陋 —— 它把选中的本地文件直接转成 base64 字符串扔进画布 JSON。这样做的坏处很明显:

  • JSON 体积爆炸(一张 2MB 的图就能让设计稿 JSON 超过 3MB)
  • 用户会话间丢失(刷新就没了)
  • 无法跨设备同步
  • 没有删除、收藏、分类

生产环境里强烈建议你接入自己的对象存储(OSS / S3 / COS / 自建图床)。本页给出两种层次的改造方案。


方案 A:最小改动 —— setUploadFunc

最简单的改造:保留内置 UI,只替换上传逻辑。这样本地文件上传到你服务器后,画布只存短 URL。

import { setUploadFunc } from '@ydesign/react-editor/side-panel/upload-panel';

setUploadFunc(async (file: File) => {
const formData = new FormData();
formData.append('file', file);

const res = await fetch('https://your-api.com/upload', {
method: 'POST',
body: formData,
});
const { url } = await res.json();

// 必须返回可直接使用的 URL
return url;
});

签名:(file: File) => Promise<string> —— 收一个原始 File,返回一个字符串 URL。

这种改法适合:

  • 关心"别用 base64",其他 UX 保持默认
  • 不需要"历史上传记录"、"搜索"、"删除"等能力

方案 B:完整替换 UploadSection

如果你想做更完整的图床体验(显示历史上传列表、搜索、删除、收藏),就要替换整个 Section:

import { useEffect, useState, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { Button, Upload } from 'antd';
import { Plus, Trash } from 'lucide-react';
import {
SectionTab,
DEFAULT_SECTIONS,
SidePanel,
} from '@ydesign/react-editor/side-panel';
import { ImagesGrid } from '@ydesign/react-editor/side-panel/images-grid';
import type { Section } from '@ydesign/react-editor/side-panel';

type Asset = {
id: string;
url: string;
thumbnail: string;
width: number;
height: number;
};

const MyUploadSection: Section = {
name: 'upload', // 👈 用同名 name 覆盖默认
Tab: observer(props => (
<SectionTab name="我的上传" {...props}>
<Plus size={20} />
</SectionTab>
)),
Panel: observer(({ store }) => {
const [assets, setAssets] = useState<Asset[]>([]);
const [loading, setLoading] = useState(false);

// 加载用户历史上传
const loadAssets = async () => {
const res = await fetch('/api/assets').then(r => r.json());
setAssets(res.list);
};

useEffect(() => {
loadAssets();
}, []);

// 上传新图
const handleUpload = async (file: File) => {
setLoading(true);
try {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
}).then(r => r.json());
setAssets(prev => [res, ...prev]);
} finally {
setLoading(false);
}
return false; // 阻止 Antd 默认上传
};

// 删除
const handleDelete = async (id: string) => {
await fetch(`/api/assets/${id}`, { method: 'DELETE' });
setAssets(prev => prev.filter(a => a.id !== id));
};

return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Upload accept="image/*" beforeUpload={handleUpload} showUploadList={false}>
<Button icon={<Plus size={14} />} loading={loading} block>
上传图片
</Button>
</Upload>

<div style={{ flex: 1, marginTop: 12, overflow: 'auto' }}>
<ImagesGrid
images={assets}
isLoading={loading}
getPreview={a => a.thumbnail}
getCredit={a => (
<Trash
size={12}
onClick={e => {
e.stopPropagation();
handleDelete(a.id);
}}
/>
)}
onSelect={(a, pos) => {
store.addElement({
type: 'image',
src: a.url,
left: pos?.x ?? 50,
top: pos?.y ?? 50,
width: a.width,
height: a.height,
});
}}
/>
</div>
</div>
);
}),
};

// 用自定义的 Section 替换默认
const sections = DEFAULT_SECTIONS.map(s =>
s.name === 'upload' ? MyUploadSection : s
);

<SidePanel store={store} sections={sections} />;

要点:

  • 复用 <ImagesGrid /> 展示历史上传
  • 用 Antd UploadbeforeUpload 拦截文件、走自己的上传接口
  • getCredit 在每张图底部渲染删除按钮(记得 stopPropagation

方案 C:同时替换默认上传接口(推荐配合 A 或 B)

@ydesign/react-editor 内部有一个叫 uploadImage 的 API key,可以直接通过 setAPI 覆盖:

import { setAPI, setBaseURL } from '@ydesign/react-editor';

setBaseURL('https://api.your-company.com');

setAPI('uploadImage', () => ({
method: 'POST',
url: 'https://your-api.com/assets/upload',
}));

这样其他涉及"上传图片"的代码路径(比如导出前自动把 base64 转为远程 URL、消除笔产出图片等)也会走你的接口。


三种方案对比

方案改动大小收益
A · setUploadFunc解决 base64 臃肿问题
B · 整段替换 Section⭐⭐⭐完整图床体验(历史 / 删除 / 搜索 / 分类)
C · setAPI('uploadImage', …)统一所有"上传图片"的去向

生产项目建议组合使用 A + C;重度素材管理场景再升级到 B。


注意事项

  1. 跨域:上传到自己的存储后,返回的 URL 要支持 CORS,否则 Fabric 加载图片时会 taint canvas、无法导出。默认 ImagesGrid 已经加了 crossOrigin="anonymous"
  2. 图片尺寸:上传接口最好能返回原图的 width / height,否则 store.addElement 可能要用默认宽高加载后再校正。
  3. 超大图:导出 PDF / 印刷物料时,建议保留原图文件地址;显示用 CDN 缩略图,插入画布时再指向原图。

下一步