FEAT => 本次更新项目为:
基础组件构建完成
This commit is contained in:
384
nkebao/src/components/Upload/FileUpload.module.scss
Normal file
384
nkebao/src/components/Upload/FileUpload.module.scss
Normal file
@@ -0,0 +1,384 @@
|
||||
// 文件上传组件样式
|
||||
.fileUploadContainer {
|
||||
width: 100%;
|
||||
|
||||
.fileUploadButton {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(24, 144, 255, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.uploadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.uploadingIcon {
|
||||
font-size: 32px;
|
||||
color: #1890ff;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.uploadingText {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.uploadProgress {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.uploadIcon {
|
||||
font-size: 48px;
|
||||
color: #1890ff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.uploadText {
|
||||
.uploadTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.uploadSubtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .uploadIcon {
|
||||
transform: scale(1.1);
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fileItem {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.fileItemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.fileIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Excel文件图标样式
|
||||
:global(.anticon-file-excel) {
|
||||
color: #217346;
|
||||
background: rgba(33, 115, 70, 0.1);
|
||||
}
|
||||
|
||||
// Word文件图标样式
|
||||
:global(.anticon-file-word) {
|
||||
color: #2b579a;
|
||||
background: rgba(43, 87, 154, 0.1);
|
||||
}
|
||||
|
||||
// PPT文件图标样式
|
||||
:global(.anticon-file-ppt) {
|
||||
color: #d24726;
|
||||
background: rgba(210, 71, 38, 0.1);
|
||||
}
|
||||
|
||||
// 默认文件图标样式
|
||||
:global(.anticon-file) {
|
||||
color: #666;
|
||||
background: rgba(102, 102, 102, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.fileInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.fileName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.fileActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.previewBtn,
|
||||
.deleteBtn {
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.previewBtn {
|
||||
color: #1890ff;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
color: #ff7875;
|
||||
background: #fff2f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.itemProgress {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.filePreview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 500px;
|
||||
|
||||
iframe {
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.fileUploadContainer {
|
||||
.fileUploadButton {
|
||||
min-height: 100px;
|
||||
|
||||
.uploadContent {
|
||||
padding: 16px;
|
||||
|
||||
.uploadIcon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.uploadText {
|
||||
.uploadTitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uploadSubtitle {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploadingContainer {
|
||||
padding: 16px;
|
||||
|
||||
.uploadingIcon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.uploadingText {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fileItem {
|
||||
padding: 10px;
|
||||
|
||||
.fileItemContent {
|
||||
gap: 10px;
|
||||
|
||||
.fileIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fileInfo {
|
||||
.fileName {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.fileActions {
|
||||
.previewBtn,
|
||||
.deleteBtn {
|
||||
padding: 3px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filePreview {
|
||||
min-height: 400px;
|
||||
|
||||
iframe {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题支持
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.fileUploadContainer {
|
||||
.fileUploadButton {
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1f1f1f 100%);
|
||||
border-color: #434343;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #1a365d 0%, #2d3748 100%);
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.uploadingContainer {
|
||||
.uploadingText {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadContent {
|
||||
.uploadText {
|
||||
.uploadTitle {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.uploadSubtitle {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fileItem {
|
||||
background: #2a2a2a;
|
||||
border-color: #434343;
|
||||
|
||||
&:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.fileItemContent {
|
||||
.fileInfo {
|
||||
.fileName {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fileSize {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.fileActions {
|
||||
.previewBtn,
|
||||
.deleteBtn {
|
||||
&:hover {
|
||||
background: #434343;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filePreview {
|
||||
background: #2a2a2a;
|
||||
|
||||
iframe {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
459
nkebao/src/components/Upload/FileUpload.tsx
Normal file
459
nkebao/src/components/Upload/FileUpload.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
import React, { useState } from "react";
|
||||
import { Upload, message, Progress, Button, Modal } from "antd";
|
||||
import {
|
||||
LoadingOutlined,
|
||||
FileOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
CloudUploadOutlined,
|
||||
FileExcelOutlined,
|
||||
FileWordOutlined,
|
||||
FilePptOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||
import style from "./FileUpload.module.scss";
|
||||
|
||||
interface FileUploadProps {
|
||||
value?: string | string[]; // 支持单个字符串或字符串数组
|
||||
onChange?: (url: string | string[]) => void; // 支持单个字符串或字符串数组
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
maxSize?: number; // 最大文件大小(MB)
|
||||
showPreview?: boolean; // 是否显示预览
|
||||
maxCount?: number; // 最大上传数量,默认为1
|
||||
acceptTypes?: string[]; // 接受的文件类型
|
||||
}
|
||||
|
||||
const FileUpload: React.FC<FileUploadProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
maxSize = 10,
|
||||
showPreview = true,
|
||||
maxCount = 1,
|
||||
acceptTypes = ["excel", "word", "ppt"],
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewUrl, setPreviewUrl] = useState("");
|
||||
|
||||
// 文件类型配置
|
||||
const fileTypeConfig = {
|
||||
excel: {
|
||||
accept: ".xlsx,.xls",
|
||||
mimeTypes: [
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-excel",
|
||||
],
|
||||
icon: FileExcelOutlined,
|
||||
name: "Excel文件",
|
||||
extensions: ["xlsx", "xls"],
|
||||
},
|
||||
word: {
|
||||
accept: ".docx,.doc",
|
||||
mimeTypes: [
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword",
|
||||
],
|
||||
icon: FileWordOutlined,
|
||||
name: "Word文件",
|
||||
extensions: ["docx", "doc"],
|
||||
},
|
||||
ppt: {
|
||||
accept: ".pptx,.ppt",
|
||||
mimeTypes: [
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.ms-powerpoint",
|
||||
],
|
||||
icon: FilePptOutlined,
|
||||
name: "PPT文件",
|
||||
extensions: ["pptx", "ppt"],
|
||||
},
|
||||
};
|
||||
|
||||
// 生成accept字符串
|
||||
const generateAcceptString = () => {
|
||||
return acceptTypes
|
||||
.map(type => fileTypeConfig[type as keyof typeof fileTypeConfig]?.accept)
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
};
|
||||
|
||||
// 获取文件类型信息
|
||||
const getFileTypeInfo = (file: File) => {
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
for (const type of acceptTypes) {
|
||||
const config = fileTypeConfig[type as keyof typeof fileTypeConfig];
|
||||
if (config && config.extensions.includes(extension || "")) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (file: File) => {
|
||||
const typeInfo = getFileTypeInfo(file);
|
||||
return typeInfo ? typeInfo.icon : FileOutlined;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
// 处理单个字符串或字符串数组
|
||||
const urls = Array.isArray(value) ? value : [value];
|
||||
const files: UploadFile[] = urls.map((url, index) => ({
|
||||
uid: `file-${index}`,
|
||||
name: `document-${index + 1}`,
|
||||
status: "done",
|
||||
url: url || "",
|
||||
}));
|
||||
setFileList(files);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
// 文件验证
|
||||
const beforeUpload = (file: File) => {
|
||||
const typeInfo = getFileTypeInfo(file);
|
||||
if (!typeInfo) {
|
||||
const allowedTypes = acceptTypes
|
||||
.map(type => fileTypeConfig[type as keyof typeof fileTypeConfig]?.name)
|
||||
.filter(Boolean)
|
||||
.join("、");
|
||||
message.error(`只能上传${allowedTypes}!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(`文件大小不能超过${maxSize}MB!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 处理文件变化
|
||||
const handleChange: UploadProps["onChange"] = info => {
|
||||
// 更新 fileList,确保所有 URL 都是字符串
|
||||
const updatedFileList = info.fileList.map(file => {
|
||||
let url = "";
|
||||
|
||||
if (file.url) {
|
||||
url = file.url;
|
||||
} else if (file.response) {
|
||||
// 处理响应对象
|
||||
if (typeof file.response === "string") {
|
||||
url = file.response;
|
||||
} else if (file.response.data) {
|
||||
url =
|
||||
typeof file.response.data === "string"
|
||||
? file.response.data
|
||||
: file.response.data.url || "";
|
||||
} else if (file.response.url) {
|
||||
url = file.response.url;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
url: url,
|
||||
};
|
||||
});
|
||||
|
||||
setFileList(updatedFileList);
|
||||
|
||||
// 处理上传状态
|
||||
if (info.file.status === "uploading") {
|
||||
setLoading(true);
|
||||
// 模拟上传进度
|
||||
const progress = Math.min(99, Math.random() * 100);
|
||||
setUploadProgress(progress);
|
||||
} else if (info.file.status === "done") {
|
||||
setLoading(false);
|
||||
setUploadProgress(100);
|
||||
message.success("文件上传成功!");
|
||||
|
||||
// 从响应中获取上传后的URL
|
||||
let uploadedUrl = "";
|
||||
|
||||
if (info.file.response) {
|
||||
if (typeof info.file.response === "string") {
|
||||
uploadedUrl = info.file.response;
|
||||
} else if (info.file.response.data) {
|
||||
uploadedUrl =
|
||||
typeof info.file.response.data === "string"
|
||||
? info.file.response.data
|
||||
: info.file.response.data.url || "";
|
||||
} else if (info.file.response.url) {
|
||||
uploadedUrl = info.file.response.url;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadedUrl) {
|
||||
if (maxCount === 1) {
|
||||
// 单个文件模式
|
||||
onChange?.(uploadedUrl);
|
||||
} else {
|
||||
// 多个文件模式
|
||||
const currentUrls = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const newUrls = [...currentUrls, uploadedUrl];
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
} else if (info.file.status === "error") {
|
||||
setLoading(false);
|
||||
setUploadProgress(0);
|
||||
message.error("上传失败,请重试");
|
||||
} else if (info.file.status === "removed") {
|
||||
if (maxCount === 1) {
|
||||
onChange?.("");
|
||||
} else {
|
||||
// 多个文件模式,移除对应的文件
|
||||
const currentUrls = Array.isArray(value) ? value : value ? [value] : [];
|
||||
const removedIndex = info.fileList.findIndex(
|
||||
f => f.uid === info.file.uid,
|
||||
);
|
||||
if (removedIndex !== -1) {
|
||||
const newUrls = currentUrls.filter(
|
||||
(_, index) => index !== removedIndex,
|
||||
);
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = (file?: UploadFile) => {
|
||||
Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: "确定要删除这个文件吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
if (maxCount === 1) {
|
||||
setFileList([]);
|
||||
onChange?.("");
|
||||
} else if (file) {
|
||||
// 多个文件模式,删除指定文件
|
||||
const currentUrls = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const fileIndex = fileList.findIndex(f => f.uid === file.uid);
|
||||
if (fileIndex !== -1) {
|
||||
const newUrls = currentUrls.filter(
|
||||
(_, index) => index !== fileIndex,
|
||||
);
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
message.success("文件已删除");
|
||||
},
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// 预览文件
|
||||
const handlePreview = (url: string) => {
|
||||
setPreviewUrl(url);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 获取文件大小显示
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 自定义上传按钮
|
||||
const uploadButton = (
|
||||
<div className={style.fileUploadButton}>
|
||||
{loading ? (
|
||||
<div className={style.uploadingContainer}>
|
||||
<div className={style.uploadingIcon}>
|
||||
<LoadingOutlined spin />
|
||||
</div>
|
||||
<div className={style.uploadingText}>上传中...</div>
|
||||
<Progress
|
||||
percent={uploadProgress}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
strokeColor="#1890ff"
|
||||
className={style.uploadProgress}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.uploadContent}>
|
||||
<div className={style.uploadIcon}>
|
||||
<CloudUploadOutlined />
|
||||
</div>
|
||||
<div className={style.uploadText}>
|
||||
<div className={style.uploadTitle}>
|
||||
{maxCount === 1
|
||||
? "上传文档"
|
||||
: `上传文档 (${fileList.length}/${maxCount})`}
|
||||
</div>
|
||||
<div className={style.uploadSubtitle}>
|
||||
支持{" "}
|
||||
{acceptTypes
|
||||
.map(
|
||||
type =>
|
||||
fileTypeConfig[type as keyof typeof fileTypeConfig]?.name,
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("、")}
|
||||
,最大 {maxSize}MB
|
||||
{maxCount > 1 && `,最多上传 ${maxCount} 个文件`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 自定义文件列表项
|
||||
const customItemRender = (
|
||||
originNode: React.ReactElement,
|
||||
file: UploadFile,
|
||||
) => {
|
||||
const FileIcon = file.originFileObj
|
||||
? getFileIcon(file.originFileObj)
|
||||
: FileOutlined;
|
||||
|
||||
if (file.status === "uploading") {
|
||||
return (
|
||||
<div className={style.fileItem}>
|
||||
<div className={style.fileItemContent}>
|
||||
<div className={style.fileIcon}>
|
||||
<FileIcon />
|
||||
</div>
|
||||
<div className={style.fileInfo}>
|
||||
<div className={style.fileName}>{file.name}</div>
|
||||
<div className={style.fileSize}>
|
||||
{file.size ? formatFileSize(file.size) : "计算中..."}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.fileActions}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove(file)}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
percent={uploadProgress}
|
||||
size="small"
|
||||
strokeColor="#1890ff"
|
||||
className={style.itemProgress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (file.status === "done") {
|
||||
return (
|
||||
<div className={style.fileItem}>
|
||||
<div className={style.fileItemContent}>
|
||||
<div className={style.fileIcon}>
|
||||
<FileIcon />
|
||||
</div>
|
||||
<div className={style.fileInfo}>
|
||||
<div className={style.fileName}>{file.name}</div>
|
||||
<div className={style.fileSize}>
|
||||
{file.size ? formatFileSize(file.size) : "未知大小"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.fileActions}>
|
||||
{showPreview && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handlePreview(file.url || "")}
|
||||
className={style.previewBtn}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove(file)}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return originNode;
|
||||
};
|
||||
|
||||
const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload";
|
||||
|
||||
return (
|
||||
<div className={`${style.fileUploadContainer} ${className || ""}`}>
|
||||
<Upload
|
||||
name="file"
|
||||
headers={{
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}}
|
||||
action={action}
|
||||
multiple={maxCount > 1}
|
||||
fileList={fileList}
|
||||
accept={generateAcceptString()}
|
||||
listType="text"
|
||||
showUploadList={{
|
||||
showPreviewIcon: false,
|
||||
showRemoveIcon: false,
|
||||
showDownloadIcon: false,
|
||||
}}
|
||||
disabled={disabled || loading}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
maxCount={maxCount}
|
||||
itemRender={customItemRender}
|
||||
>
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
|
||||
{/* 文件预览模态框 */}
|
||||
<Modal
|
||||
title="文件预览"
|
||||
open={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
centered
|
||||
>
|
||||
<div className={style.filePreview}>
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
style={{ width: "100%", height: "500px", border: "none" }}
|
||||
title="文件预览"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUpload;
|
||||
@@ -12,12 +12,13 @@ import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
interface VideoUploadProps {
|
||||
value?: string;
|
||||
onChange?: (url: string) => void;
|
||||
value?: string | string[]; // 支持单个字符串或字符串数组
|
||||
onChange?: (url: string | string[]) => void; // 支持单个字符串或字符串数组
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
maxSize?: number; // 最大文件大小(MB)
|
||||
showPreview?: boolean; // 是否显示预览
|
||||
maxCount?: number; // 最大上传数量,默认为1
|
||||
}
|
||||
|
||||
const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
@@ -27,6 +28,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
className,
|
||||
maxSize = 50,
|
||||
showPreview = true,
|
||||
maxCount = 1,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
@@ -36,13 +38,15 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
const file: UploadFile = {
|
||||
uid: "-1",
|
||||
name: "video",
|
||||
// 处理单个字符串或字符串数组
|
||||
const urls = Array.isArray(value) ? value : [value];
|
||||
const files: UploadFile[] = urls.map((url, index) => ({
|
||||
uid: `file-${index}`,
|
||||
name: `video-${index + 1}`,
|
||||
status: "done",
|
||||
url: value || "",
|
||||
};
|
||||
setFileList([file]);
|
||||
url: url || "",
|
||||
}));
|
||||
setFileList(files);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
@@ -123,27 +127,69 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
}
|
||||
|
||||
if (uploadedUrl) {
|
||||
onChange?.(uploadedUrl);
|
||||
if (maxCount === 1) {
|
||||
// 单个视频模式
|
||||
onChange?.(uploadedUrl);
|
||||
} else {
|
||||
// 多个视频模式
|
||||
const currentUrls = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const newUrls = [...currentUrls, uploadedUrl];
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
} else if (info.file.status === "error") {
|
||||
setLoading(false);
|
||||
setUploadProgress(0);
|
||||
message.error("上传失败,请重试");
|
||||
} else if (info.file.status === "removed") {
|
||||
onChange?.("");
|
||||
if (maxCount === 1) {
|
||||
onChange?.("");
|
||||
} else {
|
||||
// 多个视频模式,移除对应的视频
|
||||
const currentUrls = Array.isArray(value) ? value : value ? [value] : [];
|
||||
const removedIndex = info.fileList.findIndex(
|
||||
f => f.uid === info.file.uid,
|
||||
);
|
||||
if (removedIndex !== -1) {
|
||||
const newUrls = currentUrls.filter(
|
||||
(_, index) => index !== removedIndex,
|
||||
);
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
const handleRemove = () => {
|
||||
const handleRemove = (file?: UploadFile) => {
|
||||
Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: "确定要删除这个视频文件吗?",
|
||||
okText: "确定",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
setFileList([]);
|
||||
onChange?.("");
|
||||
if (maxCount === 1) {
|
||||
setFileList([]);
|
||||
onChange?.("");
|
||||
} else if (file) {
|
||||
// 多个视频模式,删除指定视频
|
||||
const currentUrls = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const fileIndex = fileList.findIndex(f => f.uid === file.uid);
|
||||
if (fileIndex !== -1) {
|
||||
const newUrls = currentUrls.filter(
|
||||
(_, index) => index !== fileIndex,
|
||||
);
|
||||
onChange?.(newUrls);
|
||||
}
|
||||
}
|
||||
message.success("视频已删除");
|
||||
},
|
||||
});
|
||||
@@ -188,9 +234,14 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
<CloudUploadOutlined />
|
||||
</div>
|
||||
<div className={style.uploadText}>
|
||||
<div className={style.uploadTitle}>上传视频</div>
|
||||
<div className={style.uploadTitle}>
|
||||
{maxCount === 1
|
||||
? "上传视频"
|
||||
: `上传视频 (${fileList.length}/${maxCount})`}
|
||||
</div>
|
||||
<div className={style.uploadSubtitle}>
|
||||
支持 MP4、AVI、MOV 等格式,最大 {maxSize}MB
|
||||
{maxCount > 1 && `,最多上传 ${maxCount} 个视频`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +272,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove()}
|
||||
onClick={() => handleRemove(file)}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
@@ -263,7 +314,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove()}
|
||||
onClick={() => handleRemove(file)}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
@@ -285,7 +336,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}}
|
||||
action={action}
|
||||
multiple={false}
|
||||
multiple={maxCount > 1}
|
||||
fileList={fileList}
|
||||
accept="video/*"
|
||||
listType="text"
|
||||
@@ -298,10 +349,10 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
maxCount={1}
|
||||
maxCount={maxCount}
|
||||
itemRender={customItemRender}
|
||||
>
|
||||
{fileList.length >= 1 ? null : uploadButton}
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
|
||||
{/* 视频预览模态框 */}
|
||||
|
||||
@@ -5,6 +5,7 @@ import NavCommon from "@/components/NavCommon";
|
||||
import ImageUpload from "@/components/Upload/ImageUpload";
|
||||
import AvatarUpload from "@/components/Upload/AvatarUpload";
|
||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||
import FileUpload from "@/components/Upload/FileUpload";
|
||||
import styles from "./upload.module.scss";
|
||||
|
||||
// 错误边界组件
|
||||
@@ -61,7 +62,20 @@ const UploadTestPage: React.FC = () => {
|
||||
|
||||
// 视频上传状态
|
||||
const [videoUrl, setVideoUrl] = useState<string>("");
|
||||
const [videoUrls, setVideoUrls] = useState<string[]>([]);
|
||||
const [videoDisabled, setVideoDisabled] = useState(false);
|
||||
const [videoCount, setVideoCount] = useState(1);
|
||||
|
||||
// 文件上传状态
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
const [fileUrls, setFileUrls] = useState<string[]>([]);
|
||||
const [fileDisabled, setFileDisabled] = useState(false);
|
||||
const [fileCount, setFileCount] = useState(1);
|
||||
const [fileTypes, setFileTypes] = useState<string[]>([
|
||||
"excel",
|
||||
"word",
|
||||
"ppt",
|
||||
]);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="上传组件功能测试" />} loading={loading}>
|
||||
@@ -134,28 +148,189 @@ const UploadTestPage: React.FC = () => {
|
||||
<ErrorBoundary>
|
||||
<Card className={styles.testSection}>
|
||||
<h3>视频上传组件测试</h3>
|
||||
<p>支持视频文件上传,最大50MB,支持预览功能</p>
|
||||
<p>支持视频文件上传,最大50MB,支持预览功能,可设置上传数量</p>
|
||||
|
||||
{/* 视频上传控制面板 */}
|
||||
<div className={styles.controlPanel}>
|
||||
<div className={styles.controlItem}>
|
||||
<span>视频上传数量:</span>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setVideoCount(Math.max(1, videoCount - 1))}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<span>{videoCount}</span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setVideoCount(Math.min(10, videoCount + 1))}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VideoUpload
|
||||
value={videoUrl}
|
||||
onChange={setVideoUrl}
|
||||
value={videoCount === 1 ? videoUrl : videoUrls}
|
||||
onChange={url => {
|
||||
if (videoCount === 1) {
|
||||
setVideoUrl(url as string);
|
||||
} else {
|
||||
setVideoUrls(url as string[]);
|
||||
}
|
||||
}}
|
||||
disabled={videoDisabled}
|
||||
maxSize={50}
|
||||
showPreview={true}
|
||||
maxCount={videoCount}
|
||||
/>
|
||||
|
||||
<div className={styles.result}>
|
||||
<h4>当前视频URL:</h4>
|
||||
<div className={styles.urlList}>
|
||||
<div className={styles.urlItem}>
|
||||
{videoUrl ? (
|
||||
<div className={styles.url}>
|
||||
{typeof videoUrl === "string" ? videoUrl : "无效URL"}
|
||||
{videoCount === 1 ? (
|
||||
<div className={styles.urlItem}>
|
||||
{videoUrl ? (
|
||||
<div className={styles.url}>
|
||||
{typeof videoUrl === "string" ? videoUrl : "无效URL"}
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无视频</span>
|
||||
)}
|
||||
</div>
|
||||
) : videoUrls.length > 0 ? (
|
||||
videoUrls.map((url, index) => (
|
||||
<div key={index} className={styles.urlItem}>
|
||||
<span>{index + 1}.</span>
|
||||
<div className={styles.url}>
|
||||
{typeof url === "string" ? url : "无效URL"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无视频</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无视频</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* 文件上传测试 */}
|
||||
<ErrorBoundary>
|
||||
<Card className={styles.testSection}>
|
||||
<h3>文件上传组件测试</h3>
|
||||
<p>支持Excel、Word、PPT格式文件上传,可设置上传数量和文件类型</p>
|
||||
|
||||
{/* 文件上传控制面板 */}
|
||||
<div className={styles.controlPanel}>
|
||||
<div className={styles.controlItem}>
|
||||
<span>文件上传数量:</span>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setFileCount(Math.max(1, fileCount - 1))}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<span>{fileCount}</span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setFileCount(Math.min(10, fileCount + 1))}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className={styles.controlItem}>
|
||||
<span>文件类型:</span>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
color={fileTypes.includes("excel") ? "primary" : "default"}
|
||||
onClick={() => {
|
||||
if (fileTypes.includes("excel")) {
|
||||
setFileTypes(fileTypes.filter(t => t !== "excel"));
|
||||
} else {
|
||||
setFileTypes([...fileTypes, "excel"]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Excel
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color={fileTypes.includes("word") ? "primary" : "default"}
|
||||
onClick={() => {
|
||||
if (fileTypes.includes("word")) {
|
||||
setFileTypes(fileTypes.filter(t => t !== "word"));
|
||||
} else {
|
||||
setFileTypes([...fileTypes, "word"]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Word
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color={fileTypes.includes("ppt") ? "primary" : "default"}
|
||||
onClick={() => {
|
||||
if (fileTypes.includes("ppt")) {
|
||||
setFileTypes(fileTypes.filter(t => t !== "ppt"));
|
||||
} else {
|
||||
setFileTypes([...fileTypes, "ppt"]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
PPT
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileUpload
|
||||
value={fileCount === 1 ? fileUrl : fileUrls}
|
||||
onChange={url => {
|
||||
if (fileCount === 1) {
|
||||
setFileUrl(url as string);
|
||||
} else {
|
||||
setFileUrls(url as string[]);
|
||||
}
|
||||
}}
|
||||
disabled={fileDisabled}
|
||||
maxSize={10}
|
||||
showPreview={true}
|
||||
maxCount={fileCount}
|
||||
acceptTypes={fileTypes}
|
||||
/>
|
||||
|
||||
<div className={styles.result}>
|
||||
<h4>当前文件URL:</h4>
|
||||
<div className={styles.urlList}>
|
||||
{fileCount === 1 ? (
|
||||
<div className={styles.urlItem}>
|
||||
{fileUrl ? (
|
||||
<div className={styles.url}>
|
||||
{typeof fileUrl === "string" ? fileUrl : "无效URL"}
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无文件</span>
|
||||
)}
|
||||
</div>
|
||||
) : fileUrls.length > 0 ? (
|
||||
fileUrls.map((url, index) => (
|
||||
<div key={index} className={styles.urlItem}>
|
||||
<span>{index + 1}.</span>
|
||||
<div className={styles.url}>
|
||||
{typeof url === "string" ? url : "无效URL"}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无文件</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user