feat: 内容管理

This commit is contained in:
2025-10-29 17:10:53 +08:00
parent 476ee7a754
commit 4c628fee22
67 changed files with 8210 additions and 4494 deletions

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