feat: 本次提交更新内容如下
格式更新一下
This commit is contained in:
75
nkebao/src/components/Upload/README.md
Normal file
75
nkebao/src/components/Upload/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Upload 上传组件
|
||||
|
||||
基于 antd Upload 组件封装,简化了 API 请求和文件验证逻辑。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 自动处理文件上传 API 请求
|
||||
- ✅ 文件类型和大小验证
|
||||
- ✅ 支持编辑和新增场景
|
||||
- ✅ 支持单文件和多文件上传
|
||||
- ✅ 上传状态显示
|
||||
- ✅ 文件删除功能
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 图片上传组件 (UploadComponent)
|
||||
|
||||
```tsx
|
||||
import UploadComponent from '@/components/Upload';
|
||||
|
||||
// 单图片上传
|
||||
<UploadComponent
|
||||
value={imageUrl ? [imageUrl] : []}
|
||||
onChange={(urls) => setImageUrl(urls[0] || "")}
|
||||
count={1}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
// 多图片上传
|
||||
<UploadComponent
|
||||
value={imageUrls}
|
||||
onChange={setImageUrls}
|
||||
count={9}
|
||||
accept="image/*"
|
||||
listType="picture-card"
|
||||
/>
|
||||
```
|
||||
|
||||
### 视频上传组件 (VideoUpload)
|
||||
|
||||
```tsx
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
|
||||
<VideoUpload value={videoUrl} onChange={setVideoUrl} />;
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### UploadComponent
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | -------------- | --------------------------------------- | ---------------- |
|
||||
| value | 文件 URL 数组 | `string[]` | `[]` |
|
||||
| onChange | 文件变化回调 | `(urls: string[]) => void` | - |
|
||||
| count | 最大上传数量 | `number` | `9` |
|
||||
| accept | 接受的文件类型 | `string` | `"image/*"` |
|
||||
| listType | 列表类型 | `"text" \| "picture" \| "picture-card"` | `"picture-card"` |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
|
||||
### VideoUpload
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | ------------ | ----------------------- | ------- |
|
||||
| value | 视频 URL | `string` | `""` |
|
||||
| onChange | 视频变化回调 | `(url: string) => void` | - |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 组件内部使用 `/v1/attachment/upload` 接口进行文件上传
|
||||
2. 图片文件大小限制为 5MB,视频文件大小限制为 50MB
|
||||
3. 支持编辑场景,传入 `value` 时会自动显示已上传的文件
|
||||
4. 文件上传成功后会自动调用 `onChange` 回调
|
||||
144
nkebao/src/components/Upload/VideoUpload.tsx
Normal file
144
nkebao/src/components/Upload/VideoUpload.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from "react";
|
||||
import { Upload, message } from "antd";
|
||||
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||
import request from "@/api/request";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface VideoUploadProps {
|
||||
value?: string;
|
||||
onChange?: (url: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
// 将value转换为fileList格式
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
const file: UploadFile = {
|
||||
uid: "-1",
|
||||
name: "video",
|
||||
status: "done",
|
||||
url: value,
|
||||
};
|
||||
setFileList([file]);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// 文件验证
|
||||
const beforeUpload = (file: File) => {
|
||||
const isVideo = file.type.startsWith("video/");
|
||||
if (!isVideo) {
|
||||
message.error("只能上传视频文件!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLt50M = file.size / 1024 / 1024 < 50;
|
||||
if (!isLt50M) {
|
||||
message.error("视频大小不能超过50MB!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false; // 阻止自动上传
|
||||
};
|
||||
|
||||
// 处理文件变化
|
||||
const handleChange: UploadProps["onChange"] = info => {
|
||||
if (info.file.status === "uploading") {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
if (info.file.status === "done") {
|
||||
setLoading(false);
|
||||
// 更新fileList
|
||||
setFileList([info.file]);
|
||||
// 调用onChange
|
||||
onChange?.(info.file.url || "");
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义上传请求
|
||||
const customRequest: UploadProps["customRequest"] = async ({
|
||||
file,
|
||||
onSuccess,
|
||||
onError,
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file as File);
|
||||
|
||||
const response = await request("/v1/attachment/upload", formData, "POST");
|
||||
|
||||
if (response) {
|
||||
const uploadedUrl =
|
||||
typeof response === "string" ? response : response.url || response;
|
||||
onSuccess?.(uploadedUrl);
|
||||
} else {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("上传失败:", error);
|
||||
onError?.(error as Error);
|
||||
message.error("上传失败,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = () => {
|
||||
setFileList([]);
|
||||
onChange?.("");
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<div className={style["upload-button"]}>
|
||||
{loading ? (
|
||||
<div className={style["uploading"]}>
|
||||
<LoadingOutlined className={style["upload-icon"]} />
|
||||
<div className={style["upload-text"]}>上传中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PlusOutlined className={style["upload-icon"]} />
|
||||
<div className={style["upload-text"]}>上传视频</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${style["upload-container"]} ${className || ""}`}>
|
||||
<Upload
|
||||
name="file"
|
||||
multiple={false}
|
||||
fileList={fileList}
|
||||
accept="video/*"
|
||||
listType="text"
|
||||
showUploadList={true}
|
||||
disabled={disabled || loading}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={customRequest}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
>
|
||||
{fileList.length >= 1 ? null : uploadButton}
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoUpload;
|
||||
75
nkebao/src/components/Upload/index.module.scss
Normal file
75
nkebao/src/components/Upload/index.module.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
background: #f0f8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.upload-icon {
|
||||
color: #1677ff;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖antd默认样式
|
||||
:global {
|
||||
.ant-upload-list-picture-card {
|
||||
.ant-upload-list-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
164
nkebao/src/components/Upload/index.tsx
Normal file
164
nkebao/src/components/Upload/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React, { useState } from "react";
|
||||
import { Upload, message } from "antd";
|
||||
import { LoadingOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||
import request from "@/api/request";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface UploadComponentProps {
|
||||
value?: string[];
|
||||
onChange?: (urls: string[]) => void;
|
||||
count?: number; // 最大上传数量
|
||||
accept?: string; // 文件类型
|
||||
listType?: "text" | "picture" | "picture-card";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const UploadComponent: React.FC<UploadComponentProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
count = 9,
|
||||
accept = "image/*",
|
||||
listType = "picture-card",
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
// 将value转换为fileList格式
|
||||
React.useEffect(() => {
|
||||
if (value && value.length > 0) {
|
||||
const files = value.map((url, index) => ({
|
||||
uid: `-${index}`,
|
||||
name: `file-${index}`,
|
||||
status: "done" as const,
|
||||
url,
|
||||
}));
|
||||
setFileList(files);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// 文件验证
|
||||
const beforeUpload = (file: File) => {
|
||||
const isValidType = file.type.startsWith(accept.replace("*", ""));
|
||||
if (!isValidType) {
|
||||
message.error(`只能上传${accept}格式的文件!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
message.error("文件大小不能超过5MB!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false; // 阻止自动上传
|
||||
};
|
||||
|
||||
// 处理文件变化
|
||||
const handleChange: UploadProps["onChange"] = info => {
|
||||
if (info.file.status === "uploading") {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
if (info.file.status === "done") {
|
||||
setLoading(false);
|
||||
// 更新fileList
|
||||
const newFileList = [...fileList];
|
||||
const fileIndex = newFileList.findIndex(f => f.uid === info.file.uid);
|
||||
if (fileIndex > -1) {
|
||||
newFileList[fileIndex] = info.file;
|
||||
} else {
|
||||
newFileList.push(info.file);
|
||||
}
|
||||
setFileList(newFileList);
|
||||
|
||||
// 调用onChange
|
||||
const urls = newFileList.map(f => f.url).filter(Boolean) as string[];
|
||||
onChange?.(urls);
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义上传请求
|
||||
const customRequest: UploadProps["customRequest"] = async ({
|
||||
file,
|
||||
onSuccess,
|
||||
onError,
|
||||
onProgress,
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file as File);
|
||||
|
||||
const response = await request("/v1/attachment/upload", formData, "POST");
|
||||
|
||||
if (response) {
|
||||
const uploadedUrl =
|
||||
typeof response === "string" ? response : response.url || response;
|
||||
onSuccess?.(uploadedUrl);
|
||||
} else {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("上传失败:", error);
|
||||
onError?.(error as Error);
|
||||
message.error("上传失败,请重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
const newFileList = fileList.filter(f => f.uid !== file.uid);
|
||||
setFileList(newFileList);
|
||||
const urls = newFileList.map(f => f.url).filter(Boolean) as string[];
|
||||
onChange?.(urls);
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<div className={style["upload-button"]}>
|
||||
{loading ? (
|
||||
<div className={style["uploading"]}>
|
||||
<LoadingOutlined className={style["upload-icon"]} />
|
||||
<div className={style["upload-text"]}>上传中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PlusOutlined className={style["upload-icon"]} />
|
||||
<div className={style["upload-text"]}>
|
||||
{count > 1 ? "上传文件" : "上传文件"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${style["upload-container"]} ${className || ""}`}>
|
||||
<Upload
|
||||
name="file"
|
||||
multiple={count > 1}
|
||||
fileList={fileList}
|
||||
accept={accept}
|
||||
listType={listType}
|
||||
showUploadList={true}
|
||||
disabled={disabled || loading}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={customRequest}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
>
|
||||
{fileList.length >= count ? null : uploadButton}
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadComponent;
|
||||
Reference in New Issue
Block a user