上传素材管控-历史素材success
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { AudioOutlined, CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd';
|
||||
import { getUploadResourceHistoryItems } from '../../api';
|
||||
import type { UploadResourceHistoryItem } from '../../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type MediaTypeFilter = '' | 'image' | 'video' | 'audio';
|
||||
|
||||
interface UploadResourceHistoryPickerProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (items: UploadResourceHistoryItem[]) => void;
|
||||
allowedTypes?: Array<'image' | 'video' | 'audio'>;
|
||||
selectedIds?: string[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const today = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const buildPreviewUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
if (/^(https?:|data:|blob:)/i.test(url)) return url;
|
||||
const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '');
|
||||
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
const typeIcon = (type: string) => {
|
||||
if (type === 'video') return <VideoCameraOutlined />;
|
||||
if (type === 'audio') return <AudioOutlined />;
|
||||
return <PictureOutlined />;
|
||||
};
|
||||
|
||||
const typeLabel = (type: string) => {
|
||||
if (type === 'video') return '视频';
|
||||
if (type === 'audio') return '音频';
|
||||
return '图片';
|
||||
};
|
||||
|
||||
const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onSelect,
|
||||
allowedTypes,
|
||||
selectedIds = [],
|
||||
title = '选择历史上传素材',
|
||||
}) => {
|
||||
const [date, setDate] = useState(today());
|
||||
const [resourceType, setResourceType] = useState<MediaTypeFilter>('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [items, setItems] = useState<UploadResourceHistoryItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [checkedMap, setCheckedMap] = useState<Map<string, UploadResourceHistoryItem>>(new Map());
|
||||
|
||||
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||||
const effectiveAllowedTypes = useMemo(() => new Set(allowedTypes || ['image', 'video', 'audio']), [allowedTypes]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!open) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getUploadResourceHistoryItems(date, {
|
||||
resourceType: resourceType || undefined,
|
||||
keyword: keyword || undefined,
|
||||
page,
|
||||
pageSize,
|
||||
scene: 'picker',
|
||||
});
|
||||
const filtered = (res.items || []).filter((item) => effectiveAllowedTypes.has(item.resourceType));
|
||||
setItems(filtered);
|
||||
setTotal(res.total || 0);
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '加载历史上传素材失败');
|
||||
setItems([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [date, effectiveAllowedTypes, keyword, open, page, pageSize, resourceType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) load();
|
||||
}, [open, load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setCheckedMap(new Map());
|
||||
}, [open]);
|
||||
|
||||
const toggle = (item: UploadResourceHistoryItem) => {
|
||||
if (selectedIdSet.has(item.id)) {
|
||||
message.warning('该素材已经在参考内容中');
|
||||
return;
|
||||
}
|
||||
setCheckedMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
if (next.has(item.id)) next.delete(item.id);
|
||||
else next.set(item.id, item);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
const selected = Array.from(checkedMap.values());
|
||||
if (selected.length === 0) {
|
||||
message.warning('请先选择历史素材');
|
||||
return;
|
||||
}
|
||||
onSelect(selected);
|
||||
setCheckedMap(new Map());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const typeOptions = useMemo(() => {
|
||||
const all = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '音频', value: 'audio' },
|
||||
];
|
||||
return all.filter((opt) => !opt.value || effectiveAllowedTypes.has(opt.value as any));
|
||||
}, [effectiveAllowedTypes]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width={920}
|
||||
centered
|
||||
destroyOnHidden
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onClose}>取消</Button>,
|
||||
<Button key="ok" type="primary" onClick={confirm} style={{ background: '#8b5cf6' }}>
|
||||
应用选择 ({checkedMap.size})
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Space wrap style={{ width: '100%', marginBottom: 16 }}>
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => {
|
||||
setDate(e.target.value || today());
|
||||
setPage(1);
|
||||
}}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Select
|
||||
value={resourceType}
|
||||
options={typeOptions}
|
||||
style={{ width: 120 }}
|
||||
onChange={(value) => {
|
||||
setResourceType(value);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
value={keyword}
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索文件名"
|
||||
style={{ width: 220 }}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
setPage(1);
|
||||
load();
|
||||
}}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
||||
</Space>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
{items.length === 0 ? (
|
||||
<Empty description="暂无可复用的历史上传素材" style={{ padding: '48px 0' }} />
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14, minHeight: 260 }}>
|
||||
{items.map((item) => {
|
||||
const checked = checkedMap.has(item.id);
|
||||
const disabled = selectedIdSet.has(item.id);
|
||||
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => !disabled && toggle(item)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: 14,
|
||||
border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0',
|
||||
background: disabled ? '#f8fafc' : '#fff',
|
||||
opacity: disabled ? 0.55 : 1,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
overflow: 'hidden',
|
||||
boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)',
|
||||
}}
|
||||
>
|
||||
<div style={{ height: 112, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{item.resourceType === 'image' ? (
|
||||
<img src={preview} alt={item.fileName || item.id} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
) : item.resourceType === 'video' ? (
|
||||
<video src={preview} muted style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<div style={{ width: 58, height: 58, borderRadius: 18, background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 24 }}>
|
||||
<AudioOutlined />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: 10 }}>
|
||||
<Space size={6} style={{ marginBottom: 6 }}>
|
||||
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
|
||||
{item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null}
|
||||
</Space>
|
||||
<Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}>
|
||||
{item.fileName || item.id}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{item.moduleLabel || '普通上传'}</Text>
|
||||
</div>
|
||||
{checked && (
|
||||
<div style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, background: '#8b5cf6', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
)}
|
||||
{disabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag>已选</Tag></div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
onChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage);
|
||||
setPageSize(nextSize);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadResourceHistoryPicker;
|
||||
Reference in New Issue
Block a user