Files
video-gen/video-gen-app/src/pages/MaterialListPage.tsx
T

401 lines
12 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, Input, Pagination, Typography, Select, App } from 'antd';
import { useNavigate } from 'react-router-dom';
import { FolderOpenOutlined, EyeOutlined } from '@ant-design/icons';
import { getResourcesMaterialList } from '../api';
import PreResultDisplay from '../components/PreResultDisplay';
// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
// 安全拼接URL
const buildUrl = (path: string): string => {
if (!path) return '';
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return `${cleanBase}/${cleanPath}`;
};
// 资源类型配置
const resourceTypeConfig: Record<string, { label: string; color: string }> = {
image: { label: '图片', color: 'green' },
video: { label: '视频', color: 'blue' },
};
// 状态配置
const statusConfig: Record<string, { label: string; color: string }> = {
'FAILED': { label: '失败', color: 'error' },
'PENDING': { label: '处理中', color: 'processing' },
'SUCCESS': { label: '成功', color: 'success' },
};
interface MaterialResource {
fileName: string;
resourceUrl: string;
remoteUrl: string;
storageType: string;
storagePath: string;
fileSizeBytes: number;
sourceModel: string;
sourceModelModule: string;
sourceId: string;
engineId: string;
engineType: string;
provider: string;
modelName: string;
generatedAt: string;
resourceMonth: string;
createdAt: string;
}
interface MaterialData {
id: string;
oauth_idId: string;
advertiserId: string;
targetTable: string;
targetId: string;
materialId: string;
uploadId: string;
resourceType: string;
userId: string;
taskId: string;
note: string;
status: string;
preResult: string;
preTestTemplateId: string;
createdAt: string;
updatedAt: string;
resource: MaterialResource;
}
const MaterialListPage: React.FC = () => {
const { message } = App.useApp();
const navigate = useNavigate();
const [materials, setMaterials] = useState<MaterialData[]>([]);
const [listLoading, setListLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [searchParams, setSearchParams] = useState({
advertiser_id: '',
material_id: '',
upload_id: '',
file_name: '',
resource_type: undefined as string | undefined,
});
useEffect(() => {
loadMaterialList();
}, []);
const loadMaterialList = async (page = 1, pageSizeNum = 10, params = searchParams) => {
setListLoading(true);
try {
const response = await getResourcesMaterialList({
advertiser_id: params.advertiser_id || undefined,
material_id: params.material_id || undefined,
upload_id: params.upload_id || undefined,
file_name: params.file_name || undefined,
resource_type: params.resource_type,
page,
page_size: pageSizeNum,
});
if (response?.code === 0) {
setMaterials(response.data || []);
setTotal(response.total || 0);
} else {
setMaterials([]);
setTotal(0);
}
} catch (error) {
message.error('获取素材列表失败');
console.error('获取素材列表失败:', error);
} finally {
setListLoading(false);
}
};
const columns = [
// {
// title: 'ID',
// dataIndex: 'id',
// key: 'id',
// width: 100,
// },
{
title: '广告主ID',
dataIndex: 'advertiserId',
key: 'advertiserId',
width: 100,
},
{
title: '素材ID',
dataIndex: 'materialId',
key: 'materialId',
width: 120,
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
},
{
title: '上传平台Id',
dataIndex: 'uploadId',
key: 'uploadId',
width: 120,
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
},
{
title: '资源类型',
dataIndex: 'resourceType',
key: 'resourceType',
width: 100,
render: (text: string) => {
const config = resourceTypeConfig[text];
return <Tag color={config?.color}>{config?.label || text}</Tag>;
},
},
{
title: '预览',
key: 'preview',
width: 100,
render: (_: unknown, record: MaterialData) => {
const url = record.resource?.resourceUrl || record.resource?.remoteUrl;
if (url) {
return (
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => window.open(buildUrl(url), '_blank')}
size="small"
>
查看
</Button>
);
}
return <Tag>-</Tag>;
},
},
{
title: '文件名',
dataIndex: ['resource', 'fileName'],
key: 'fileName',
width: 160,
ellipsis: true,
render: (text: string) => <span style={{ color: '#1e293b' }}>{text || '-'}</span>,
},
{
title: '文件大小',
dataIndex: ['resource', 'fileSizeBytes'],
key: 'fileSizeBytes',
ellipsis: true,
render: (text: number) =>{
return `${(text / 1024 / 1024).toFixed(2)} MB`
// return `${text} 字`
// return `${text} 字节`
}
},
// {
// title: '来源模型',
// dataIndex: ['resource', 'sourceModel'],
// key: 'sourceModel',
// width: 120,
// ellipsis: true,
// render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
// },
// {
// title: '供应商',
// dataIndex: ['resource', 'provider'],
// key: 'provider',
// width: 100,
// ellipsis: true,
// render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
// },
// {
// title: '模型名称',
// dataIndex: ['resource', 'modelName'],
// key: 'modelName',
// width: 120,
// ellipsis: true,
// render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
// },
{
title: '前测状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (text: string) => {
const config = statusConfig[text];
return <Tag color={config?.color}>{config?.label || text || '-'}</Tag>;
},
},
{
title: '前测结果',
dataIndex: 'preResult',
key: 'preResult',
width: 120,
render: (text: string) => <PreResultDisplay preResult={text} />,
},
{
title: '备注',
dataIndex: 'note',
key: 'note',
width: 150,
render: (text: string) => (
<Input.TextArea
value={text || ''}
readOnly
autoSize={{ minRows: 1, maxRows: 4 }}
style={{ color: '#94a3b8', resize: 'none', border: 'none', background: 'transparent', padding: 0 }}
placeholder="-"
/>
),
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
width: 120,
// ellipsis: true,
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
},
{
title: '操作',
fixed: 'right' as const,
key: 'action',
render: (_: unknown, record) => (
<Button
type="link"
onClick={() => navigate(`/consume?advertiserId=${record.advertiserId}`)}
style={{ color: '#6366f1', padding: 0 }}
>
查看消耗
</Button>
),
},
// {
// title: '更新时间',
// dataIndex: 'updatedAt',
// key: 'updatedAt',
// width: 160,
// render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
// },
];
const tableData = materials.map((item, index) => ({
...item,
index: index + 1,
key: item.id,
}));
return (
<div style={{ minHeight: '94vh' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<FolderOpenOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<Typography.Text strong style={{ fontSize: 16 }}>素材列表</Typography.Text>
</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
<Input
placeholder="广告主ID"
value={searchParams.advertiser_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="素材ID"
value={searchParams.material_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, material_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="上传ID"
value={searchParams.upload_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, upload_id: e.target.value }))}
style={{ width: 160 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Input
placeholder="文件名"
value={searchParams.file_name}
onChange={(e) => setSearchParams(prev => ({ ...prev, file_name: e.target.value }))}
style={{ width: 140 }}
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
/>
<Select
placeholder="资源类型"
value={searchParams.resource_type}
onChange={(value) => setSearchParams(prev => ({ ...prev, resource_type: value }))}
style={{ width: 120 }}
allowClear
options={[
{ value: 'image', label: '图片' },
{ value: 'video', label: '视频' },
]}
/>
<Button
type="primary"
onClick={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
>
搜索
</Button>
<Button
onClick={() => {
setSearchParams({ advertiser_id: '', material_id: '', upload_id: '', file_name: '', resource_type: undefined });
setCurrentPage(1);
loadMaterialList(1, pageSize);
}}
>
重置
</Button>
</div>
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
loading={listLoading}
pagination={false}
rowKey="id"
bordered={false}
scroll={{ x: 'max-content' }}
/>
<div style={{ padding: '16px', textAlign: 'right' }}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showSizeChanger
showTotal={(total) => `共 ${total} 条记录`}
onChange={(page, size) => {
setCurrentPage(page);
setPageSize(size);
loadMaterialList(page, size);
}}
size="small"
/>
</div>
</div>
</div>
);
};
export default MaterialListPage;