import SparkMD5 from 'spark-md5'; import path from 'path'; import qs from 'querystring'; import axios from 'axios'; import { merge as Merge, isArray, } from 'lodash'; import Cjfec from '../Cjfec'; const CancelToken = axios.CancelToken; type sliceFunc = (start?: number, end?: number, contentType?: string) => Blob; interface CJFile extends File { mozSlice: sliceFunc; webkitSlice: sliceFunc; } declare var File: { prototype: CJFile; }; export class UploadOptions { public quickUpload: boolean = true; public quickUploadUrl: string = 'api/upload/quick-upload'; public getOssParamsUrl: string = 'api/upload/oss-parameters/batch'; public setOssParams: Function; public md5ChunkSize: number = 1024 * 1024; public onProgress?: (e: any) => void; public getAbort?: (abort: any) => void; [props: string]: any; } export class UploadFileOptions { public getAbort?: (abort: any) => void; } export interface QuickUploadParams { extension: string; md5: string; size: number; } export interface Res { code: number; message: string; data?: any; } export interface FileInfo { fileId: string; url: string; uploaded?: boolean; [props: string]: any; } export const defaultUploadOptions = new UploadOptions(); function checkJson(res: Res) { if (res.code !== 0) { throw new Error(res.message); } return res; } /** * 上传组件帮助工具 * TODO: * 1. md5文件 done. * 2. 秒传|非秒传 done. * 3. 上传 done. * 4. 批量上传 done. */ class Helper { /** * 上传 * @param files * @param options */ public static upload(files: File[] | File, options?: UploadOptions): Promise { const instance = new Helper(options); return Array.isArray(files) ? instance.uploadFiles(files) : instance.uploadFile(files); } /** * 获取实例 * @param options */ public static getInstance(options: UploadOptions = defaultUploadOptions) { return new Helper(options); } /** * 自定义上传 */ public static customRequest(options: UploadOptions = ({} as UploadOptions)) { options = options || ({} as UploadOptions); return (option: any) => { // 进度绑定 const onProgress = (e: any) => { e.percent = e.loaded / e.total * 100; option.onProgress(e); }; const { getAbort, ...rest } = options; let abort: any; const uploadOptions = {onProgress, ...rest, getAbort: (a: any) => { abort = a; getAbort && getAbort(a); }}; // 上传 Helper.upload(option.file, uploadOptions) .then((data) => { option.onSuccess(data); }).catch((e) => { option.onError(e); }); return { abort: () => { abort(); console.log(131, '停止'); }, }; }; } private _options: UploadOptions = defaultUploadOptions; public constructor(options: UploadOptions = defaultUploadOptions) { options = { ...defaultUploadOptions, ...options }; this._options = options; } /** * 文件md5处理 * @param file * @param options */ public md5(file: File, options?: any) { return this._md5(file, { chunkSize: (options && options.chunkSize) || this._options.md5ChunkSize || 1024 * 1024, }); } /** * 快传 * @param quickUpload */ public async quickUpload(file: File) { const quickUploadParams = await this.md5(file); return this._quickUpload(file, quickUploadParams as QuickUploadParams); } /** * 上传 */ public async uploadFiles(files: File[]): Promise { const options = this._options; let uploadResults: FileInfo[] = []; let uploadParams: any[]; if (options.quickUpload) { const s: any[] = []; files.forEach((file) => { s.push(this.quickUpload(file)); }); const quickUploadResult = await Promise.all(s); uploadResults = uploadParams = quickUploadResult.map( (res) => res.data.uploaded ? res.data : res.data.parameters, ); } else { const { data: ossParams } = await this._getOssParams(files.length); console.log(151, '获取参数成功', ossParams); uploadParams = ossParams; } const uploadList: any[] = []; let abort; const cancelToken = new CancelToken(function executor(c) { abort = c; }); options.getAbort && options.getAbort(abort); files.forEach((file, i) => { if (!uploadParams[i].uploaded) { uploadParams[i].cancelToken = cancelToken; uploadList.push(this._uploadFile(file, uploadParams[i])); } }); if (uploadList.length) { const uploadRst = await Promise.all(uploadList); let i = 0; uploadResults = uploadParams.map((rst) => { if (!rst.uploaded) { return uploadRst[i++].data; } return rst; }); } return uploadResults; } /** * 上传单个文件 * @param file */ public async uploadFile(file: File): Promise { const uploadResults = await this.uploadFiles([ file ]); return uploadResults[0]; } /** * md5处理 */ private _md5(file: File, options?: any): Promise<{}> { const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; const chunkSize = (options && options.chunkSize) || 1024 * 1024; // 1M const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const { setOssParams } = this._options; const ossParams = setOssParams && setOssParams(file); return new Promise((resolve, reject) => { const frOnload = (e: any) => { spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { resolve({ fileName: file.name, size: file.size, md5: spark.end(), extension: path.extname(file.name).substr(1), isPublic: true, ...ossParams, }); } }; const frOnerror = function(this: any) { reject(this.error); }; const loadNext = () => { const fileReader = new FileReader(); fileReader.onload = frOnload; fileReader.onerror = frOnerror; const start = currentChunk * chunkSize; const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); }; loadNext(); }); } /** * 单纯快传请求 * @param file * @param quickUploadParams */ private _quickUpload(file: File, quickUploadParams: QuickUploadParams) { let quickUploadUrl = ''; if(/^http/g.test(this._options.quickUploadUrl)) { quickUploadUrl = this._options.quickUploadUrl } else { quickUploadUrl = Cjfec.getInstance().getApi() + '/' + this._options.quickUploadUrl; } return axios.post(quickUploadUrl, quickUploadParams, { withCredentials: true, }) .then(({ data }) => { data.data = Merge({ fileName: file.name }, data.data); return data; }) .then(checkJson); } /** * 单纯上传文件 * @param file * @param params */ private _uploadFile(file: File, params: any) { const fd = new FormData(); // 替换文件名, 保留后缀,解决oss签名错误的问题 const ext = file.name ? file.name.split('.').slice(-1)[0] : 'png'; const fileName = new Date().getTime() + '.' + ext; let uploadData: any = { 'key': params.objectKey, 'OSSAccessKeyId': params.accessId || params.accessKeyId, 'callback': params.callback, 'signature': params.signature, 'success_action_status': 200, 'policy': params.policy, 'x:filename': fileName, }; if (params.data) { uploadData = { 'key': params.objectKey, 'OSSAccessKeyId': params.accessKeyId, 'callback': params.callback, 'signature': params.signature, 'success_action_status': 200, 'policy': params.policy, 'expiration': params.expiration, 'x:filename': fileName, 'x:data': params.data, }; } for (const k of Object.keys(uploadData)) { fd.append(k, uploadData[k]); } fd.append('file', file, fileName); return axios.post(params.uploadUrl, fd, { onUploadProgress: (progressEvent) => { this._options.onProgress && this._options.onProgress(progressEvent); }, // 使用 cancel token 取消请求 cancelToken: params.cancelToken, }) .then(({ data }) => { data.data = Merge({ fileName: file.name }, data.data); return data; }) .then(checkJson); } /** * 获取oss上传参数 * @param count 数量 */ private _getOssParams(count: number = 1) { let url = '' // Cjfec.getInstance().getApi() + '/' + this._options.getOssParamsUrl + '?' + qs.stringify({ count }); if(/^http/g.test(this._options.getOssParamsUrl)){ url = this._options.getOssParamsUrl + '?' + qs.stringify({ count }); } else { url = Cjfec.getInstance().getApi() + '/' + this._options.getOssParamsUrl + '?' + qs.stringify({ count }); } return axios.post(url, null, { withCredentials: true, }) .then((res) => res.data) .then(checkJson) .then((data) => { if (!isArray(data.data)) { data.data = [ data.data ]; } return data; }); } } export default Helper;