From 7621b91f153ad28ff6f4ec23be96c20eafc7fae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 4 Aug 2025 14:11:36 +0800 Subject: [PATCH] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=BB=84=E4=BB=B6=E7=9B=B8=E5=85=B3=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=95=B4=E5=90=88=E4=B8=8A=E4=BC=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E8=87=B3=E4=B8=BB=E5=9B=BE=E4=B8=8A=E4=BC=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=8A=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => AvatarUpload}/index.module.scss | 0 .../index.tsx} | 0 .../index.module.scss} | 0 .../{FileUpload.tsx => FileUpload/index.tsx} | 2 +- .../Upload/{ => ImageUpload}/ImageUpload.tsx | 0 .../Upload/ImageUpload/index.module.scss | 484 +++++++++++++++ .../Upload/MainImgUpload/index.module.scss | 558 ++++++++++++++++++ .../components/Upload/MainImgUpload/index.tsx | 313 ++++++++++ nkebao/src/components/Upload/README.md | 209 ++----- .../Upload/VideoUpload/index.module.scss | 484 +++++++++++++++ .../index.tsx} | 0 .../mobile/content/materials/form/index.tsx | 2 +- nkebao/src/pages/mobile/test/upload.tsx | 82 ++- 13 files changed, 1969 insertions(+), 165 deletions(-) rename nkebao/src/components/Upload/{ => AvatarUpload}/index.module.scss (100%) rename nkebao/src/components/Upload/{AvatarUpload.tsx => AvatarUpload/index.tsx} (100%) rename nkebao/src/components/Upload/{FileUpload.module.scss => FileUpload/index.module.scss} (100%) rename nkebao/src/components/Upload/{FileUpload.tsx => FileUpload/index.tsx} (99%) rename nkebao/src/components/Upload/{ => ImageUpload}/ImageUpload.tsx (100%) create mode 100644 nkebao/src/components/Upload/ImageUpload/index.module.scss create mode 100644 nkebao/src/components/Upload/MainImgUpload/index.module.scss create mode 100644 nkebao/src/components/Upload/MainImgUpload/index.tsx create mode 100644 nkebao/src/components/Upload/VideoUpload/index.module.scss rename nkebao/src/components/Upload/{VideoUpload.tsx => VideoUpload/index.tsx} (100%) diff --git a/nkebao/src/components/Upload/index.module.scss b/nkebao/src/components/Upload/AvatarUpload/index.module.scss similarity index 100% rename from nkebao/src/components/Upload/index.module.scss rename to nkebao/src/components/Upload/AvatarUpload/index.module.scss diff --git a/nkebao/src/components/Upload/AvatarUpload.tsx b/nkebao/src/components/Upload/AvatarUpload/index.tsx similarity index 100% rename from nkebao/src/components/Upload/AvatarUpload.tsx rename to nkebao/src/components/Upload/AvatarUpload/index.tsx diff --git a/nkebao/src/components/Upload/FileUpload.module.scss b/nkebao/src/components/Upload/FileUpload/index.module.scss similarity index 100% rename from nkebao/src/components/Upload/FileUpload.module.scss rename to nkebao/src/components/Upload/FileUpload/index.module.scss diff --git a/nkebao/src/components/Upload/FileUpload.tsx b/nkebao/src/components/Upload/FileUpload/index.tsx similarity index 99% rename from nkebao/src/components/Upload/FileUpload.tsx rename to nkebao/src/components/Upload/FileUpload/index.tsx index a262cf7f9..9edf1c4f2 100644 --- a/nkebao/src/components/Upload/FileUpload.tsx +++ b/nkebao/src/components/Upload/FileUpload/index.tsx @@ -11,7 +11,7 @@ import { FilePptOutlined, } from "@ant-design/icons"; import type { UploadProps, UploadFile } from "antd/es/upload/interface"; -import style from "./FileUpload.module.scss"; +import style from "./index.module.scss"; interface FileUploadProps { value?: string | string[]; // 支持单个字符串或字符串数组 diff --git a/nkebao/src/components/Upload/ImageUpload.tsx b/nkebao/src/components/Upload/ImageUpload/ImageUpload.tsx similarity index 100% rename from nkebao/src/components/Upload/ImageUpload.tsx rename to nkebao/src/components/Upload/ImageUpload/ImageUpload.tsx diff --git a/nkebao/src/components/Upload/ImageUpload/index.module.scss b/nkebao/src/components/Upload/ImageUpload/index.module.scss new file mode 100644 index 000000000..abec0b860 --- /dev/null +++ b/nkebao/src/components/Upload/ImageUpload/index.module.scss @@ -0,0 +1,484 @@ +.uploadContainer { + width: 100%; + + // 自定义上传组件样式 + :global { + .adm-image-uploader { + .adm-image-uploader-upload-button { + width: 100px; + height: 100px; + border: 1px dashed #d9d9d9; + border-radius: 8px; + background: #fafafa; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + + &:hover { + border-color: #1677ff; + background: #f0f8ff; + } + + .adm-image-uploader-upload-button-icon { + font-size: 32px; + color: #999; + } + } + + .adm-image-uploader-item { + width: 100px; + height: 100px; + border-radius: 8px; + overflow: hidden; + position: relative; + + .adm-image-uploader-item-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + .adm-image-uploader-item-delete { + position: absolute; + top: 4px; + right: 4px; + width: 24px; + height: 24px; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 14px; + cursor: pointer; + } + + .adm-image-uploader-item-loading { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + } + } + } + } +} + +// 禁用状态 +.uploadContainer.disabled { + opacity: 0.6; + pointer-events: none; +} + +// 错误状态 +.uploadContainer.error { + :global { + .adm-image-uploader-upload-button { + border-color: #ff4d4f; + background: #fff2f0; + } + } +} + +// 响应式设计 +@media (max-width: 768px) { + .uploadContainer { + :global { + .adm-image-uploader { + .adm-image-uploader-upload-button, + .adm-image-uploader-item { + width: 80px; + height: 80px; + } + + .adm-image-uploader-upload-button-icon { + font-size: 28px; + } + } + } + } +} + +// 头像上传组件样式 +.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; + } +} + +// 视频上传组件样式 +.videoUploadContainer { + width: 100%; + + .videoUploadButton { + 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; + } + } + } + + .videoItem { + 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); + } + + .videoItemContent { + display: flex; + align-items: center; + gap: 12px; + + .videoIcon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 18px; + flex-shrink: 0; + } + + .videoInfo { + flex: 1; + min-width: 0; + + .videoName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .videoSize { + font-size: 12px; + color: #666; + } + } + + .videoActions { + 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; + } + } + + .videoPreview { + display: flex; + justify-content: center; + align-items: center; + background: #000; + border-radius: 8px; + overflow: hidden; + + video { + border-radius: 8px; + } + } +} + +// 动画效果 +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .videoUploadContainer { + .videoUploadButton { + 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; + } + } + } + } + + .videoItem { + background: #2a2a2a; + border-color: #434343; + + &:hover { + border-color: #40a9ff; + } + + .videoItemContent { + .videoInfo { + .videoName { + color: #fff; + } + + .videoSize { + color: #ccc; + } + } + + .videoActions { + .previewBtn, + .deleteBtn { + &:hover { + background: #434343; + } + } + } + } + } + } +} diff --git a/nkebao/src/components/Upload/MainImgUpload/index.module.scss b/nkebao/src/components/Upload/MainImgUpload/index.module.scss new file mode 100644 index 000000000..7b6def67d --- /dev/null +++ b/nkebao/src/components/Upload/MainImgUpload/index.module.scss @@ -0,0 +1,558 @@ +.mainImgUploadContainer { + width: 100%; + display: flex; + flex-direction: column; + + // 覆盖 antd Upload 组件的默认样式 + :global { + .ant-upload { + width: 100%; + } + + .ant-upload-list { + width: 100%; + } + + .ant-upload-list-text { + width: 100%; + } + + .ant-upload-list-text .ant-upload-list-item { + width: 100%; + } + } + + .mainImgUploadButton { + width: 100%; + aspect-ratio: 16 / 9; + min-height: 180px; + min-width: 320px; + 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; + flex: 1; + + &: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; + } + } + + .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; + } + } + } + + .mainImgItem { + 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); + } + + .mainImgItemContent { + display: flex; + align-items: center; + gap: 12px; + + .mainImgIcon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 18px; + flex-shrink: 0; + } + + .mainImgInfo { + flex: 1; + min-width: 0; + + .mainImgName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mainImgSize { + font-size: 12px; + color: #666; + } + } + + .mainImgActions { + 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; + } + } + } + } + + .mainImgPreview { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + min-height: 180px; + border-radius: 8px; + overflow: hidden; + background: #f5f5f5; + + .mainImgImage { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; + } + + .mainImgOverlay { + 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; + opacity: 0; + transition: opacity 0.3s ease; + border-radius: 8px; + + .mainImgActions { + display: flex; + gap: 8px; + + .previewBtn, + .deleteBtn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: rgba(255, 255, 255, 0.9); + color: #666; + cursor: pointer; + transition: all 0.3s ease; + + &:hover { + background: white; + color: #1890ff; + transform: scale(1.1); + } + + .anticon { + font-size: 14px; + } + } + + .deleteBtn:hover { + color: #ff4d4f; + } + } + } + + &:hover .mainImgOverlay { + opacity: 1; + } + } + } +} + +// 禁用状态 +.mainImgUploadContainer.disabled { + opacity: 0.6; + pointer-events: none; +} + +// 错误状态 +.mainImgUploadContainer.error { + .mainImgUploadButton { + border-color: #ff4d4f; + background: #fff2f0; + } +} + +// 动画效果 +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +// 响应式设计 +@media (max-width: 768px) { + .mainImgUploadContainer { + .mainImgUploadButton { + aspect-ratio: 16 / 9; + min-height: 135px; + min-width: 240px; + + .uploadContent { + padding: 16px; + + .uploadIcon { + font-size: 36px; + } + + .uploadText { + .uploadTitle { + font-size: 14px; + } + + .uploadSubtitle { + font-size: 11px; + } + } + } + + .uploadingContainer { + padding: 16px; + + .uploadingIcon { + font-size: 28px; + } + + .uploadingText { + font-size: 12px; + } + } + } + + .mainImgItem { + .mainImgItemContent { + padding: 8px; + + .mainImgIcon { + width: 32px; + height: 32px; + font-size: 16px; + } + + .mainImgInfo { + .mainImgName { + font-size: 12px; + } + + .mainImgSize { + font-size: 11px; + } + } + + .mainImgActions { + .previewBtn, + .deleteBtn { + padding: 3px 6px; + font-size: 12px; + } + } + } + + .mainImgPreview { + aspect-ratio: 16 / 9; + min-height: 135px; + + .mainImgOverlay { + .mainImgActions { + .previewBtn, + .deleteBtn { + width: 28px; + height: 28px; + + .anticon { + font-size: 12px; + } + } + } + } + } + } + } +} + +@media (max-width: 480px) { + .mainImgUploadContainer { + .mainImgUploadButton { + aspect-ratio: 16 / 9; + min-height: 90px; + min-width: 160px; + + .uploadContent { + padding: 12px; + gap: 8px; + + .uploadIcon { + font-size: 28px; + } + + .uploadText { + .uploadTitle { + font-size: 12px; + } + + .uploadSubtitle { + font-size: 10px; + } + } + } + + .uploadingContainer { + padding: 12px; + gap: 8px; + + .uploadingIcon { + font-size: 24px; + } + + .uploadingText { + font-size: 11px; + } + } + } + + .mainImgItem { + .mainImgItemContent { + padding: 6px; + gap: 8px; + + .mainImgIcon { + width: 28px; + height: 28px; + font-size: 14px; + } + + .mainImgInfo { + .mainImgName { + font-size: 11px; + } + + .mainImgSize { + font-size: 10px; + } + } + + .mainImgActions { + .previewBtn, + .deleteBtn { + padding: 2px 4px; + font-size: 11px; + } + } + } + + .mainImgPreview { + aspect-ratio: 16 / 9; + min-height: 90px; + + .mainImgOverlay { + .mainImgActions { + .previewBtn, + .deleteBtn { + width: 24px; + height: 24px; + + .anticon { + font-size: 11px; + } + } + } + } + } + } + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .mainImgUploadContainer { + .mainImgUploadButton { + 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; + } + } + } + } + + .mainImgItem { + background: #2a2a2a; + border-color: #434343; + + &:hover { + border-color: #40a9ff; + } + + .mainImgItemContent { + .mainImgInfo { + .mainImgName { + color: #fff; + } + + .mainImgSize { + color: #ccc; + } + } + + .mainImgActions { + .previewBtn, + .deleteBtn { + &:hover { + background: #434343; + } + } + } + } + + .mainImgPreview { + background: #1f1f1f; + + .mainImgOverlay { + .mainImgActions { + .previewBtn, + .deleteBtn { + background: rgba(255, 255, 255, 0.1); + color: #ccc; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } + } + } + } + } + } + } +} diff --git a/nkebao/src/components/Upload/MainImgUpload/index.tsx b/nkebao/src/components/Upload/MainImgUpload/index.tsx new file mode 100644 index 000000000..2d26cd84d --- /dev/null +++ b/nkebao/src/components/Upload/MainImgUpload/index.tsx @@ -0,0 +1,313 @@ +import React, { useState, useEffect } from "react"; +import { Upload, message, Button } from "antd"; +import { + LoadingOutlined, + PictureOutlined, + DeleteOutlined, + EyeOutlined, + CloudUploadOutlined, +} from "@ant-design/icons"; +import type { UploadProps, UploadFile } from "antd/es/upload/interface"; +import style from "./index.module.scss"; + +interface MainImgUploadProps { + value?: string; + onChange?: (url: string) => void; + disabled?: boolean; + className?: string; + maxSize?: number; // 最大文件大小(MB) + showPreview?: boolean; // 是否显示预览 +} + +const MainImgUpload: React.FC = ({ + value = "", + onChange, + disabled = false, + className, + maxSize = 5, + showPreview = true, +}) => { + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + + useEffect(() => { + if (value) { + const files: UploadFile[] = [ + { + uid: "main-img", + name: "main-image", + status: "done", + url: value, + }, + ]; + setFileList(files); + } else { + setFileList([]); + } + }, [value]); + + // 文件验证 + const beforeUpload = (file: File) => { + const isImage = file.type.startsWith("image/"); + if (!isImage) { + message.error("只能上传图片文件!"); + 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); + } else if (info.file.status === "done") { + setLoading(false); + 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) { + onChange?.(uploadedUrl); + } + } else if (info.file.status === "error") { + setLoading(false); + message.error("上传失败,请重试"); + } else if (info.file.status === "removed") { + onChange?.(""); + } + }; + + // 删除文件 + const handleRemove = () => { + setFileList([]); + onChange?.(""); + message.success("图片已删除"); + return true; + }; + + // 预览图片 + const handlePreview = (url: string) => { + const img = new Image(); + img.src = url; + const newWindow = window.open(); + if (newWindow) { + newWindow.document.write(img.outerHTML); + } + }; + + // 格式化文件大小 + 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 = ( +
+ {loading ? ( +
+
+ +
+
上传中...
+
+ ) : ( +
+
+ +
+
+
上传主图封面
+
+ 支持 JPG、PNG、GIF 等格式,最大 {maxSize}MB +
+
+
+ )} +
+ ); + + // 自定义文件列表项 + const customItemRender = ( + originNode: React.ReactElement, + file: UploadFile, + ) => { + if (file.status === "uploading") { + return ( +
+
+
+ +
+
+
{file.name}
+
+ {file.size ? formatFileSize(file.size) : "计算中..."} +
+
+
+
+
+
+ ); + } + + if (file.status === "done") { + return ( +
+
+
+ +
+
+
{file.name}
+
+ {file.size ? formatFileSize(file.size) : "未知大小"} +
+
+
+ {showPreview && ( +
+
+
+ {file.name} +
+
+ {showPreview && ( +
+
+
+
+ ); + } + + return originNode; + }; + + const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload"; + + return ( +
+ + {fileList.length >= 1 ? null : uploadButton} + +
+ ); +}; + +export default MainImgUpload; diff --git a/nkebao/src/components/Upload/README.md b/nkebao/src/components/Upload/README.md index 3005bf291..ac467f611 100644 --- a/nkebao/src/components/Upload/README.md +++ b/nkebao/src/components/Upload/README.md @@ -1,183 +1,68 @@ -# Upload 上传组件 +# Upload 组件使用说明 -基于 antd-mobile 的 ImageUploader 组件封装的上传组件,支持图片上传、预览、删除等功能。 +## MainImgUpload 主图封面上传组件 -## 功能特性 +### 功能特点 -- ✅ 支持单张/多张图片上传 -- ✅ 文件类型和大小验证 -- ✅ 上传进度显示 -- ✅ 图片预览功能 -- ✅ 删除确认 -- ✅ 数量限制 -- ✅ 编辑和新增状态支持 -- ✅ 响应式设计 -- ✅ 头像上传专用组件 +- 只支持上传一张图片作为主图封面 +- 上传后右上角显示删除按钮 +- 支持图片预览功能 +- 响应式设计,适配移动端 +- 样式参考VideoUpload组件风格 -## 组件列表 - -### 1. UploadComponent (图片上传) - -通用的图片上传组件,支持多张图片上传。 - -### 2. AvatarUpload (头像上传) - -专门的头像上传组件,支持圆形头像显示、删除功能。 - -### 3. VideoUpload (视频上传) - -视频上传组件,支持视频文件上传和预览。 - -## 使用方法 - -### 基础用法 +### 使用方法 ```tsx -import React, { useState } from "react"; -import UploadComponent from "@/components/Upload"; +import MainImgUpload from "@/components/Upload/MainImgUpload"; const MyComponent = () => { - const [images, setImages] = useState([]); + const [mainImage, setMainImage] = useState(""); return ( - - ); -}; -``` - -### 头像上传 - -```tsx -import React, { useState } from "react"; -import AvatarUpload from "@/components/Upload/AvatarUpload"; - -const AvatarComponent = () => { - const [avatar, setAvatar] = useState(""); - - return ( - ); }; ``` -### 编辑模式 +### Props 参数 -```tsx -const EditComponent = () => { - const [images, setImages] = useState([ - "https://example.com/image1.jpg", - "https://example.com/image2.jpg", - ]); +| 参数 | 类型 | 默认值 | 说明 | +| ----------- | --------------------- | ------ | ---------------- | +| value | string | '' | 当前图片URL | +| onChange | (url: string) => void | - | 图片URL变化回调 | +| disabled | boolean | false | 是否禁用 | +| className | string | - | 自定义样式类名 | +| maxSize | number | 5 | 最大文件大小(MB) | +| showPreview | boolean | true | 是否显示预览按钮 | - return ( - - ); -}; +### 样式特点 + +- 上传区域:200x200px 的虚线边框区域 +- 图片预览:上传后显示图片,鼠标悬停显示操作按钮 +- 删除按钮:右上角红色删除图标 +- 预览按钮:眼睛图标,点击在新窗口预览 +- 响应式:移动端自动调整尺寸 + +### 文件结构 + +``` +src/components/Upload/ +├── MainImgUpload.tsx # 主图上传组件 +├── mainImgUpload.module.scss # 主图上传样式 +├── VideoUpload.tsx # 视频上传组件 +└── index.module.scss # 通用上传样式 ``` -### 禁用状态 +### 技术实现 -```tsx - -``` - -## API - -### UploadComponent Props - -| 参数 | 说明 | 类型 | 默认值 | -| --------- | -------------- | -------------------------- | ----------- | -| value | 图片URL数组 | `string[]` | `[]` | -| onChange | 图片变化回调 | `(urls: string[]) => void` | - | -| count | 最大上传数量 | `number` | `9` | -| accept | 接受的文件类型 | `string` | `"image/*"` | -| 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` | - -## 注意事项 - -1. **文件大小限制**: 默认限制为 5MB -2. **文件类型**: 默认只接受图片文件 -3. **上传接口**: 使用 `/v1/attachment/upload` 接口 -4. **认证**: 自动携带 token 进行认证 -5. **预览**: 点击图片可预览 -6. **删除**: 删除图片会有确认提示 -7. **头像组件**: 支持圆形显示、删除按钮、上传覆盖层 - -## 样式定制 - -组件支持通过 CSS 模块进行样式定制: - -```scss -.uploadContainer { - // 自定义样式 - :global { - .adm-image-uploader { - // 覆盖 antd-mobile 默认样式 - } - } -} - -.avatarUploadContainer { - // 头像上传组件样式 - .avatarWrapper { - // 头像容器样式 - } -} -``` - -## 错误处理 - -- 文件类型不匹配时会显示错误提示 -- 文件大小超限时会显示错误提示 -- 上传失败时会显示错误提示 -- 网络错误时会显示错误提示 - -## 头像上传特性 - -- **圆形显示**: 头像以圆形方式显示 -- **占位符**: 无头像时显示用户图标 -- **上传覆盖**: 鼠标悬停显示上传图标 -- **删除功能**: 右上角删除按钮 -- **加载状态**: 上传时显示加载提示 -- **尺寸可调**: 支持自定义头像尺寸 +- 基于 antd Upload 组件 +- 使用 antd-mobile 的 Toast 提示 +- 支持 FormData 上传 +- 自动处理文件验证和错误提示 +- 集成项目统一的API请求封装 diff --git a/nkebao/src/components/Upload/VideoUpload/index.module.scss b/nkebao/src/components/Upload/VideoUpload/index.module.scss new file mode 100644 index 000000000..abec0b860 --- /dev/null +++ b/nkebao/src/components/Upload/VideoUpload/index.module.scss @@ -0,0 +1,484 @@ +.uploadContainer { + width: 100%; + + // 自定义上传组件样式 + :global { + .adm-image-uploader { + .adm-image-uploader-upload-button { + width: 100px; + height: 100px; + border: 1px dashed #d9d9d9; + border-radius: 8px; + background: #fafafa; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + + &:hover { + border-color: #1677ff; + background: #f0f8ff; + } + + .adm-image-uploader-upload-button-icon { + font-size: 32px; + color: #999; + } + } + + .adm-image-uploader-item { + width: 100px; + height: 100px; + border-radius: 8px; + overflow: hidden; + position: relative; + + .adm-image-uploader-item-image { + width: 100%; + height: 100%; + object-fit: cover; + } + + .adm-image-uploader-item-delete { + position: absolute; + top: 4px; + right: 4px; + width: 24px; + height: 24px; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 14px; + cursor: pointer; + } + + .adm-image-uploader-item-loading { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + } + } + } + } +} + +// 禁用状态 +.uploadContainer.disabled { + opacity: 0.6; + pointer-events: none; +} + +// 错误状态 +.uploadContainer.error { + :global { + .adm-image-uploader-upload-button { + border-color: #ff4d4f; + background: #fff2f0; + } + } +} + +// 响应式设计 +@media (max-width: 768px) { + .uploadContainer { + :global { + .adm-image-uploader { + .adm-image-uploader-upload-button, + .adm-image-uploader-item { + width: 80px; + height: 80px; + } + + .adm-image-uploader-upload-button-icon { + font-size: 28px; + } + } + } + } +} + +// 头像上传组件样式 +.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; + } +} + +// 视频上传组件样式 +.videoUploadContainer { + width: 100%; + + .videoUploadButton { + 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; + } + } + } + + .videoItem { + 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); + } + + .videoItemContent { + display: flex; + align-items: center; + gap: 12px; + + .videoIcon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 18px; + flex-shrink: 0; + } + + .videoInfo { + flex: 1; + min-width: 0; + + .videoName { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .videoSize { + font-size: 12px; + color: #666; + } + } + + .videoActions { + 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; + } + } + + .videoPreview { + display: flex; + justify-content: center; + align-items: center; + background: #000; + border-radius: 8px; + overflow: hidden; + + video { + border-radius: 8px; + } + } +} + +// 动画效果 +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .videoUploadContainer { + .videoUploadButton { + 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; + } + } + } + } + + .videoItem { + background: #2a2a2a; + border-color: #434343; + + &:hover { + border-color: #40a9ff; + } + + .videoItemContent { + .videoInfo { + .videoName { + color: #fff; + } + + .videoSize { + color: #ccc; + } + } + + .videoActions { + .previewBtn, + .deleteBtn { + &:hover { + background: #434343; + } + } + } + } + } + } +} diff --git a/nkebao/src/components/Upload/VideoUpload.tsx b/nkebao/src/components/Upload/VideoUpload/index.tsx similarity index 100% rename from nkebao/src/components/Upload/VideoUpload.tsx rename to nkebao/src/components/Upload/VideoUpload/index.tsx diff --git a/nkebao/src/pages/mobile/content/materials/form/index.tsx b/nkebao/src/pages/mobile/content/materials/form/index.tsx index 73001e32f..2a1ca504e 100644 --- a/nkebao/src/pages/mobile/content/materials/form/index.tsx +++ b/nkebao/src/pages/mobile/content/materials/form/index.tsx @@ -13,7 +13,7 @@ import { } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; -import UploadComponent from "@/components/Upload/ImageUpload"; +import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload"; import VideoUpload from "@/components/Upload/VideoUpload"; import { getContentItemDetail, diff --git a/nkebao/src/pages/mobile/test/upload.tsx b/nkebao/src/pages/mobile/test/upload.tsx index d87232ba2..22bfb77e6 100644 --- a/nkebao/src/pages/mobile/test/upload.tsx +++ b/nkebao/src/pages/mobile/test/upload.tsx @@ -2,10 +2,11 @@ import React, { useState } from "react"; import { Button, Card, Space, Divider, Toast, Switch } from "antd-mobile"; import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; -import ImageUpload from "@/components/Upload/ImageUpload"; +import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload"; import AvatarUpload from "@/components/Upload/AvatarUpload"; import VideoUpload from "@/components/Upload/VideoUpload"; import FileUpload from "@/components/Upload/FileUpload"; +import MainImgUpload from "@/components/Upload/MainImgUpload"; import styles from "./upload.module.scss"; // 错误边界组件 @@ -77,6 +78,12 @@ const UploadTestPage: React.FC = () => { "ppt", ]); + // 主图上传状态 + const [mainImgUrl, setMainImgUrl] = useState(""); + const [mainImgDisabled, setMainImgDisabled] = useState(false); + const [mainImgMaxSize, setMainImgMaxSize] = useState(5); + const [mainImgShowPreview, setMainImgShowPreview] = useState(true); + return ( } loading={loading}>
@@ -335,6 +342,79 @@ const UploadTestPage: React.FC = () => {
+ + {/* 主图上传测试 */} + + +

主图封面上传组件测试

+

支持单张图片上传作为主图封面,上传后右上角显示删除按钮

+ + {/* 主图上传控制面板 */} +
+
+ 最大文件大小: + + + {mainImgMaxSize}MB + + +
+ +
+ 显示预览按钮: + +
+ +
+ 禁用状态: + +
+
+ + + +
+

当前主图URL:

+
+
+ {mainImgUrl ? ( +
+ {typeof mainImgUrl === "string" ? mainImgUrl : "无效URL"} +
+ ) : ( + 暂无主图 + )} +
+
+
+
+
);