Files
dukang/apps/admin-web/src/components/OssUpload.tsx
T
2026-09-03 16:04:10 +08:00

181 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}