FEAT => 本次更新项目为:

存一下
This commit is contained in:
2025-07-30 15:19:15 +08:00
parent 6638e24901
commit 9815134536
15 changed files with 2335 additions and 234 deletions

View 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;

View File

@@ -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 = () => {
- 文件大小超限时会显示错误提示
- 上传失败时会显示错误提示
- 网络错误时会显示错误提示
## 头像上传特性
- **圆形显示**: 头像以圆形方式显示
- **占位符**: 无头像时显示用户图标
- **上传覆盖**: 鼠标悬停显示上传图标
- **删除功能**: 右上角删除按钮
- **加载状态**: 上传时显示加载提示
- **尺寸可调**: 支持自定义头像尺寸

View File

@@ -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;
}
}