FEAT => 本次更新项目为:
存一下
This commit is contained in:
181
nkebao/src/components/Upload/AvatarUpload.tsx
Normal file
181
nkebao/src/components/Upload/AvatarUpload.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Toast, Dialog } from "antd-mobile";
|
||||
import { UserOutlined, CameraOutlined } from "@ant-design/icons";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface AvatarUploadProps {
|
||||
value?: string;
|
||||
onChange?: (url: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
size?: number; // 头像尺寸
|
||||
}
|
||||
|
||||
const AvatarUpload: React.FC<AvatarUploadProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
size = 100,
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [avatarUrl, setAvatarUrl] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setAvatarUrl(value);
|
||||
}, [value]);
|
||||
|
||||
// 文件验证
|
||||
const beforeUpload = (file: File) => {
|
||||
// 检查文件类型
|
||||
const isValidType = file.type.startsWith("image/");
|
||||
if (!isValidType) {
|
||||
Toast.show("只能上传图片文件!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查文件大小 (5MB)
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
Toast.show("图片大小不能超过5MB!");
|
||||
return null;
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
// 上传函数
|
||||
const upload = async (file: File): Promise<{ url: string }> => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/v1/attachment/upload`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 200) {
|
||||
Toast.show("头像上传成功");
|
||||
return { url: result.data.url || result.data };
|
||||
} else {
|
||||
throw new Error(result.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.show("头像上传失败,请重试");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理头像上传
|
||||
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file || disabled || uploading) return;
|
||||
|
||||
const validatedFile = beforeUpload(file);
|
||||
if (!validatedFile) return;
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const result = await upload(validatedFile);
|
||||
setAvatarUrl(result.url);
|
||||
onChange?.(result.url);
|
||||
} catch (error) {
|
||||
console.error("头像上传失败:", error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除头像
|
||||
const handleDelete = () => {
|
||||
return Dialog.confirm({
|
||||
content: "确定要删除头像吗?",
|
||||
onConfirm: () => {
|
||||
setAvatarUrl("");
|
||||
onChange?.("");
|
||||
Toast.show("头像已删除");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${style.avatarUploadContainer} ${className || ""}`}>
|
||||
<div
|
||||
className={style.avatarWrapper}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt="头像"
|
||||
className={style.avatarImage}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={style.avatarPlaceholder}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<UserOutlined />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传覆盖层 */}
|
||||
<div
|
||||
className={style.avatarUploadOverlay}
|
||||
onClick={() =>
|
||||
!disabled && !uploading && fileInputRef.current?.click()
|
||||
}
|
||||
>
|
||||
{uploading ? (
|
||||
<div className={style.uploadLoading}>上传中...</div>
|
||||
) : (
|
||||
<CameraOutlined />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
{avatarUrl && !disabled && (
|
||||
<div className={style.avatarDeleteBtn} onClick={handleDelete}>
|
||||
×
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 隐藏的文件输入 */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleAvatarChange}
|
||||
disabled={disabled || uploading}
|
||||
/>
|
||||
|
||||
{/* 提示文字 */}
|
||||
<div className={style.avatarTip}>
|
||||
{uploading
|
||||
? "正在上传头像..."
|
||||
: "点击头像可更换,支持JPG、PNG格式,大小不超过5MB"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 创建 ref
|
||||
const fileInputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
export default AvatarUpload;
|
||||
@@ -12,6 +12,21 @@
|
||||
- ✅ 数量限制
|
||||
- ✅ 编辑和新增状态支持
|
||||
- ✅ 响应式设计
|
||||
- ✅ 头像上传专用组件
|
||||
|
||||
## 组件列表
|
||||
|
||||
### 1. UploadComponent (图片上传)
|
||||
|
||||
通用的图片上传组件,支持多张图片上传。
|
||||
|
||||
### 2. AvatarUpload (头像上传)
|
||||
|
||||
专门的头像上传组件,支持圆形头像显示、删除功能。
|
||||
|
||||
### 3. VideoUpload (视频上传)
|
||||
|
||||
视频上传组件,支持视频文件上传和预览。
|
||||
|
||||
## 使用方法
|
||||
|
||||
@@ -35,6 +50,26 @@ const MyComponent = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### 头像上传
|
||||
|
||||
```tsx
|
||||
import React, { useState } from "react";
|
||||
import AvatarUpload from "@/components/Upload/AvatarUpload";
|
||||
|
||||
const AvatarComponent = () => {
|
||||
const [avatar, setAvatar] = useState<string>("");
|
||||
|
||||
return (
|
||||
<AvatarUpload
|
||||
value={avatar}
|
||||
onChange={setAvatar}
|
||||
size={100}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 编辑模式
|
||||
|
||||
```tsx
|
||||
@@ -63,7 +98,7 @@ const EditComponent = () => {
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
### UploadComponent Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | -------------- | -------------------------- | ----------- |
|
||||
@@ -74,11 +109,30 @@ const EditComponent = () => {
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
|
||||
### AvatarUpload Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | ------------ | ----------------------- | ------- |
|
||||
| value | 头像URL | `string` | `""` |
|
||||
| onChange | 头像变化回调 | `(url: string) => void` | - |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
| size | 头像尺寸 | `number` | `100` |
|
||||
|
||||
### VideoUpload Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------- | ------------ | ----------------------- | ------- |
|
||||
| value | 视频URL | `string` | `""` |
|
||||
| onChange | 视频变化回调 | `(url: string) => void` | - |
|
||||
| disabled | 是否禁用 | `boolean` | `false` |
|
||||
| className | 自定义类名 | `string` | - |
|
||||
|
||||
### 事件
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| -------- | ------------------ | -------------------------- |
|
||||
| onChange | 图片列表变化时触发 | `(urls: string[]) => void` |
|
||||
| onChange | 文件列表变化时触发 | `(urls: string[]) => void` |
|
||||
|
||||
## 注意事项
|
||||
|
||||
@@ -88,6 +142,7 @@ const EditComponent = () => {
|
||||
4. **认证**: 自动携带 token 进行认证
|
||||
5. **预览**: 点击图片可预览
|
||||
6. **删除**: 删除图片会有确认提示
|
||||
7. **头像组件**: 支持圆形显示、删除按钮、上传覆盖层
|
||||
|
||||
## 样式定制
|
||||
|
||||
@@ -102,6 +157,13 @@ const EditComponent = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatarUploadContainer {
|
||||
// 头像上传组件样式
|
||||
.avatarWrapper {
|
||||
// 头像容器样式
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
@@ -110,3 +172,12 @@ const EditComponent = () => {
|
||||
- 文件大小超限时会显示错误提示
|
||||
- 上传失败时会显示错误提示
|
||||
- 网络错误时会显示错误提示
|
||||
|
||||
## 头像上传特性
|
||||
|
||||
- **圆形显示**: 头像以圆形方式显示
|
||||
- **占位符**: 无头像时显示用户图标
|
||||
- **上传覆盖**: 鼠标悬停显示上传图标
|
||||
- **删除功能**: 右上角删除按钮
|
||||
- **加载状态**: 上传时显示加载提示
|
||||
- **尺寸可调**: 支持自定义头像尺寸
|
||||
|
||||
@@ -106,3 +106,105 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 头像上传组件样式
|
||||
.avatarUploadContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.avatarWrapper {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
border: 2px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 4px 12px rgba(24, 142, 238, 0.3);
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatarPlaceholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.avatarUploadOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.uploadLoading {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarDeleteBtn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background: #ff7875;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .avatarUploadOverlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarTip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user