增强文件和视频上传功能:添加上传成功和失败的反馈信息,检查响应状态以确保上传结果的准确性;更新素材管理模块,优化删除和编辑功能,增加确认删除的弹窗,提升用户体验。

This commit is contained in:
超级老白兔
2025-09-28 15:38:06 +08:00
parent 3b063ff64c
commit c6639a8259
8 changed files with 911 additions and 228 deletions

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

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

View File

@@ -176,12 +176,17 @@ const FileUpload: React.FC<FileUploadProps> = ({
} else if (info.file.status === "done") {
setLoading(false);
setUploadProgress(100);
message.success("文件上传成功!");
// 从响应中获取上传后的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) {
@@ -195,6 +200,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
}
if (uploadedUrl) {
message.success("文件上传成功!");
if (maxCount === 1) {
// 单个文件模式
onChange?.(uploadedUrl);
@@ -208,6 +214,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
const newUrls = [...currentUrls, uploadedUrl];
onChange?.(newUrls);
}
} else {
message.error("上传失败未获取到文件URL");
}
} else if (info.file.status === "error") {
setLoading(false);

View File

@@ -108,12 +108,17 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
} else if (info.file.status === "done") {
setLoading(false);
setUploadProgress(100);
message.success("视频上传成功!");
// 从响应中获取上传后的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) {
@@ -127,6 +132,7 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
}
if (uploadedUrl) {
message.success("视频上传成功!");
if (maxCount === 1) {
// 单个视频模式
onChange?.(uploadedUrl);
@@ -140,6 +146,8 @@ const VideoUpload: React.FC<VideoUploadProps> = ({
const newUrls = [...currentUrls, uploadedUrl];
onChange?.(newUrls);
}
} else {
message.error("上传失败未获取到视频URL");
}
} else if (info.file.status === "error") {
setLoading(false);

View File

@@ -15,7 +15,9 @@ export interface ContentItem {
// 链接数据类型
export interface LinkData {
title: string;
url: string;
cover: string;
}
export interface MaterialAddRequest {
@@ -50,7 +52,7 @@ export function getMaterialDetails(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) {
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) {
return request("/v1/kefu/content/keywords/del", { id }, "GET");
return request("/v1/kefu/content/keywords/del", { id }, "DELETE");
}
// 关键词回复-更新

View File

@@ -4,7 +4,7 @@ import React, {
forwardRef,
useImperativeHandle,
} from "react";
import { Button, Input, Card, message, Switch, Tag } from "antd";
import { Button, Input, Card, message, Modal } from "antd";
import {
SearchOutlined,
FilterOutlined,
@@ -12,12 +12,12 @@ import {
FileTextOutlined,
FileImageOutlined,
PlayCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import styles from "../../index.module.scss";
import {
getMaterialList,
deleteMaterial,
setMaterialStatus,
type MaterialListParams,
} from "../../api";
import MaterialModal from "../modals/MaterialModal";
@@ -40,232 +40,234 @@ interface MaterialItem {
userName: string;
}
const MaterialManagement = forwardRef<any, {}>((props, ref) => {
const [searchValue, setSearchValue] = useState<string>("");
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
null,
);
const MaterialManagement = forwardRef<any, Record<string, never>>(
(props, ref) => {
const [searchValue, setSearchValue] = useState<string>("");
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
null,
);
// 获取类型图标
const getTypeIcon = (type: string) => {
switch (type) {
case "文本":
return <FileTextOutlined className={styles.typeIcon} />;
case "图片":
return <FileImageOutlined className={styles.typeIcon} />;
case "视频":
return <PlayCircleOutlined className={styles.typeIcon} />;
default:
return <FileTextOutlined className={styles.typeIcon} />;
}
};
// 获取类型图标
const getTypeIcon = (type: string) => {
switch (type) {
case "文本":
return <FileTextOutlined className={styles.typeIcon} />;
case "图片":
return <FileImageOutlined className={styles.typeIcon} />;
case "视频":
return <PlayCircleOutlined className={styles.typeIcon} />;
default:
return <FileTextOutlined className={styles.typeIcon} />;
}
};
// 获取类型颜色
const getTypeColor = (type: string) => {
switch (type) {
case "文本":
return "#fa8c16";
case "图片":
return "#52c41a";
case "视频":
return "#1890ff";
default:
return "#8c8c8c";
}
};
// 获取素材列表
const fetchMaterials = async (params?: MaterialListParams) => {
try {
setLoading(true);
const response = await getMaterialList(params || {});
if (response) {
setMaterialsList(response.list || []);
} else {
// 获取素材列表
const fetchMaterials = async (params?: MaterialListParams) => {
try {
setLoading(true);
const response = await getMaterialList(params || {});
if (response) {
setMaterialsList(response.list || []);
} else {
setMaterialsList([]);
message.error(response?.message || "获取素材列表失败");
}
} catch (error) {
console.error("获取素材列表失败:", error);
setMaterialsList([]);
message.error(response?.message || "获取素材列表失败");
message.error("获取素材列表失败");
} finally {
setLoading(false);
}
} catch (error) {
console.error("获取素材列表失败:", error);
setMaterialsList([]);
message.error("获取素材列表失败");
} finally {
setLoading(false);
}
};
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchMaterials,
}));
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
fetchMaterials,
}));
// 素材管理相关函数
const handleDeleteMaterial = async (id: number) => {
try {
const response = await deleteMaterial(id.toString());
if (response) {
setMaterialsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} else {
message.error(response?.message || "删除失败");
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 素材管理相关函数
const handleDeleteMaterial = async (id: number) => {
Modal.confirm({
title: "确认删除",
content: "确定要删除这个素材吗?删除后无法恢复。",
okText: "确定",
cancelText: "取消",
okType: "danger",
onOk: async () => {
try {
await deleteMaterial(id.toString());
setMaterialsList(prev => prev.filter(item => item.id !== id));
message.success("删除成功");
} catch (error) {
message.error("删除失败");
}
},
});
};
// 切换素材状态
const handleToggleMaterialStatus = async (id: number, checked: boolean) => {
try {
const response = await setMaterialStatus({ id: id.toString() });
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) => {
setEditingMaterialId(id);
setEditModalVisible(true);
};
// 编辑素材
const handleEditMaterial = (id: number) => {
setEditingMaterialId(id);
setEditModalVisible(true);
};
// 编辑弹窗成功回调
const handleEditSuccess = () => {
fetchMaterials(); // 重新获取数据
};
// 编辑弹窗成功回调
const handleEditSuccess = () => {
fetchMaterials(); // 重新获取数据
};
// 搜索处理函数
const handleSearch = (value: string) => {
fetchMaterials({ keyword: value });
};
// 搜索处理函数
const handleSearch = (value: string) => {
fetchMaterials({ keyword: value });
};
// 组件挂载时获取数据
useEffect(() => {
fetchMaterials();
}, []);
// 组件挂载时获取数据
useEffect(() => {
fetchMaterials();
}, []);
return (
<div className={styles.materialContent}>
<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.materialContent}>
<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>
<div className={styles.materialGrid}>
{loading ? (
<div className={styles.loading}>...</div>
) : materialsList.length === 0 ? (
<div className={styles.empty}></div>
) : (
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%",
<div className={styles.materialGrid}>
{loading ? (
<div className={styles.loading}>...</div>
) : materialsList.length === 0 ? (
<div className={styles.empty}></div>
) : (
materialsList.map(item => (
<Card
key={item.id}
className={styles.materialCard}
hoverable
actions={[
<Button
key="edit"
type="text"
icon={<FormOutlined />}
onClick={e => {
e.stopPropagation();
handleEditMaterial(item.id);
}}
>
<img
src={item.cover}
alt={item.title}
</Button>,
<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={{
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",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
color: "#999",
flexDirection: "column",
}}
>
{item.type}
{getTypeIcon(item.type)}
<span style={{ marginTop: "8px", fontSize: "12px" }}>
{item.type}
</span>
</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>
</Card>
))
)}
</div>
{/* 编辑弹窗 */}
<MaterialModal
visible={editModalVisible}
mode="edit"
materialId={editingMaterialId}
onCancel={() => {
setEditModalVisible(false);
setEditingMaterialId(null);
}}
onSuccess={handleEditSuccess}
/>
</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>
</Card>
))
)}
</div>
{/* 编辑弹窗 */}
<MaterialModal
visible={editModalVisible}
mode="edit"
materialId={editingMaterialId}
onCancel={() => {
setEditModalVisible(false);
setEditingMaterialId(null);
}}
onSuccess={handleEditSuccess}
/>
</div>
);
},
);
MaterialManagement.displayName = "MaterialManagement";
export default MaterialManagement;

View File

@@ -13,7 +13,8 @@ import {
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
import VideoUpload from "@/components/Upload/VideoUpload";
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 { Option } = Select;
@@ -93,7 +94,7 @@ const ContentManager: React.FC<ContentManagerProps> = ({
// 根据新类型重置数据
let newData: any;
if (newType === "link") {
newData = "";
newData = { title: "", url: "", cover: "" };
} else {
newData = "";
}
@@ -181,7 +182,10 @@ const ContentManager: React.FC<ContentManagerProps> = ({
return (
<VideoUpload
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}
showPreview={true}
/>
@@ -190,7 +194,10 @@ const ContentManager: React.FC<ContentManagerProps> = ({
return (
<FileUpload
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}
showPreview={true}
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
@@ -198,23 +205,48 @@ const ContentManager: React.FC<ContentManagerProps> = ({
);
case "audio":
return (
<FileUpload
<AudioUpload
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}
showPreview={true}
acceptTypes={["mp3", "wav", "aac", "m4a", "ogg"]}
/>
);
case "link": {
const linkData = (item.data as LinkData) || {
title: "",
url: "",
cover: "",
};
return (
<div>
<Input
value={item.data as string}
onChange={e => updateItemData(index, e.target.value)}
value={linkData.title}
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"
style={{ marginBottom: 8 }}
/>
<ImageUpload
count={1}
accept="image/*"
value={linkData.cover ? [linkData.cover] : []}
onChange={urls =>
updateItemData(index, { ...linkData, cover: urls[0] || "" })
}
/>
</div>
);
}

View File

@@ -55,14 +55,17 @@ const MaterialModal: React.FC<MaterialModalProps> = ({
[form],
);
// 当弹窗打开且为编辑模式时,获取详情
// 当弹窗打开时处理数据
useEffect(() => {
if (visible && mode === "edit" && materialId) {
fetchMaterialDetails(materialId);
} else if (visible && mode === "add") {
// 添加模式时重置表单
form.resetFields();
setContentItems([]);
if (visible) {
if (mode === "edit" && materialId) {
// 编辑模式:获取详情
fetchMaterialDetails(materialId);
} else if (mode === "add") {
// 添加模式:重置表单
form.resetFields();
setContentItems([]);
}
}
}, [visible, mode, materialId, fetchMaterialDetails, form]);