自定义文件上传没有进度条,折腾了好久解决了。
P.S. 图片预览需要自己有域名,懒得折腾了
antd upLoad 自定义上传 不显示进度条 解决办法 参考了这篇文章,发现进度条只有一开始和完成,没有中间进度。 结合 customRequest.tsx官方例子,在onProgress方法中实时更新file的percent属性,终于实现实时进度条了。
0 准备工作
在阿里云oss 建立子ram账号,并给予权限(STS留给服务端授权用,使用方法1可以忽略)
建立好bucket之后配置跨域
1.key和secret写在前端(不安全)
import OSS from "ali-oss";
import React, { useState } from "react";
import { Upload, Spin } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import type { UploadProps } from "antd";
interface UploadFileProps extends UploadProps {
onProgress?: (progress: any, file: any) => void;
}
const UploadFile = (props) => {
// 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
// const changeSrc = props.changeSrc;
const [show, changeShow] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const client = new OSS({
region: "oss-cn-hangzhou", //你的oss服务器所在区域
accessKeyId: "你的ram账号key",
accessKeySecret: "你的ram账号accessKeySecret",
bucket: process.env.NODE_ENV == "production" ? "nest-prod" : "nest-devp", // oss上你的bucket名称
});
const uploadPath = (path, file) => {
return `${path}/${file.name.split(".")[0]}-${file.uid}.${
file.type.split("/")[1]
}`;
};
const OssUpload = async (option) => {
const { file, onSuccess, onProgress, onError } = option;
const folder = "test";
const url = uploadPath(folder, file);
let data = null;
try {
data = await client.multipartUpload(url, file, {
progress: function (p) {
console.log("获取进度条的值==>", (p * 100).toFixed(2));
onProgress({ percent: (p * 100).toFixed(2) }, file);
},
});
onSuccess(
data,
file,
);
} catch (e) {
// message.error(e.message);
onError(e);
}
};
const beforeUpload = (file) => {
return true;
};
const uploadProps:UploadFileProps = {
showUploadList: true,
customRequest: OssUpload,
beforeUpload: beforeUpload,
fileList: fileList,
listType: "picture-card",
onProgress({ percent }, file) {
const index = fileList.findIndex((item) => item.uid === file.uid);
fileList[index].percent = percent;
setFileList([...fileList]);
// console.log("onProgress", `${percent}%`, file);
},
onChange(info: any) {
console.log("onChange😊===》", info);
if (info.file.status !== "uploading") {
console.log(info.file, info.fileList);
}
if (info.file.status === "done") {
// console.log(`${info.file.name} 文件上传成功`);
message.success(`${info.file.name} 文件上传成功`);
} else if (info.file.status === "error") {
info.fileList = info.fileList.filter(
(item) => item.uid !== info.file.uid,
);
message.error(`${info.file.name} 文件上传失败`);
}
setFileList([...info.fileList]);
},
};
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div>
{show === true ? (
<Spin style={{ position: "relative", left: "40px" }} />
) : (
<Upload {...uploadProps}>{uploadButton}</Upload>
)}
<br />
</div>
);
};
export default UploadFile;
2.key和secret接口获取(安全)
2.1 准备工作- 使用STS进行临时授权
- 在阿里云oss 建立oss访问权限的角色
2.2 服务端部分(nest.js):
import { Injectable } from '@nestjs/common';
import { CreateUploadDto } from './dto/create-upload.dto';
import { UpdateUploadDto } from './dto/update-upload.dto';
import { STS } from 'ali-oss';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from '../enum/config.enum';
import * as CryptoJS from 'crypto-js';
@Injectable()
export class UploadService {
private readonly client: STS;
constructor(private configService: ConfigService) {
this.client = new STS({
accessKeyId: this.configService.get(ConfigEnum.OSS_KEY),
accessKeySecret: this.configService.get(ConfigEnum.OSS_SECRET),
});
}
async findAll() {
// roleArn填写角色ARN。
// policy填写自定义权限策略。
// expiration用于设置临时访问凭证有效时间单位为秒,最小值为900,最大值以当前角色设定的最大会话时间为准。
// sessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为SessionTest。
const result = await this.client.assumeRole(
'这里填写ARN,就是上面图标箭头指出的部分',
'{"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}',
3600,
'child-ram-oss',
);
/**
* 使用CryptoJS对AccessKeySecret进行加密,key为OSS_KEY
*/
const key = this.configService.get(ConfigEnum.OSS_KEY);
const AccessKeySecret = CryptoJS.AES.encrypt(
result.credentials.AccessKeySecret,
key,
).toString();
return {
AccessKeyId: result.credentials.AccessKeyId,
AccessKeySecret: AccessKeySecret,
SecurityToken: result.credentials.SecurityToken,
Expiration: result.credentials.Expiration,
};
}
2.3 前端部分
与1部分的区别在于accessKeyId,accessKeySecret是从接口获取的,还多了stsToken属性。 为了安全,accessKeySecret之前在服务端进行了加密,这里需要解密。key写在.env文件中
import OSS from "ali-oss";
import React, { useState } from "react";
import { Upload, Spin } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import { GET_STS } from "@/api/upload";
import * as CryptoJS from "crypto-js";
import type { UploadProps } from "antd";
interface UploadFileProps extends UploadProps {
onProgress?: (progress: any, file: any) => void;
}
const UploadFile = (props) => {
// 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
// const changeSrc = props.changeSrc;
const [show, changeShow] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const uploadPath = (path, file) => {
return `${path}/${file.name.split(".")[0]}-${file.uid}.${
file.type.split("/")[1]
}`;
};
const OssUpload = async (option) => {
/**
* 获取accessKeyId,accessKeySecret等信息
*/
const STS = await GET_STS();
/**
* 解密AccessKeySecret
*/
const decryptedSecret = await CryptoJS.AES.decrypt(
STS.data?.AccessKeySecret,
import.meta.env.VITE_OSS_CRYPTO_KEY,
);
const AccessKeySecret = decryptedSecret.toString(CryptoJS.enc.Utf8);
const client = new OSS({
region: "oss-cn-hangzhou",
accessKeyId: STS.data?.AccessKeyId,
accessKeySecret: AccessKeySecret,
stsToken: STS.data?.SecurityToken,
bucket: process.env.NODE_ENV == "production" ? "nest-prod" : "nest-devp", // oss上你的bucket名称
});
const { file, onSuccess, onProgress, onError } = option;
const folder = "test";
const url = uploadPath(folder, file);
let data = null;
try {
data = await client.multipartUpload(url, file, {
progress: function (p) {
console.log("获取进度条的值==>", (p * 100).toFixed(2));
onProgress({ percent: (p * 100).toFixed(2) }, file);
},
});
onSuccess(
data,
file,
);
} catch (e) {
// message.error(e.message);
onError(e);
}
};
const beforeUpload = (file) => {
return true;
};
const uploadProps:UploadFileProps = {
showUploadList: true,
customRequest: OssUpload,
beforeUpload: beforeUpload,
fileList: fileList,
listType: "picture-card",
onProgress({ percent }, file) {
const index = fileList.findIndex((item) => item.uid === file.uid);
fileList[index].percent = percent;
setFileList([...fileList]);
// console.log("onProgress", `${percent}%`, file);
},
onChange(info: any) {
// console.log("onChange😊===》", info);
if (info.file.status !== "uploading") {
console.log(info.file, info.fileList);
}
if (info.file.status === "done") {
// console.log(`${info.file.name} 文件上传成功`);
message.success(`${info.file.name} 文件上传成功`);
} else if (info.file.status === "error") {
info.fileList = info.fileList.filter(
(item) => item.uid !== info.file.uid,
);
message.error(`${info.file.name} 文件上传失败`);
}
setFileList([...info.fileList]);
},
};
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text">Upload</div>
</div>
);
return (
<div>
{show === true ? (
<Spin style={{ position: "relative", left: "40px" }} />
) : (
<Upload {...uploadProps}>{uploadButton}</Upload>
)}
<br />
</div>
);
};
export default UploadFile;
3.进一步优化
3.1 把client和key,secret放到状态管理zustand中
//store/module/oss.ts
import { create } from "@/store/index";
import OSS from "ali-oss";
import { GET_STS } from "@/api/upload";
import type { StsInterface } from "@/api/upload";
import * as CryptoJS from "crypto-js";
export const useOSS = create<{
expires: number;
client: OSS;
STSData: StsInterface;
initOSS: () => Promise<{
client: OSS;
STSData: StsInterface;
}>;
}>()((set, get) => ({
expires: 0,
client: OSS, // 你可能需要提供适当的默认值
STSData: {} as StsInterface, // 你可能需要提供适当的默认值
initOSS: async (): Promise<{
client: OSS;
STSData: StsInterface;
}> => {
/**
* 判断STS是否过期
*/
if (get().expires < new Date().getTime()) {
const STS = await GET_STS();
/**
* 解密AccessKeySecret
*/
const decryptedSecret = await CryptoJS.AES.decrypt(
STS.data?.AccessKeySecret,
import.meta.env.VITE_OSS_CRYPTO_KEY,
);
const AccessKeySecret = decryptedSecret.toString(CryptoJS.enc.Utf8);
const client = new OSS({
region: STS.data?.OSS_REGION, // 你的oss地址
accessKeyId: STS.data?.AccessKeyId, // 你的accessKeyId
accessKeySecret: AccessKeySecret, // 你的accessKeySecret
stsToken: STS.data?.SecurityToken, // 你的stsToken
bucket: STS.data?.OSS_BUCKET, // 你的bucket
timeout: 60000, // 上传超时时间,1分钟
});
const STSData = STS!.data as StsInterface;
/**
* 转换expires时间为时间戳
*/
const expires = new Date(STS.data!.Expiration).getTime();
set({ expires, client, STSData: STSData });
return { client: client, STSData: STSData! };
}
return { client: get().client, STSData: get().STSData };
},
}));
上传组件
import React, { useEffect, useState } from "react";
import { Upload, Spin, Modal, Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import type { UploadProps } from "antd";
import type { RcFile } from "antd/es/upload";
import { useOSS } from "@/store";
interface UploadFileProps extends UploadProps {
onProgress?: (progress: any, file: any) => void;
}
const StoreUploadFile = (props) => {
// 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
// const changeSrc = props.changeSrc;
const [show, changeShow] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState("");
const [previewTitle, setPreviewTitle] = useState("");
const [previewUrl, setPreviewUrl] = useState("");
const oss = useOSS();
useEffect(() => {
oss.initOSS();
}, []);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
const handleCancel = () => setPreviewOpen(false);
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as RcFile);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
setPreviewTitle(
file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1),
);
};
const uploadPath = (path, file) => {
return `${path}/${file.name.split(".")[0]}-${file.uid}.${
file.type.split("/")[1]
}`;
};
const OssUpload = async (option) => {
const { client, STSData } = await oss.initOSS();
const { file, onSuccess, onProgress, onError } = option;
const folder = STSData!.OSS_DIR;
const url = uploadPath(folder, file);
let data = null;
try {
data = await client.multipartUpload(url, file, {
progress: function (p) {
console.log("获取进度条的值==>", (p * 100).toFixed(2));
onProgress({ percent: (p * 100).toFixed(2) }, file);
},
});
onSuccess(data, file);
} catch (e) {
// message.error(e.message);
onError(e);
}
};
const OSS_DOWNLOAD = async (url): Promise<void> => {
const { client } = await oss.initOSS();
try {
const result = await client.signatureUrl(url, {
expires: 3600, // 有效时间 3600 秒
method: "GET",
});
console.log("OSS_DOWNLOAD result:", result);
setPreviewUrl(result);
// return result;
} catch (error) {
console.error("OSS_DOWNLOAD error:", error);
throw error;
}
};
const OSS_DELETE = async (url): Promise<void> => {
const { client } = await oss.initOSS();
try {
const result = await client.delete(url);
console.log("OSS_DELETE result:", result);
if (result.res.status === 204) {
message.success("删除成功");
}
// return result;
} catch (error) {
console.error("OSS_DELETE error:", error);
throw error;
}
};
const beforeUpload = (file) => {
return true;
};
const uploadProps: UploadFileProps = {
showUploadList: true,
customRequest: OssUpload,
beforeUpload: beforeUpload,
fileList: fileList,
listType: "picture-card",
onPreview: handlePreview,
onProgress({ percent }, file) {
const index = fileList.findIndex((item) => item.uid === file.uid);
fileList[index].percent = percent;
setFileList([...fileList]);
// console.log("onProgress", `${percent}%`, file);
},
onChange(info: any) {
console.log("onChange😊===》", info);
if (info.file.status !== "uploading") {
console.log(info.file, info.fileList);
}
if (info.file.status === "done") {
// console.log(`${info.file.name} 文件上传成功`);
message.success(`${info.file.name} 文件上传成功`);
} else if (info.file.status === "error") {
info.fileList = info.fileList.filter(
(item) => item.uid !== info.file.uid,
);
message.error(`${info.file.name} 文件上传失败`);
}
setFileList([...info.fileList]);
},
};
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text">上传</div>
</div>
);
return (
<div>
{show === true ? (
<Spin style={{ position: "relative", left: "40px" }} />
) : (
<Upload {...uploadProps}>{uploadButton}</Upload>
)}
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={handleCancel}
>
<img alt="example" style={{ width: "100%" }} src={previewImage} />
</Modal>
<img
alt="example"
style={{ width: "200px", height: "200px" }}
src={previewUrl}
/>
{/*<span>{previewUrl}</span>*/}
<Button
onClick={() => {
OSS_DOWNLOAD("test/113027753_p0-rc-upload-1704442853470-3.png");
}}
>
下载
</Button>
<Button
onClick={() => {
OSS_DELETE("test/113027753_p0-rc-upload-1704436150951-3.png");
}}
>
删除
</Button>
<br />
</div>
);
};
export default StoreUploadFile;