FEAT => 本次更新项目为:

基础组件构建完成
This commit is contained in:
超级老白兔
2025-08-04 11:43:08 +08:00
parent f2a10e249f
commit 38f5fcf360
4 changed files with 1099 additions and 30 deletions

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

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

View File

@@ -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}>
MP4AVIMOV {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>
{/* 视频预览模态框 */}

View File

@@ -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>ExcelWordPPT格式文件上传</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>