401 lines
12 KiB
TypeScript
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; |