430 lines
11 KiB
Markdown
430 lines
11 KiB
Markdown
|
---
|
|||
|
toc: menu
|
|||
|
---
|
|||
|
|
|||
|
# 上传配置
|
|||
|
|
|||
|
编辑器默认实现了上传逻辑
|
|||
|
|
|||
|
我们可以在引擎实例中的 `request.upload` 访问到
|
|||
|
|
|||
|
`request.upload` 内部实用 `XMLHttpRequest` 上传文件,好处是可以获取到上传进度
|
|||
|
|
|||
|
```ts
|
|||
|
engine.request.upload(options: UploaderOptions, files: Array<File>, name?: string)
|
|||
|
// 上传可选项类型
|
|||
|
export type UploaderOptions = {
|
|||
|
// 上传地址
|
|||
|
url: string;
|
|||
|
// 请求类型,默认 json
|
|||
|
type?: string;
|
|||
|
// 内容类型
|
|||
|
contentType?: string;
|
|||
|
// 额外数据
|
|||
|
data?: {};
|
|||
|
// 跨域
|
|||
|
crossOrigin?: boolean;
|
|||
|
// 请求头
|
|||
|
headers?: { [key: string]: string };
|
|||
|
// 上传前,可以做文件大小限制判断
|
|||
|
onBefore?: (file: File) => Promise<boolean | void>;
|
|||
|
// 开始上传
|
|||
|
onReady?: (fileInfo: FileInfo, file: File) => Promise<void>;
|
|||
|
// 上传中
|
|||
|
onUploading?: (file: File, progress: { percent: number }) => void;
|
|||
|
// 上传错误
|
|||
|
onError?: (error: Error, file: File) => void;
|
|||
|
// 上传成功
|
|||
|
onSuccess?: (response: any, file: File) => void;
|
|||
|
};
|
|||
|
// FileInfo 类型
|
|||
|
export type FileInfo = {
|
|||
|
uid: string;
|
|||
|
src: string | ArrayBuffer | null;
|
|||
|
name: string;
|
|||
|
size: number;
|
|||
|
type: string;
|
|||
|
ext: string;
|
|||
|
};
|
|||
|
```
|
|||
|
|
|||
|
除了 upload 外,还有 `getFiles(options?: OpenDialogOptions)` 实用方法,可以弹出本地文件选择器
|
|||
|
|
|||
|
```ts
|
|||
|
export type OpenDialogOptions = {
|
|||
|
event?: MouseEvent;
|
|||
|
accept?: string;
|
|||
|
multiple?: boolean | number;
|
|||
|
};
|
|||
|
```
|
|||
|
|
|||
|
下面的插件都是依赖 `engine.request.upload` 实现上传的
|
|||
|
|
|||
|
我们只需要按照对应插件的说明简单配置后就可以实现上传
|
|||
|
|
|||
|
- ImageUploader
|
|||
|
- FileUploader
|
|||
|
- VideoUploader
|
|||
|
|
|||
|
## 自定义上传
|
|||
|
|
|||
|
### 单个插件上传
|
|||
|
|
|||
|
以 ImageUploader 为例
|
|||
|
|
|||
|
```ts
|
|||
|
import {
|
|||
|
getExtensionName,
|
|||
|
FileInfo,
|
|||
|
File,
|
|||
|
isAndroid,
|
|||
|
isEngine,
|
|||
|
} from '@aomao/engine';
|
|||
|
import { ImageComponent, ImageUploader } from '@aomao/plugin-image';
|
|||
|
import { ImageValue } from 'plugins/image/dist/component';
|
|||
|
// 继承原 ImageUploader 类,重写 execute 方法
|
|||
|
class CustomizeImageUploader extends ImageUploader {
|
|||
|
// 当前上传中的卡片实例
|
|||
|
private imageComponents: Record<string, ImageComponent> = {};
|
|||
|
// 上传前处理图片,获取图片的base64在上传等待中显示在编辑器中
|
|||
|
handleBefore(uid: string, file: File) {
|
|||
|
const { type, name, size } = file;
|
|||
|
// 获取文件后缀名
|
|||
|
const ext = getExtensionName(file);
|
|||
|
// 异步读取文件
|
|||
|
return new Promise<false | { file: File; info: FileInfo }>(
|
|||
|
(resolve, reject) => {
|
|||
|
const fileReader = new FileReader();
|
|||
|
fileReader.addEventListener(
|
|||
|
'load',
|
|||
|
() => {
|
|||
|
resolve({
|
|||
|
file,
|
|||
|
info: {
|
|||
|
// 唯一编号
|
|||
|
uid,
|
|||
|
// Blob
|
|||
|
src: fileReader.result,
|
|||
|
// 文件名称
|
|||
|
name,
|
|||
|
// 文件大小
|
|||
|
size,
|
|||
|
// 文件类型
|
|||
|
type,
|
|||
|
// 文件后缀名
|
|||
|
ext,
|
|||
|
},
|
|||
|
});
|
|||
|
},
|
|||
|
false,
|
|||
|
);
|
|||
|
fileReader.addEventListener('error', () => {
|
|||
|
reject(false);
|
|||
|
});
|
|||
|
fileReader.readAsDataURL(file);
|
|||
|
},
|
|||
|
);
|
|||
|
}
|
|||
|
// 上传前插入编辑器
|
|||
|
onReady(fileInfo: FileInfo) {
|
|||
|
// 如果当前图片的 ImageComponent 实例存在就不处理
|
|||
|
if (!isEngine(this.editor) || !!this.imageComponents[fileInfo.uid])
|
|||
|
return;
|
|||
|
// 插入ImageComponent 卡片
|
|||
|
const component = this.editor.card.insert(ImageComponent.cardName, {
|
|||
|
// 设置状态为上传中
|
|||
|
status: 'uploading',
|
|||
|
// 显示在 handleBefore 中获取的 base64 图片,这样不会导致编辑器区域空白
|
|||
|
src: fileInfo.src,
|
|||
|
}) as ImageComponent;
|
|||
|
// 记录当前上传文件的 卡片实例
|
|||
|
this.imageComponents[fileInfo.uid] = component;
|
|||
|
}
|
|||
|
// 上传中
|
|||
|
onUploading(uid: string, { percent }: { percent: number }) {
|
|||
|
// 获取file 对应的 ImageComponent 实例
|
|||
|
const component = this.imageComponents[uid];
|
|||
|
if (!component) return;
|
|||
|
// 设置当前上传进度百分比
|
|||
|
component.setProgressPercent(percent);
|
|||
|
}
|
|||
|
// 上传成功
|
|||
|
onSuccess(response: any, uid: string) {
|
|||
|
// 获取file 对应的 ImageComponent 实例
|
|||
|
const component = this.imageComponents[uid];
|
|||
|
if (!component) return;
|
|||
|
// 获取上传成功后的图片地址
|
|||
|
let src = '';
|
|||
|
// 处理服务端返回的 response,如果上传出错就更新对应 file 对应的 ImageComponent 实例的状态值
|
|||
|
if (!response.result) {
|
|||
|
// 更新卡片的值
|
|||
|
this.editor.card.update(component.id, {
|
|||
|
status: 'error',
|
|||
|
message:
|
|||
|
response.message ||
|
|||
|
this.editor.language.get('image', 'uploadError'),
|
|||
|
});
|
|||
|
} else {
|
|||
|
// 上传成功
|
|||
|
src = response.data;
|
|||
|
}
|
|||
|
// 设置为file 对应的 ImageComponent 实例的状态值为 done
|
|||
|
const value: ImageValue = {
|
|||
|
status: 'done',
|
|||
|
src,
|
|||
|
};
|
|||
|
// 有获取的上传图片后的url
|
|||
|
if (src) {
|
|||
|
// 调用 ImageUploader 当前实例的方法去加载这个 url 图片,如果加载失败,就设置状态为error并显示无法加载,否则就正常加载图片
|
|||
|
this.loadImage(component.id, value);
|
|||
|
}
|
|||
|
// 删除当前的临时记录
|
|||
|
delete this.imageComponents[uid];
|
|||
|
}
|
|||
|
|
|||
|
// 上传出错
|
|||
|
onError(error: Error, uid: string) {
|
|||
|
const component = this.imageComponents[uid];
|
|||
|
if (!component) return;
|
|||
|
// 更新卡片状态为 error,并显示错误信息
|
|||
|
this.editor.card.update(component.id, {
|
|||
|
status: 'error',
|
|||
|
message:
|
|||
|
error.message ||
|
|||
|
this.editor.language.get('image', 'uploadError'),
|
|||
|
});
|
|||
|
// 删除当前的临时记录
|
|||
|
delete this.imageComponents[uid];
|
|||
|
}
|
|||
|
|
|||
|
async execute(files?: Array<File> | string | MouseEvent) {
|
|||
|
// 是阅读器View就不处理
|
|||
|
if (!isEngine(this.editor)) return;
|
|||
|
// 获取当前传入的可选项值
|
|||
|
const { request, language } = this.editor;
|
|||
|
const { multiple } = this.options.file;
|
|||
|
// 上传大小限制
|
|||
|
const limitSize = this.options.file.limitSize || 5 * 1024 * 1024;
|
|||
|
// 传入的files不是数组获取不是图片地址,那就是 MouseEvent 弹出文件选择器
|
|||
|
if (!Array.isArray(files) && typeof files !== 'string') {
|
|||
|
// 弹出文件选择器,让用户选择文件
|
|||
|
files = await request.getFiles({
|
|||
|
// 用户目标的单击事件
|
|||
|
event: files,
|
|||
|
// 可选取的文件后缀名称。this.extensionNames 是 ImageUploader 插件内默认支持的后缀和可选项传进来的后缀合并后的值
|
|||
|
accept: isAndroid
|
|||
|
? 'image/*'
|
|||
|
: this.extensionNames.length > 0
|
|||
|
? '.' + this.extensionNames.join(',.')
|
|||
|
: '',
|
|||
|
// 最多可选取数量
|
|||
|
multiple,
|
|||
|
});
|
|||
|
}
|
|||
|
// 如果传入的文件地址,那就执行图片地址的上传,insertRemote 如果判断是非本站第三方网站图片地址就会请求api到服务端下载然后服务端存储后再返回新的图片地址
|
|||
|
// 因为非本站第三方网站的图片可能存在跨域或者无法访问的情况,建议进行后端下载处理
|
|||
|
else if (typeof files === 'string') {
|
|||
|
this.insertRemote(files);
|
|||
|
return;
|
|||
|
}
|
|||
|
// 如果没有任何文件就不处理
|
|||
|
if (files.length === 0) return;
|
|||
|
const promiseList = [];
|
|||
|
for (let f = 0; f < files.length; f++) {
|
|||
|
const file = files[f];
|
|||
|
// 当前上传文件唯一标识
|
|||
|
const uid = Date.now() + '-' + f;
|
|||
|
// 判断文件大小
|
|||
|
if (file.size > limitSize) {
|
|||
|
// 显示错误
|
|||
|
this.editor.messageError(
|
|||
|
language
|
|||
|
.get<string>('image', 'uploadLimitError')
|
|||
|
.replace(
|
|||
|
'$size',
|
|||
|
(limitSize / 1024 / 1024).toFixed(0) + 'M',
|
|||
|
),
|
|||
|
);
|
|||
|
return;
|
|||
|
}
|
|||
|
promiseList.push(this.handleBefore(uid, file));
|
|||
|
}
|
|||
|
//全部图片读取完成后再插入编辑器
|
|||
|
Promise.all(promiseList).then((values) => {
|
|||
|
if (values.some((value) => value === false)) {
|
|||
|
this.editor.messageError('read image failed');
|
|||
|
return;
|
|||
|
}
|
|||
|
const files = values as { file: File; info: FileInfo }[];
|
|||
|
files.forEach((v) => {
|
|||
|
// 插入编辑器
|
|||
|
this.onReady(v.info);
|
|||
|
});
|
|||
|
// 处理上传
|
|||
|
this.handleUpload(files);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 处理文件上传
|
|||
|
* @param values
|
|||
|
*/
|
|||
|
handleUpload(values: { file: File; info: FileInfo }[]) {
|
|||
|
const files = values.map((v) => {
|
|||
|
v.file.uid = v.info.uid;
|
|||
|
return v.file;
|
|||
|
});
|
|||
|
// 自定义上传方法
|
|||
|
this.editor.request.upload(
|
|||
|
{
|
|||
|
url: this.options.file.action,
|
|||
|
onUploading: (file, percent) => {
|
|||
|
this.onUploading(file.uid || '', percent);
|
|||
|
},
|
|||
|
onSuccess: (response, file) => {
|
|||
|
this.onSuccess(response, file.uid || '');
|
|||
|
},
|
|||
|
onError: (error, file) => {
|
|||
|
this.onError(error, file.uid || '');
|
|||
|
},
|
|||
|
},
|
|||
|
files,
|
|||
|
);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
export default CustomizeImageUploader;
|
|||
|
```
|
|||
|
|
|||
|
### 全局上传
|
|||
|
|
|||
|
重写编辑器 `engine.request.upload` 方法
|
|||
|
|
|||
|
```ts
|
|||
|
import Engine, {
|
|||
|
EngineInterface,
|
|||
|
FileInfo,
|
|||
|
File,
|
|||
|
getExtensionName,
|
|||
|
UploaderOptions,
|
|||
|
} from '@aomao/engine';
|
|||
|
|
|||
|
export default class {
|
|||
|
// 上传前处理图片,获取文件的Blob在上传等待中显示在编辑器中
|
|||
|
handleBefore(uid: string, file: File) {
|
|||
|
const { type, name, size } = file;
|
|||
|
// 获取文件后缀名
|
|||
|
const ext = getExtensionName(file);
|
|||
|
// 异步读取文件
|
|||
|
return new Promise<false | { file: File; info: FileInfo }>(
|
|||
|
(resolve, reject) => {
|
|||
|
const fileReader = new FileReader();
|
|||
|
fileReader.addEventListener(
|
|||
|
'load',
|
|||
|
() => {
|
|||
|
resolve({
|
|||
|
file,
|
|||
|
info: {
|
|||
|
// 唯一编号
|
|||
|
uid,
|
|||
|
// Blob格式
|
|||
|
src: fileReader.result,
|
|||
|
// 文件名称
|
|||
|
name,
|
|||
|
// 文件大小
|
|||
|
size,
|
|||
|
// 文件类型
|
|||
|
type,
|
|||
|
// 文件后缀名
|
|||
|
ext,
|
|||
|
},
|
|||
|
});
|
|||
|
},
|
|||
|
false,
|
|||
|
);
|
|||
|
fileReader.addEventListener('error', () => {
|
|||
|
reject(false);
|
|||
|
});
|
|||
|
fileReader.readAsDataURL(file);
|
|||
|
},
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
setGlobalUpload(engine: EngineInterface = new Engine('.container')) {
|
|||
|
// 重写编辑器中 upload 方法
|
|||
|
engine.request.upload = async (options, files, name) => {
|
|||
|
const { onBefore, onReady } = options;
|
|||
|
// 如果没有任何文件就不处理
|
|||
|
if (files.length === 0) return;
|
|||
|
const promiseList = [];
|
|||
|
for (let f = 0; f < files.length; f++) {
|
|||
|
const file = files[f];
|
|||
|
// 当前上传文件唯一标识
|
|||
|
const uid = Date.now() + '-' + f;
|
|||
|
file.uid = uid;
|
|||
|
if (onBefore && (await onBefore(file)) === false) return;
|
|||
|
promiseList.push(this.handleBefore(uid, file));
|
|||
|
}
|
|||
|
//全部文件读取完成后再插入编辑器
|
|||
|
Promise.all(promiseList).then(async (values) => {
|
|||
|
if (values.some((value) => value === false)) {
|
|||
|
engine.messageError('read image failed');
|
|||
|
return;
|
|||
|
}
|
|||
|
const files = values as { file: File; info: FileInfo }[];
|
|||
|
Promise.all([
|
|||
|
...files.map(async (v) => {
|
|||
|
return new Promise(async (resolve) => {
|
|||
|
if (onReady) {
|
|||
|
await onReady(v.info, v.file);
|
|||
|
}
|
|||
|
resolve(true);
|
|||
|
});
|
|||
|
}),
|
|||
|
]).then(() => {
|
|||
|
files.forEach(async (file) => {
|
|||
|
// 处理上传
|
|||
|
this.handleUpload(file.file, options, name);
|
|||
|
});
|
|||
|
});
|
|||
|
});
|
|||
|
};
|
|||
|
}
|
|||
|
/**
|
|||
|
* 处理上传
|
|||
|
* @param url 上传地址
|
|||
|
* @param name formData 参数名称
|
|||
|
* @param file 文件
|
|||
|
*/
|
|||
|
handleUpload(file: File, options: UploaderOptions, name: string = 'file') {
|
|||
|
// 表单数据
|
|||
|
const formData = new FormData();
|
|||
|
formData.append(name, file, file.name);
|
|||
|
if (file.data) {
|
|||
|
Object.keys(file.data).forEach((key) => {
|
|||
|
formData.append(key, file.data![key]);
|
|||
|
});
|
|||
|
}
|
|||
|
const {
|
|||
|
// 上传地址
|
|||
|
url,
|
|||
|
// 额外数据
|
|||
|
data,
|
|||
|
// 上传中的进度回调
|
|||
|
onUploading,
|
|||
|
// 上传成功回调
|
|||
|
onSuccess,
|
|||
|
// 上传错误回调
|
|||
|
onError,
|
|||
|
} = options;
|
|||
|
if (data) {
|
|||
|
Object.keys(data).forEach((key) => {
|
|||
|
formData.append(key, data![key]);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// 自定义上传,并调用 onUploading onSuccess onError 回调方法
|
|||
|
}
|
|||
|
}
|
|||
|
```
|