181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import { useState, type ReactNode } from 'react';
|
||
import { Button, Image, Input, Modal, Space, Typography, Upload, message } from 'antd';
|
||
import { EyeOutlined, FilePdfOutlined, UploadOutlined } from '@ant-design/icons';
|
||
import type { UploadProps } from 'antd';
|
||
import { uploadFileToOss, type OssMediaType, type UploadFileResult } from '../lib/upload';
|
||
|
||
type OssUploadProps = {
|
||
value?: string;
|
||
onChange?: (url: string) => void;
|
||
onUploaded?: (result: UploadFileResult) => void;
|
||
bizType: string;
|
||
mediaType?: OssMediaType;
|
||
accept?: string;
|
||
maxSizeMb?: number;
|
||
placeholder?: string;
|
||
uploadHint?: ReactNode;
|
||
};
|
||
|
||
function isImageUrl(url: string) {
|
||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(url);
|
||
}
|
||
|
||
function isPdfUrl(url: string) {
|
||
return /\.pdf(\?|#|$)/i.test(url);
|
||
}
|
||
|
||
function formatUploadImageMessage(result: UploadFileResult): string {
|
||
const img = result.image;
|
||
if (!img) return '上传成功';
|
||
const sizeLabel = `${img.width}×${img.height}`;
|
||
if (img.compressed && img.originalWidth && img.originalHeight) {
|
||
const original = `${img.originalWidth}×${img.originalHeight}`;
|
||
if (original !== sizeLabel) {
|
||
return `上传成功(${sizeLabel},已由 ${original} 自动压缩)`;
|
||
}
|
||
}
|
||
return `上传成功(${sizeLabel})`;
|
||
}
|
||
|
||
function formatUploadImageHint(result: UploadFileResult): string | null {
|
||
const img = result.image;
|
||
if (!img) return null;
|
||
const sizeLabel = `${img.width}×${img.height}`;
|
||
if (img.compressed && img.originalWidth && img.originalHeight) {
|
||
const original = `${img.originalWidth}×${img.originalHeight}`;
|
||
if (original !== sizeLabel) {
|
||
return `${sizeLabel}(已由 ${original} 自动压缩)`;
|
||
}
|
||
}
|
||
return sizeLabel;
|
||
}
|
||
|
||
export default function OssUpload({
|
||
value,
|
||
onChange,
|
||
onUploaded,
|
||
bizType,
|
||
mediaType = 'IMAGE',
|
||
accept,
|
||
placeholder = '上传后自动填入,或手动粘贴 URL',
|
||
uploadHint,
|
||
}: OssUploadProps) {
|
||
const [uploading, setUploading] = useState(false);
|
||
const [pdfPreviewOpen, setPdfPreviewOpen] = useState(false);
|
||
const [imageMetaHint, setImageMetaHint] = useState<string | null>(null);
|
||
|
||
const resolvedAccept =
|
||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? undefined : 'image/*');
|
||
|
||
const customRequest: UploadProps['customRequest'] = async ({ file, onSuccess, onError }) => {
|
||
const raw = file as File;
|
||
setUploading(true);
|
||
try {
|
||
const result = await uploadFileToOss(raw, { bizType, mediaType });
|
||
onChange?.(result.url);
|
||
onUploaded?.(result);
|
||
const successMessage = formatUploadImageMessage(result);
|
||
setImageMetaHint(formatUploadImageHint(result));
|
||
message.success(successMessage);
|
||
onSuccess?.(result);
|
||
} catch (e) {
|
||
const err = e instanceof Error ? e : new Error('上传失败');
|
||
message.error(err.message);
|
||
onError?.(err);
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
};
|
||
|
||
const filePreview =
|
||
value && mediaType === 'FILE' ? (
|
||
isImageUrl(value) ? (
|
||
<Image src={value} width={120} height={120} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||
) : isPdfUrl(value) ? (
|
||
<Space direction="vertical" size={8}>
|
||
<div
|
||
style={{
|
||
width: 120,
|
||
height: 120,
|
||
borderRadius: 4,
|
||
border: '1px solid #f0f0f0',
|
||
background: '#fafafa',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
color: '#cf1322',
|
||
}}
|
||
>
|
||
<FilePdfOutlined style={{ fontSize: 36 }} />
|
||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>PDF 合同</span>
|
||
</div>
|
||
<Space wrap>
|
||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => setPdfPreviewOpen(true)}>
|
||
预览
|
||
</Button>
|
||
<Button type="link" size="small" href={value} target="_blank" rel="noreferrer">
|
||
新窗口打开
|
||
</Button>
|
||
</Space>
|
||
<Modal
|
||
title="签约合同预览"
|
||
open={pdfPreviewOpen}
|
||
onCancel={() => setPdfPreviewOpen(false)}
|
||
footer={null}
|
||
width="90vw"
|
||
styles={{ body: { height: '75vh', padding: 0 } }}
|
||
destroyOnClose
|
||
>
|
||
<iframe title="合同 PDF 预览" src={value} style={{ width: '100%', height: '100%', border: 0 }} />
|
||
</Modal>
|
||
</Space>
|
||
) : (
|
||
<Button type="link" href={value} target="_blank" rel="noreferrer" style={{ paddingLeft: 0 }}>
|
||
打开已上传文件
|
||
</Button>
|
||
)
|
||
) : null;
|
||
|
||
return (
|
||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||
{value && mediaType === 'IMAGE' && (
|
||
<Image src={value} width={120} height={120} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||
)}
|
||
{value && mediaType === 'VIDEO' && (
|
||
<video src={value} controls style={{ maxWidth: '100%', maxHeight: 160, borderRadius: 4 }} />
|
||
)}
|
||
{filePreview}
|
||
{uploadHint ? (
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
{uploadHint}
|
||
</Typography.Text>
|
||
) : null}
|
||
{imageMetaHint ? (
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
当前底图:{imageMetaHint}
|
||
</Typography.Text>
|
||
) : null}
|
||
<Space wrap>
|
||
<Upload
|
||
accept={resolvedAccept}
|
||
showUploadList={false}
|
||
customRequest={customRequest}
|
||
disabled={uploading}
|
||
>
|
||
<Button icon={<UploadOutlined />} loading={uploading}>
|
||
{mediaType === 'VIDEO' ? '上传视频' : mediaType === 'FILE' ? '上传文件' : '上传图片'}
|
||
</Button>
|
||
</Upload>
|
||
</Space>
|
||
<Input
|
||
value={value}
|
||
placeholder={placeholder}
|
||
onChange={(e) => onChange?.(e.target.value)}
|
||
allowClear
|
||
/>
|
||
</Space>
|
||
);
|
||
}
|