feat: 内容管理
This commit is contained in:
@@ -61,7 +61,6 @@ const AudioUploader: React.FC<AudioUploaderProps> = ({
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 实际的后端接口上传
|
||||
const uploadToServer = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
|
||||
147
src/components/Upload/UploadImages/index.tsx
Normal file
147
src/components/Upload/UploadImages/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { GetProp, UploadFile, UploadProps } from 'antd';
|
||||
import { Image, message, Spin, Upload } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { uploadImage } from '@/services/infra/media';
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
// accept: .doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document image/*,.pdf
|
||||
const UploadImages: React.FC<{
|
||||
value?: string;
|
||||
onChange?: (value: string | string[]) => void;
|
||||
multiple?: boolean;
|
||||
accept?: string;
|
||||
maxCount?: number;
|
||||
}> = (props) => {
|
||||
const {
|
||||
value,
|
||||
multiple = false,
|
||||
maxCount = 1,
|
||||
accept = 'image/png,image/jpeg',
|
||||
onChange,
|
||||
} = props;
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setFileList([{ uid: '-1', url: value, status: 'done', name: value }]);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
}, [value]);
|
||||
const beforeUpload = (file: FileType) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('仅支持.jpg .png 格式!');
|
||||
}
|
||||
// const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
// if (!isLt2M) {
|
||||
// message.error('Image must smaller than 2MB!');
|
||||
// }
|
||||
return isJpgOrPng;
|
||||
};
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj as FileType);
|
||||
}
|
||||
|
||||
setPreviewImage(file.url || (file.preview as string));
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleRemove = (file: UploadFile): boolean => {
|
||||
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||
const newUrl = newFileList.map((item) => item.url) as string[];
|
||||
onChange?.(newUrl[0]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</button>
|
||||
);
|
||||
const uploadFile = useCallback(async (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
uploadImage(formData).then((res) => {
|
||||
if (res) {
|
||||
resolve(res);
|
||||
} else {
|
||||
reject(new Error('上传失败:未返回有效的URL'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const handleLoadImage: UploadProps['customRequest'] = async (option) => {
|
||||
const { file, onSuccess, onError, onProgress } = option;
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
// 模拟进度更新
|
||||
onProgress?.({ percent: 10 });
|
||||
// 调用后端接口
|
||||
const url = await uploadFile(file as File);
|
||||
onProgress?.({ percent: 100 });
|
||||
if (url) {
|
||||
onChange?.(url);
|
||||
onSuccess?.({ url });
|
||||
message.success('上传成功');
|
||||
} else {
|
||||
throw new Error('上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
onError?.(error as Error);
|
||||
message.error(`上传失败: ${(error as Error).message}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Spin spinning={uploading}>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onPreview={handlePreview}
|
||||
onRemove={handleRemove}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={handleLoadImage}
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
>
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
{previewImage && (
|
||||
<Image
|
||||
wrapperStyle={{ display: 'none' }}
|
||||
preview={{
|
||||
visible: previewOpen,
|
||||
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||
afterOpenChange: (visible) => !visible && setPreviewImage(''),
|
||||
}}
|
||||
src={previewImage}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadImages;
|
||||
192
src/components/Upload/UploadVideo/index.tsx
Normal file
192
src/components/Upload/UploadVideo/index.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { GetProp, UploadFile, UploadProps } from 'antd';
|
||||
import { Modal, message, Spin, Upload } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import VideoThumbnail from 'react-video-thumbnail';
|
||||
import { uploadImage } from '@/services/infra/media';
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
interface CustomUploadFile extends UploadFile {
|
||||
thumbUrl?: string;
|
||||
preview?: string;
|
||||
}
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
// accept: .doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document image/*,.pdf
|
||||
const UploadVideo: React.FC<{
|
||||
value?: string;
|
||||
onChange?: (value: string | string[]) => void;
|
||||
multiple?: boolean;
|
||||
accept?: string;
|
||||
maxCount?: number;
|
||||
}> = (props) => {
|
||||
const {
|
||||
value,
|
||||
multiple = false,
|
||||
maxCount = 1,
|
||||
accept = 'video/*',
|
||||
onChange,
|
||||
} = props;
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewVideo, setPreviewVideo] = useState('');
|
||||
const [fileList, setFileList] = useState<CustomUploadFile[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setFileList([
|
||||
{ uid: '-1', url: value, thumbUrl: value, status: 'done', name: value },
|
||||
]);
|
||||
} else {
|
||||
setFileList([]);
|
||||
}
|
||||
}, [value]);
|
||||
const beforeUpload = (file: FileType) => {
|
||||
const isJpgOrPng = file.type === 'video/mp4' || file.type === 'video/mov';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('仅支持.mp4 .mov 格式!');
|
||||
}
|
||||
// const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
// if (!isLt2M) {
|
||||
// message.error('Image must smaller than 2MB!');
|
||||
// }
|
||||
return isJpgOrPng;
|
||||
};
|
||||
|
||||
// // 生成缩略图组件
|
||||
// const renderThumbnail = (videoUrl: string) => {
|
||||
// return (
|
||||
// <div style={{ width: "100%", height: "100%" }}>
|
||||
// <VideoThumbnail
|
||||
// videoUrl={videoUrl}
|
||||
// width={120}
|
||||
// height={90}
|
||||
// snapshotAt={1}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
const handlePreview = async (file: CustomUploadFile) => {
|
||||
if (!file.url && !file.preview && file.originFileObj) {
|
||||
file.preview = await getBase64(file.originFileObj);
|
||||
}
|
||||
setPreviewVideo(file.url || file.preview || '');
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
// const customItemRender: UploadProps["itemRender"] = (
|
||||
// _,
|
||||
// file: CustomUploadFile
|
||||
// ) => {
|
||||
// return (
|
||||
// <div className="ant-upload-list-item-container">
|
||||
// <div className="ant-upload-list-item-card">
|
||||
// <div className="video-thumbnail" onClick={() => handlePreview(file)}>
|
||||
// {file.thumbUrl ? (
|
||||
// renderThumbnail("https://petshy.tashowz.com" + file.thumbUrl)
|
||||
// ) : (
|
||||
// <div className="video-icon">
|
||||
// <PlayCircleOutlined
|
||||
// style={{ fontSize: 48, color: "#1890ff" }}
|
||||
// />
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// <div className="ant-upload-list-item-name">{file.name}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
const handleRemove = (file: UploadFile): boolean => {
|
||||
const newFileList = fileList.filter((item) => item.uid !== file.uid);
|
||||
const newUrl = newFileList.map((item) => item.url) as string[];
|
||||
onChange?.(newUrl[0]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const uploadButton = (
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>上传</div>
|
||||
</button>
|
||||
);
|
||||
const uploadFile = useCallback(async (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
uploadImage(formData).then((res) => {
|
||||
if (res) {
|
||||
resolve(res);
|
||||
} else {
|
||||
reject(new Error('上传失败:未返回有效的URL'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const handleLoadImage: UploadProps['customRequest'] = async (option) => {
|
||||
const { file, onSuccess, onError, onProgress } = option;
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
// 模拟进度更新
|
||||
onProgress?.({ percent: 10 });
|
||||
// 调用后端接口
|
||||
const url = await uploadFile(file as File);
|
||||
onProgress?.({ percent: 100 });
|
||||
if (url) {
|
||||
onChange?.(url);
|
||||
onSuccess?.({ url });
|
||||
message.success('上传成功');
|
||||
} else {
|
||||
throw new Error('上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
onError?.(error as Error);
|
||||
message.error(`上传失败: ${(error as Error).message}`);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Spin spinning={uploading}>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={fileList}
|
||||
onPreview={handlePreview}
|
||||
// itemRender={customItemRender}
|
||||
onRemove={handleRemove}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={handleLoadImage}
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
>
|
||||
{fileList.length >= maxCount ? null : uploadButton}
|
||||
</Upload>
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
title="视频预览"
|
||||
footer={null}
|
||||
onCancel={() => setPreviewOpen(false)}
|
||||
width={800}
|
||||
>
|
||||
<video controls style={{ width: '100%', height: '500px' }} autoPlay>
|
||||
<source src={previewVideo} type="video/mp4" />
|
||||
<track kind="captions" src="" srcLang="zh" label="Chinese" />
|
||||
您的浏览器不支持视频预览功能
|
||||
</video>
|
||||
</Modal>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadVideo;
|
||||
Reference in New Issue
Block a user