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