FEAT => 本次更新项目为:移除上传组件相关文件,整合上传功能至主图上传组件,优化代码结构及样式
This commit is contained in:
@@ -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[]; // 支持单个字符串或字符串数组
|
||||
484
nkebao/src/components/Upload/ImageUpload/index.module.scss
Normal file
484
nkebao/src/components/Upload/ImageUpload/index.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
558
nkebao/src/components/Upload/MainImgUpload/index.module.scss
Normal file
558
nkebao/src/components/Upload/MainImgUpload/index.module.scss
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
313
nkebao/src/components/Upload/MainImgUpload/index.tsx
Normal file
313
nkebao/src/components/Upload/MainImgUpload/index.tsx
Normal file
@@ -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<MainImgUploadProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
maxSize = 5,
|
||||
showPreview = true,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
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 = (
|
||||
<div className={style.mainImgUploadButton}>
|
||||
{loading ? (
|
||||
<div className={style.uploadingContainer}>
|
||||
<div className={style.uploadingIcon}>
|
||||
<LoadingOutlined spin />
|
||||
</div>
|
||||
<div className={style.uploadingText}>上传中...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.uploadContent}>
|
||||
<div className={style.uploadIcon}>
|
||||
<CloudUploadOutlined />
|
||||
</div>
|
||||
<div className={style.uploadText}>
|
||||
<div className={style.uploadTitle}>上传主图封面</div>
|
||||
<div className={style.uploadSubtitle}>
|
||||
支持 JPG、PNG、GIF 等格式,最大 {maxSize}MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 自定义文件列表项
|
||||
const customItemRender = (
|
||||
originNode: React.ReactElement,
|
||||
file: UploadFile,
|
||||
) => {
|
||||
if (file.status === "uploading") {
|
||||
return (
|
||||
<div className={style.mainImgItem}>
|
||||
<div className={style.mainImgItemContent}>
|
||||
<div className={style.mainImgIcon}>
|
||||
<PictureOutlined />
|
||||
</div>
|
||||
<div className={style.mainImgInfo}>
|
||||
<div className={style.mainImgName}>{file.name}</div>
|
||||
<div className={style.mainImgSize}>
|
||||
{file.size ? formatFileSize(file.size) : "计算中..."}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.mainImgActions}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove()}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (file.status === "done") {
|
||||
return (
|
||||
<div className={style.mainImgItem}>
|
||||
<div className={style.mainImgItemContent}>
|
||||
<div className={style.mainImgIcon}>
|
||||
<PictureOutlined />
|
||||
</div>
|
||||
<div className={style.mainImgInfo}>
|
||||
<div className={style.mainImgName}>{file.name}</div>
|
||||
<div className={style.mainImgSize}>
|
||||
{file.size ? formatFileSize(file.size) : "未知大小"}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.mainImgActions}>
|
||||
{showPreview && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handlePreview(file.url || "")}
|
||||
className={style.previewBtn}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove()}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.mainImgPreview}>
|
||||
<img
|
||||
src={file.url}
|
||||
alt={file.name}
|
||||
className={style.mainImgImage}
|
||||
/>
|
||||
<div className={style.mainImgOverlay}>
|
||||
<div className={style.mainImgActions}>
|
||||
{showPreview && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handlePreview(file.url || "")}
|
||||
className={style.previewBtn}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemove()}
|
||||
className={style.deleteBtn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return originNode;
|
||||
};
|
||||
|
||||
const action = import.meta.env.VITE_API_BASE_URL + "/v1/attachment/upload";
|
||||
|
||||
return (
|
||||
<div className={`${style.mainImgUploadContainer} ${className || ""}`}>
|
||||
<Upload
|
||||
name="file"
|
||||
headers={{
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}}
|
||||
action={action}
|
||||
multiple={false}
|
||||
fileList={fileList}
|
||||
accept="image/*"
|
||||
listType="text"
|
||||
showUploadList={{
|
||||
showPreviewIcon: false,
|
||||
showRemoveIcon: false,
|
||||
showDownloadIcon: false,
|
||||
}}
|
||||
disabled={disabled || loading}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
maxCount={1}
|
||||
itemRender={customItemRender}
|
||||
>
|
||||
{fileList.length >= 1 ? null : uploadButton}
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainImgUpload;
|
||||
@@ -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<string[]>([]);
|
||||
const [mainImage, setMainImage] = useState<string>("");
|
||||
|
||||
return (
|
||||
<UploadComponent
|
||||
value={images}
|
||||
onChange={setImages}
|
||||
count={5}
|
||||
accept="image/*"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 头像上传
|
||||
|
||||
```tsx
|
||||
import React, { useState } from "react";
|
||||
import AvatarUpload from "@/components/Upload/AvatarUpload";
|
||||
|
||||
const AvatarComponent = () => {
|
||||
const [avatar, setAvatar] = useState<string>("");
|
||||
|
||||
return (
|
||||
<AvatarUpload
|
||||
value={avatar}
|
||||
onChange={setAvatar}
|
||||
size={100}
|
||||
<MainImgUpload
|
||||
value={mainImage}
|
||||
onChange={setMainImage}
|
||||
maxSize={5} // 最大5MB
|
||||
showPreview={true} // 显示预览按钮
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 编辑模式
|
||||
### Props 参数
|
||||
|
||||
```tsx
|
||||
const EditComponent = () => {
|
||||
const [images, setImages] = useState<string[]>([
|
||||
"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 (
|
||||
<UploadComponent
|
||||
value={images}
|
||||
onChange={setImages}
|
||||
count={9}
|
||||
disabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
### 样式特点
|
||||
|
||||
- 上传区域:200x200px 的虚线边框区域
|
||||
- 图片预览:上传后显示图片,鼠标悬停显示操作按钮
|
||||
- 删除按钮:右上角红色删除图标
|
||||
- 预览按钮:眼睛图标,点击在新窗口预览
|
||||
- 响应式:移动端自动调整尺寸
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
src/components/Upload/
|
||||
├── MainImgUpload.tsx # 主图上传组件
|
||||
├── mainImgUpload.module.scss # 主图上传样式
|
||||
├── VideoUpload.tsx # 视频上传组件
|
||||
└── index.module.scss # 通用上传样式
|
||||
```
|
||||
|
||||
### 禁用状态
|
||||
### 技术实现
|
||||
|
||||
```tsx
|
||||
<UploadComponent value={images} onChange={setImages} disabled={true} />
|
||||
```
|
||||
|
||||
## 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请求封装
|
||||
|
||||
484
nkebao/src/components/Upload/VideoUpload/index.module.scss
Normal file
484
nkebao/src/components/Upload/VideoUpload/index.module.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>("");
|
||||
const [mainImgDisabled, setMainImgDisabled] = useState(false);
|
||||
const [mainImgMaxSize, setMainImgMaxSize] = useState(5);
|
||||
const [mainImgShowPreview, setMainImgShowPreview] = useState(true);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="上传组件功能测试" />} loading={loading}>
|
||||
<div className={styles.container}>
|
||||
@@ -335,6 +342,79 @@ const UploadTestPage: React.FC = () => {
|
||||
</div>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* 主图上传测试 */}
|
||||
<ErrorBoundary>
|
||||
<Card className={styles.testSection}>
|
||||
<h3>主图封面上传组件测试</h3>
|
||||
<p>支持单张图片上传作为主图封面,上传后右上角显示删除按钮</p>
|
||||
|
||||
{/* 主图上传控制面板 */}
|
||||
<div className={styles.controlPanel}>
|
||||
<div className={styles.controlItem}>
|
||||
<span>最大文件大小:</span>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setMainImgMaxSize(Math.max(1, mainImgMaxSize - 1))
|
||||
}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<span>{mainImgMaxSize}MB</span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setMainImgMaxSize(Math.min(20, mainImgMaxSize + 1))
|
||||
}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className={styles.controlItem}>
|
||||
<span>显示预览按钮:</span>
|
||||
<Switch
|
||||
checked={mainImgShowPreview}
|
||||
onChange={setMainImgShowPreview}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.controlItem}>
|
||||
<span>禁用状态:</span>
|
||||
<Switch
|
||||
checked={mainImgDisabled}
|
||||
onChange={setMainImgDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MainImgUpload
|
||||
value={mainImgUrl}
|
||||
onChange={setMainImgUrl}
|
||||
disabled={mainImgDisabled}
|
||||
maxSize={mainImgMaxSize}
|
||||
showPreview={mainImgShowPreview}
|
||||
/>
|
||||
|
||||
<div className={styles.result}>
|
||||
<h4>当前主图URL:</h4>
|
||||
<div className={styles.urlList}>
|
||||
<div className={styles.urlItem}>
|
||||
{mainImgUrl ? (
|
||||
<div className={styles.url}>
|
||||
{typeof mainImgUrl === "string" ? mainImgUrl : "无效URL"}
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.emptyText}>暂无主图</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user