127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { Alert, Button, Empty, Image, Space, Typography, message } from 'antd';
|
|
import { CopyOutlined, LinkOutlined } from '@ant-design/icons';
|
|
|
|
const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
|
const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, '');
|
|
|
|
export function resolveResourceUrl(url?: string | null): string {
|
|
if (!url) return '';
|
|
const value = String(url).trim();
|
|
if (!value) return '';
|
|
if (/^(https?:)?\/\//i.test(value) || /^(blob|data):/i.test(value)) return value;
|
|
return `${RESOURCE_BASE}${value.startsWith('/') ? value : `/${value}`}`;
|
|
}
|
|
|
|
interface MediaPreviewProps {
|
|
url?: string | null;
|
|
type: 'image' | 'video';
|
|
height?: number;
|
|
emptyDescription?: string;
|
|
errorDescription?: string;
|
|
}
|
|
|
|
const MediaPreview: React.FC<MediaPreviewProps> = ({
|
|
url,
|
|
type,
|
|
height = 260,
|
|
emptyDescription = '暂无资源',
|
|
errorDescription = '资源加载失败,可能文件不存在、签名过期或访问权限受限。',
|
|
}) => {
|
|
const resolvedUrl = useMemo(() => resolveResourceUrl(url), [url]);
|
|
const [loadFailed, setLoadFailed] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setLoadFailed(false);
|
|
}, [resolvedUrl]);
|
|
|
|
const copyUrl = async () => {
|
|
if (!resolvedUrl) return;
|
|
try {
|
|
await navigator.clipboard.writeText(resolvedUrl);
|
|
message.success('资源地址已复制');
|
|
} catch {
|
|
message.error('复制失败,请手动复制');
|
|
}
|
|
};
|
|
|
|
const tools = resolvedUrl ? (
|
|
<Space wrap style={{ marginTop: 8 }}>
|
|
<Button size="small" icon={<CopyOutlined />} onClick={copyUrl}>复制地址</Button>
|
|
<Button size="small" icon={<LinkOutlined />} onClick={() => window.open(resolvedUrl, '_blank', 'noopener,noreferrer')}>新窗口打开</Button>
|
|
<Typography.Text copyable={{ text: resolvedUrl }} type="secondary" style={{ maxWidth: 460 }} ellipsis>
|
|
{resolvedUrl}
|
|
</Typography.Text>
|
|
</Space>
|
|
) : null;
|
|
|
|
if (!resolvedUrl) {
|
|
return (
|
|
<div>
|
|
<div
|
|
style={{
|
|
height,
|
|
borderRadius: 12,
|
|
border: '1px dashed #cbd5e1',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
background: '#f8fafc',
|
|
}}
|
|
>
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyDescription} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{loadFailed ? (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
message="资源预览失败"
|
|
description={errorDescription}
|
|
style={{ marginBottom: 8 }}
|
|
/>
|
|
) : null}
|
|
|
|
{type === 'image' ? (
|
|
<Image
|
|
src={resolvedUrl}
|
|
onError={() => setLoadFailed(true)}
|
|
style={{
|
|
width: '100%',
|
|
maxHeight: height,
|
|
objectFit: 'contain',
|
|
borderRadius: 12,
|
|
background: '#f8fafc',
|
|
border: '1px solid #f1f5f9',
|
|
}}
|
|
fallback=""
|
|
preview={!loadFailed}
|
|
/>
|
|
) : (
|
|
<video
|
|
src={resolvedUrl}
|
|
controls
|
|
preload="metadata"
|
|
onError={() => setLoadFailed(true)}
|
|
style={{
|
|
width: '100%',
|
|
maxHeight: height,
|
|
borderRadius: 12,
|
|
background: '#0f172a',
|
|
border: '1px solid #f1f5f9',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{tools}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MediaPreview;
|