增强文件和视频上传功能:添加上传成功和失败的反馈信息,检查响应状态以确保上传结果的准确性;更新素材管理模块,优化删除和编辑功能,增加确认删除的弹窗,提升用户体验。
This commit is contained in:
243
Touchkebao/src/components/Upload/AudioUpload/index.module.scss
Normal file
243
Touchkebao/src/components/Upload/AudioUpload/index.module.scss
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
.videoUploadContainer {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
// 覆盖 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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoUploadButton {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
min-height: clamp(90px, 20vw, 180px);
|
||||||
|
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: clamp(24px, 4vw, 32px);
|
||||||
|
color: #1890ff;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadingText {
|
||||||
|
font-size: clamp(11px, 2vw, 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: clamp(50px, 6vw, 48px);
|
||||||
|
color: #1890ff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadText {
|
||||||
|
.uploadTitle {
|
||||||
|
font-size: clamp(14px, 2.5vw, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadSubtitle {
|
||||||
|
font-size: clamp(10px, 1.5vw, 14px);
|
||||||
|
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: clamp(28px, 5vw, 40px);
|
||||||
|
height: clamp(28px, 5vw, 40px);
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: clamp(14px, 2.5vw, 18px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoInfo {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.videoName {
|
||||||
|
font-size: clamp(11px, 2vw, 14px);
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoSize {
|
||||||
|
font-size: clamp(10px, 1.5vw, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用状态
|
||||||
|
.videoUploadContainer.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误状态
|
||||||
|
.videoUploadContainer.error {
|
||||||
|
.videoUploadButton {
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
background: #fff2f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
385
Touchkebao/src/components/Upload/AudioUpload/index.tsx
Normal file
385
Touchkebao/src/components/Upload/AudioUpload/index.tsx
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Upload, message, Progress, Button, Modal } from "antd";
|
||||||
|
import {
|
||||||
|
LoadingOutlined,
|
||||||
|
SoundOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
FileOutlined,
|
||||||
|
CloudUploadOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import type { UploadProps, UploadFile } from "antd/es/upload/interface";
|
||||||
|
import style from "./index.module.scss";
|
||||||
|
|
||||||
|
interface AudioUploadProps {
|
||||||
|
value?: string | string[]; // 支持单个字符串或字符串数组
|
||||||
|
onChange?: (url: string | string[]) => void; // 支持单个字符串或字符串数组
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
maxSize?: number; // 最大文件大小(MB)
|
||||||
|
showPreview?: boolean; // 是否显示预览
|
||||||
|
maxCount?: number; // 最大上传数量,默认为1
|
||||||
|
}
|
||||||
|
|
||||||
|
const AudioUpload: React.FC<AudioUploadProps> = ({
|
||||||
|
value = "",
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
maxSize = 50,
|
||||||
|
showPreview = true,
|
||||||
|
maxCount = 1,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState("");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
// 处理单个字符串或字符串数组
|
||||||
|
const urls = Array.isArray(value) ? value : [value];
|
||||||
|
const files: UploadFile[] = urls.map((url, index) => ({
|
||||||
|
uid: `file-${index}`,
|
||||||
|
name: `audio-${index + 1}`,
|
||||||
|
status: "done",
|
||||||
|
url: url || "",
|
||||||
|
}));
|
||||||
|
setFileList(files);
|
||||||
|
} else {
|
||||||
|
setFileList([]);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
// 文件验证
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
const isAudio = file.type.startsWith("audio/");
|
||||||
|
if (!isAudio) {
|
||||||
|
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);
|
||||||
|
// 模拟上传进度
|
||||||
|
const progress = Math.min(99, Math.random() * 100);
|
||||||
|
setUploadProgress(progress);
|
||||||
|
} else if (info.file.status === "done") {
|
||||||
|
setLoading(false);
|
||||||
|
setUploadProgress(100);
|
||||||
|
|
||||||
|
// 从响应中获取上传后的URL
|
||||||
|
let uploadedUrl = "";
|
||||||
|
|
||||||
|
if (info.file.response) {
|
||||||
|
// 检查响应是否成功
|
||||||
|
if (info.file.response.code && info.file.response.code !== 200) {
|
||||||
|
message.error(info.file.response.message || "上传失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
message.success("音频上传成功!");
|
||||||
|
if (maxCount === 1) {
|
||||||
|
// 单个音频模式
|
||||||
|
onChange?.(uploadedUrl);
|
||||||
|
} else {
|
||||||
|
// 多个音频模式
|
||||||
|
const currentUrls = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: value
|
||||||
|
? [value]
|
||||||
|
: [];
|
||||||
|
const newUrls = [...currentUrls, uploadedUrl];
|
||||||
|
onChange?.(newUrls);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error("上传失败,未获取到音频URL");
|
||||||
|
}
|
||||||
|
} 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.videoUploadButton}>
|
||||||
|
{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}>
|
||||||
|
支持 MP3、WAV、AAC 等格式,最大 {maxSize}MB
|
||||||
|
{maxCount > 1 && `,最多上传 ${maxCount} 个音频`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 自定义文件列表项
|
||||||
|
const customItemRender = (
|
||||||
|
originNode: React.ReactElement,
|
||||||
|
file: UploadFile,
|
||||||
|
) => {
|
||||||
|
if (file.status === "uploading") {
|
||||||
|
return (
|
||||||
|
<div className={style.videoItem}>
|
||||||
|
<div className={style.videoItemContent}>
|
||||||
|
<div className={style.videoIcon}>
|
||||||
|
<SoundOutlined />
|
||||||
|
</div>
|
||||||
|
<div className={style.videoInfo}>
|
||||||
|
<div className={style.videoName}>{file.name}</div>
|
||||||
|
<div className={style.videoSize}>
|
||||||
|
{file.size ? formatFileSize(file.size) : "计算中..."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.videoActions}>
|
||||||
|
<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.videoItem}>
|
||||||
|
<div className={style.videoItemContent}>
|
||||||
|
<div className={style.videoIcon}>
|
||||||
|
<SoundOutlined />
|
||||||
|
</div>
|
||||||
|
<div className={style.videoInfo}>
|
||||||
|
<div className={style.videoName}>{file.name}</div>
|
||||||
|
<div className={style.videoSize}>
|
||||||
|
{file.size ? formatFileSize(file.size) : "未知大小"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.videoActions}>
|
||||||
|
{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.videoUploadContainer} ${className || ""}`}>
|
||||||
|
<Upload
|
||||||
|
name="file"
|
||||||
|
headers={{
|
||||||
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
|
}}
|
||||||
|
action={action}
|
||||||
|
multiple={maxCount > 1}
|
||||||
|
fileList={fileList}
|
||||||
|
accept="audio/*"
|
||||||
|
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={600}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<div className={style.videoPreview}>
|
||||||
|
<audio controls style={{ width: "100%" }} src={previewUrl}>
|
||||||
|
您的浏览器不支持音频播放
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioUpload;
|
||||||
@@ -176,12 +176,17 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
} else if (info.file.status === "done") {
|
} else if (info.file.status === "done") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
message.success("文件上传成功!");
|
|
||||||
|
|
||||||
// 从响应中获取上传后的URL
|
// 从响应中获取上传后的URL
|
||||||
let uploadedUrl = "";
|
let uploadedUrl = "";
|
||||||
|
|
||||||
if (info.file.response) {
|
if (info.file.response) {
|
||||||
|
// 检查响应是否成功
|
||||||
|
if (info.file.response.code && info.file.response.code !== 200) {
|
||||||
|
message.error(info.file.response.message || "上传失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof info.file.response === "string") {
|
if (typeof info.file.response === "string") {
|
||||||
uploadedUrl = info.file.response;
|
uploadedUrl = info.file.response;
|
||||||
} else if (info.file.response.data) {
|
} else if (info.file.response.data) {
|
||||||
@@ -195,6 +200,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
|
message.success("文件上传成功!");
|
||||||
if (maxCount === 1) {
|
if (maxCount === 1) {
|
||||||
// 单个文件模式
|
// 单个文件模式
|
||||||
onChange?.(uploadedUrl);
|
onChange?.(uploadedUrl);
|
||||||
@@ -208,6 +214,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
const newUrls = [...currentUrls, uploadedUrl];
|
const newUrls = [...currentUrls, uploadedUrl];
|
||||||
onChange?.(newUrls);
|
onChange?.(newUrls);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
message.error("上传失败,未获取到文件URL");
|
||||||
}
|
}
|
||||||
} else if (info.file.status === "error") {
|
} else if (info.file.status === "error") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -108,12 +108,17 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
} else if (info.file.status === "done") {
|
} else if (info.file.status === "done") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
message.success("视频上传成功!");
|
|
||||||
|
|
||||||
// 从响应中获取上传后的URL
|
// 从响应中获取上传后的URL
|
||||||
let uploadedUrl = "";
|
let uploadedUrl = "";
|
||||||
|
|
||||||
if (info.file.response) {
|
if (info.file.response) {
|
||||||
|
// 检查响应是否成功
|
||||||
|
if (info.file.response.code && info.file.response.code !== 200) {
|
||||||
|
message.error(info.file.response.message || "上传失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof info.file.response === "string") {
|
if (typeof info.file.response === "string") {
|
||||||
uploadedUrl = info.file.response;
|
uploadedUrl = info.file.response;
|
||||||
} else if (info.file.response.data) {
|
} else if (info.file.response.data) {
|
||||||
@@ -127,6 +132,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uploadedUrl) {
|
if (uploadedUrl) {
|
||||||
|
message.success("视频上传成功!");
|
||||||
if (maxCount === 1) {
|
if (maxCount === 1) {
|
||||||
// 单个视频模式
|
// 单个视频模式
|
||||||
onChange?.(uploadedUrl);
|
onChange?.(uploadedUrl);
|
||||||
@@ -140,6 +146,8 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
|
|||||||
const newUrls = [...currentUrls, uploadedUrl];
|
const newUrls = [...currentUrls, uploadedUrl];
|
||||||
onChange?.(newUrls);
|
onChange?.(newUrls);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
message.error("上传失败,未获取到视频URL");
|
||||||
}
|
}
|
||||||
} else if (info.file.status === "error") {
|
} else if (info.file.status === "error") {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export interface ContentItem {
|
|||||||
|
|
||||||
// 链接数据类型
|
// 链接数据类型
|
||||||
export interface LinkData {
|
export interface LinkData {
|
||||||
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
cover: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaterialAddRequest {
|
export interface MaterialAddRequest {
|
||||||
@@ -50,7 +52,7 @@ export function getMaterialDetails(id: string) {
|
|||||||
|
|
||||||
// 素材管理-删除
|
// 素材管理-删除
|
||||||
export function deleteMaterial(id: string) {
|
export function deleteMaterial(id: string) {
|
||||||
return request("/v1/kefu/content/material/del", { id }, "GET");
|
return request("/v1/kefu/content/material/del", { id }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 素材管理-更新
|
// 素材管理-更新
|
||||||
@@ -106,7 +108,7 @@ export function getSensitiveWordDetails(id: string) {
|
|||||||
|
|
||||||
// 违禁词管理-删除
|
// 违禁词管理-删除
|
||||||
export function deleteSensitiveWord(id: string) {
|
export function deleteSensitiveWord(id: string) {
|
||||||
return request("/v1/kefu/content/sensitiveWord/del", { id }, "GET");
|
return request("/v1/kefu/content/sensitiveWord/del", { id }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 违禁词管理-更新
|
// 违禁词管理-更新
|
||||||
@@ -161,7 +163,7 @@ export function getKeywordDetails(id: string) {
|
|||||||
|
|
||||||
// 关键词回复-删除
|
// 关键词回复-删除
|
||||||
export function deleteKeyword(id: string) {
|
export function deleteKeyword(id: string) {
|
||||||
return request("/v1/kefu/content/keywords/del", { id }, "GET");
|
return request("/v1/kefu/content/keywords/del", { id }, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键词回复-更新
|
// 关键词回复-更新
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Button, Input, Card, message, Switch, Tag } from "antd";
|
import { Button, Input, Card, message, Modal } from "antd";
|
||||||
import {
|
import {
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
FilterOutlined,
|
FilterOutlined,
|
||||||
@@ -12,12 +12,12 @@ import {
|
|||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
FileImageOutlined,
|
FileImageOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import styles from "../../index.module.scss";
|
import styles from "../../index.module.scss";
|
||||||
import {
|
import {
|
||||||
getMaterialList,
|
getMaterialList,
|
||||||
deleteMaterial,
|
deleteMaterial,
|
||||||
setMaterialStatus,
|
|
||||||
type MaterialListParams,
|
type MaterialListParams,
|
||||||
} from "../../api";
|
} from "../../api";
|
||||||
import MaterialModal from "../modals/MaterialModal";
|
import MaterialModal from "../modals/MaterialModal";
|
||||||
@@ -40,232 +40,234 @@ interface MaterialItem {
|
|||||||
userName: string;
|
userName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MaterialManagement = forwardRef<any, {}>((props, ref) => {
|
const MaterialManagement = forwardRef<any, Record<string, never>>(
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
(props, ref) => {
|
||||||
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
|
const [searchValue, setSearchValue] = useState<string>("");
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
||||||
null,
|
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
||||||
);
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
// 获取类型图标
|
// 获取类型图标
|
||||||
const getTypeIcon = (type: string) => {
|
const getTypeIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "文本":
|
case "文本":
|
||||||
return <FileTextOutlined className={styles.typeIcon} />;
|
return <FileTextOutlined className={styles.typeIcon} />;
|
||||||
case "图片":
|
case "图片":
|
||||||
return <FileImageOutlined className={styles.typeIcon} />;
|
return <FileImageOutlined className={styles.typeIcon} />;
|
||||||
case "视频":
|
case "视频":
|
||||||
return <PlayCircleOutlined className={styles.typeIcon} />;
|
return <PlayCircleOutlined className={styles.typeIcon} />;
|
||||||
default:
|
default:
|
||||||
return <FileTextOutlined className={styles.typeIcon} />;
|
return <FileTextOutlined className={styles.typeIcon} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取类型颜色
|
// 获取素材列表
|
||||||
const getTypeColor = (type: string) => {
|
const fetchMaterials = async (params?: MaterialListParams) => {
|
||||||
switch (type) {
|
try {
|
||||||
case "文本":
|
setLoading(true);
|
||||||
return "#fa8c16";
|
const response = await getMaterialList(params || {});
|
||||||
case "图片":
|
if (response) {
|
||||||
return "#52c41a";
|
setMaterialsList(response.list || []);
|
||||||
case "视频":
|
} else {
|
||||||
return "#1890ff";
|
setMaterialsList([]);
|
||||||
default:
|
message.error(response?.message || "获取素材列表失败");
|
||||||
return "#8c8c8c";
|
}
|
||||||
}
|
} catch (error) {
|
||||||
};
|
console.error("获取素材列表失败:", error);
|
||||||
|
|
||||||
// 获取素材列表
|
|
||||||
const fetchMaterials = async (params?: MaterialListParams) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await getMaterialList(params || {});
|
|
||||||
if (response) {
|
|
||||||
setMaterialsList(response.list || []);
|
|
||||||
} else {
|
|
||||||
setMaterialsList([]);
|
setMaterialsList([]);
|
||||||
message.error(response?.message || "获取素材列表失败");
|
message.error("获取素材列表失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("获取素材列表失败:", error);
|
|
||||||
setMaterialsList([]);
|
|
||||||
message.error("获取素材列表失败");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
fetchMaterials,
|
fetchMaterials,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 素材管理相关函数
|
// 素材管理相关函数
|
||||||
const handleDeleteMaterial = async (id: number) => {
|
const handleDeleteMaterial = async (id: number) => {
|
||||||
try {
|
Modal.confirm({
|
||||||
const response = await deleteMaterial(id.toString());
|
title: "确认删除",
|
||||||
if (response) {
|
content: "确定要删除这个素材吗?删除后无法恢复。",
|
||||||
setMaterialsList(prev => prev.filter(item => item.id !== id));
|
okText: "确定",
|
||||||
message.success("删除成功");
|
cancelText: "取消",
|
||||||
} else {
|
okType: "danger",
|
||||||
message.error(response?.message || "删除失败");
|
onOk: async () => {
|
||||||
}
|
try {
|
||||||
} catch (error) {
|
await deleteMaterial(id.toString());
|
||||||
console.error("删除失败:", error);
|
setMaterialsList(prev => prev.filter(item => item.id !== id));
|
||||||
message.error("删除失败");
|
message.success("删除成功");
|
||||||
}
|
} catch (error) {
|
||||||
};
|
message.error("删除失败");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 切换素材状态
|
// 编辑素材
|
||||||
const handleToggleMaterialStatus = async (id: number, checked: boolean) => {
|
const handleEditMaterial = (id: number) => {
|
||||||
try {
|
setEditingMaterialId(id);
|
||||||
const response = await setMaterialStatus({ id: id.toString() });
|
setEditModalVisible(true);
|
||||||
if (response) {
|
};
|
||||||
setMaterialsList(prev =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.id === id ? { ...item, status: checked ? 1 : 0 } : item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
message.success(checked ? "启用成功" : "禁用成功");
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "状态更新失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("状态更新失败:", error);
|
|
||||||
message.error("状态更新失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑素材
|
// 编辑弹窗成功回调
|
||||||
const handleEditMaterial = (id: number) => {
|
const handleEditSuccess = () => {
|
||||||
setEditingMaterialId(id);
|
fetchMaterials(); // 重新获取数据
|
||||||
setEditModalVisible(true);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑弹窗成功回调
|
// 搜索处理函数
|
||||||
const handleEditSuccess = () => {
|
const handleSearch = (value: string) => {
|
||||||
fetchMaterials(); // 重新获取数据
|
fetchMaterials({ keyword: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索处理函数
|
// 组件挂载时获取数据
|
||||||
const handleSearch = (value: string) => {
|
useEffect(() => {
|
||||||
fetchMaterials({ keyword: value });
|
fetchMaterials();
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
return (
|
||||||
useEffect(() => {
|
<div className={styles.materialContent}>
|
||||||
fetchMaterials();
|
<div className={styles.searchSection}>
|
||||||
}, []);
|
<Search
|
||||||
|
placeholder="搜索素材..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={e => setSearchValue(e.target.value)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
/>
|
||||||
|
<Button icon={<FilterOutlined />}>筛选</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
<div className={styles.materialGrid}>
|
||||||
<div className={styles.materialContent}>
|
{loading ? (
|
||||||
<div className={styles.searchSection}>
|
<div className={styles.loading}>加载中...</div>
|
||||||
<Search
|
) : materialsList.length === 0 ? (
|
||||||
placeholder="搜索素材..."
|
<div className={styles.empty}>暂无素材数据</div>
|
||||||
value={searchValue}
|
) : (
|
||||||
onChange={e => setSearchValue(e.target.value)}
|
materialsList.map(item => (
|
||||||
onSearch={handleSearch}
|
<Card
|
||||||
style={{ width: 300 }}
|
key={item.id}
|
||||||
prefix={<SearchOutlined />}
|
className={styles.materialCard}
|
||||||
/>
|
hoverable
|
||||||
<Button icon={<FilterOutlined />}>筛选</Button>
|
actions={[
|
||||||
</div>
|
<Button
|
||||||
|
key="edit"
|
||||||
<div className={styles.materialGrid}>
|
type="text"
|
||||||
{loading ? (
|
icon={<FormOutlined />}
|
||||||
<div className={styles.loading}>加载中...</div>
|
onClick={e => {
|
||||||
) : materialsList.length === 0 ? (
|
e.stopPropagation();
|
||||||
<div className={styles.empty}>暂无素材数据</div>
|
handleEditMaterial(item.id);
|
||||||
) : (
|
|
||||||
materialsList.map(item => (
|
|
||||||
<Card
|
|
||||||
key={item.id}
|
|
||||||
className={styles.materialCard}
|
|
||||||
hoverable
|
|
||||||
onClick={() => handleEditMaterial(item.id)}
|
|
||||||
>
|
|
||||||
<div className={styles.thumbnail}>
|
|
||||||
{item.cover ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
编辑
|
||||||
src={item.cover}
|
</Button>,
|
||||||
alt={item.title}
|
<Button
|
||||||
|
key="delete"
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteMaterial(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.thumbnail}
|
||||||
|
onClick={() => handleEditMaterial(item.id)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{item.cover ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.cover}
|
||||||
|
alt={item.title}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "4px",
|
||||||
|
right: "4px",
|
||||||
|
background: "rgba(0, 0, 0, 0.6)",
|
||||||
|
color: "white",
|
||||||
|
padding: "2px 6px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
fontSize: "10px",
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.type}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "cover",
|
display: "flex",
|
||||||
}}
|
alignItems: "center",
|
||||||
/>
|
justifyContent: "center",
|
||||||
<div
|
background: "#f5f5f5",
|
||||||
style={{
|
color: "#999",
|
||||||
position: "absolute",
|
flexDirection: "column",
|
||||||
top: "4px",
|
|
||||||
right: "4px",
|
|
||||||
background: "rgba(0, 0, 0, 0.6)",
|
|
||||||
color: "white",
|
|
||||||
padding: "2px 6px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "10px",
|
|
||||||
fontWeight: "500",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.type}
|
{getTypeIcon(item.type)}
|
||||||
|
<span style={{ marginTop: "8px", fontSize: "12px" }}>
|
||||||
|
{item.type}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
background: "#f5f5f5",
|
|
||||||
color: "#999",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getTypeIcon(item.type)}
|
|
||||||
<span style={{ marginTop: "8px", fontSize: "12px" }}>
|
|
||||||
{item.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.cardContent}>
|
|
||||||
<div className={styles.title}>{item.title}</div>
|
|
||||||
<div className={styles.meta}>
|
|
||||||
<div>创建人: {item.userName}</div>
|
|
||||||
<div>{item.createTime}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
<div className={styles.cardContent}>
|
||||||
<MaterialModal
|
<div className={styles.title}>{item.title}</div>
|
||||||
visible={editModalVisible}
|
<div className={styles.meta}>
|
||||||
mode="edit"
|
<div>创建人: {item.userName}</div>
|
||||||
materialId={editingMaterialId}
|
<div>{item.createTime}</div>
|
||||||
onCancel={() => {
|
</div>
|
||||||
setEditModalVisible(false);
|
</div>
|
||||||
setEditingMaterialId(null);
|
</Card>
|
||||||
}}
|
))
|
||||||
onSuccess={handleEditSuccess}
|
)}
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
{/* 编辑弹窗 */}
|
||||||
});
|
<MaterialModal
|
||||||
|
visible={editModalVisible}
|
||||||
|
mode="edit"
|
||||||
|
materialId={editingMaterialId}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
setEditingMaterialId(null);
|
||||||
|
}}
|
||||||
|
onSuccess={handleEditSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
MaterialManagement.displayName = "MaterialManagement";
|
||||||
|
|
||||||
export default MaterialManagement;
|
export default MaterialManagement;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
||||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||||
import FileUpload from "@/components/Upload/FileUpload";
|
import FileUpload from "@/components/Upload/FileUpload";
|
||||||
import type { ContentItem } from "../../api";
|
import AudioUpload from "@/components/Upload/AudioUpload";
|
||||||
|
import type { ContentItem, LinkData } from "../../api";
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -93,7 +94,7 @@ const ContentManager: React.FC<ContentManagerProps> = ({
|
|||||||
// 根据新类型重置数据
|
// 根据新类型重置数据
|
||||||
let newData: any;
|
let newData: any;
|
||||||
if (newType === "link") {
|
if (newType === "link") {
|
||||||
newData = "";
|
newData = { title: "", url: "", cover: "" };
|
||||||
} else {
|
} else {
|
||||||
newData = "";
|
newData = "";
|
||||||
}
|
}
|
||||||
@@ -181,7 +182,10 @@ const ContentManager: React.FC<ContentManagerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<VideoUpload
|
<VideoUpload
|
||||||
value={item.data as string}
|
value={item.data as string}
|
||||||
onChange={url => updateItemData(index, url)}
|
onChange={url => {
|
||||||
|
const videoUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||||
|
updateItemData(index, videoUrl);
|
||||||
|
}}
|
||||||
maxSize={50}
|
maxSize={50}
|
||||||
showPreview={true}
|
showPreview={true}
|
||||||
/>
|
/>
|
||||||
@@ -190,7 +194,10 @@ const ContentManager: React.FC<ContentManagerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
value={item.data as string}
|
value={item.data as string}
|
||||||
onChange={url => updateItemData(index, url)}
|
onChange={url => {
|
||||||
|
const fileUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||||
|
updateItemData(index, fileUrl);
|
||||||
|
}}
|
||||||
maxSize={10}
|
maxSize={10}
|
||||||
showPreview={true}
|
showPreview={true}
|
||||||
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
|
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
|
||||||
@@ -198,23 +205,48 @@ const ContentManager: React.FC<ContentManagerProps> = ({
|
|||||||
);
|
);
|
||||||
case "audio":
|
case "audio":
|
||||||
return (
|
return (
|
||||||
<FileUpload
|
<AudioUpload
|
||||||
value={item.data as string}
|
value={item.data as string}
|
||||||
onChange={url => updateItemData(index, url)}
|
onChange={url => {
|
||||||
|
const audioUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
||||||
|
updateItemData(index, audioUrl);
|
||||||
|
}}
|
||||||
maxSize={50}
|
maxSize={50}
|
||||||
showPreview={true}
|
showPreview={true}
|
||||||
acceptTypes={["mp3", "wav", "aac", "m4a", "ogg"]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "link": {
|
case "link": {
|
||||||
|
const linkData = (item.data as LinkData) || {
|
||||||
|
title: "",
|
||||||
|
url: "",
|
||||||
|
cover: "",
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
value={item.data as string}
|
value={linkData.title}
|
||||||
onChange={e => updateItemData(index, e.target.value)}
|
onChange={e =>
|
||||||
|
updateItemData(index, { ...linkData, title: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="链接标题"
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={linkData.url}
|
||||||
|
onChange={e =>
|
||||||
|
updateItemData(index, { ...linkData, url: e.target.value })
|
||||||
|
}
|
||||||
placeholder="链接URL"
|
placeholder="链接URL"
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: 8 }}
|
||||||
/>
|
/>
|
||||||
|
<ImageUpload
|
||||||
|
count={1}
|
||||||
|
accept="image/*"
|
||||||
|
value={linkData.cover ? [linkData.cover] : []}
|
||||||
|
onChange={urls =>
|
||||||
|
updateItemData(index, { ...linkData, cover: urls[0] || "" })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,14 +55,17 @@ const MaterialModal: React.FC<MaterialModalProps> = ({
|
|||||||
[form],
|
[form],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 当弹窗打开且为编辑模式时,获取详情
|
// 当弹窗打开时处理数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && mode === "edit" && materialId) {
|
if (visible) {
|
||||||
fetchMaterialDetails(materialId);
|
if (mode === "edit" && materialId) {
|
||||||
} else if (visible && mode === "add") {
|
// 编辑模式:获取详情
|
||||||
// 添加模式时重置表单
|
fetchMaterialDetails(materialId);
|
||||||
form.resetFields();
|
} else if (mode === "add") {
|
||||||
setContentItems([]);
|
// 添加模式:重置表单
|
||||||
|
form.resetFields();
|
||||||
|
setContentItems([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [visible, mode, materialId, fetchMaterialDetails, form]);
|
}, [visible, mode, materialId, fetchMaterialDetails, form]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user