134 lines
6.1 KiB
TypeScript
134 lines
6.1 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Button, Card, Input, Space, Table, Tag, Typography, message } from 'antd';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
|
import { adminGetPrivatePortraitAssets, adminGetPrivatePortraitProjects } from '../api';
|
|
import type { PrivatePortraitAsset, PrivatePortraitProject } from '../types';
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
const statusColor = (status?: string) => {
|
|
if (status === 'Active' || status === 'active') return 'green';
|
|
if (status === 'Processing') return 'blue';
|
|
if (status === 'Failed' || status === 'failed' || status === 'delete_failed') return 'red';
|
|
if (status?.includes('deleted')) return 'default';
|
|
return 'default';
|
|
};
|
|
|
|
const AdminPrivatePortraitProjects: React.FC = () => {
|
|
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]);
|
|
const [assets, setAssets] = useState<PrivatePortraitAsset[]>([]);
|
|
const [projectTotal, setProjectTotal] = useState(0);
|
|
const [assetTotal, setAssetTotal] = useState(0);
|
|
const [loadingProjects, setLoadingProjects] = useState(false);
|
|
const [loadingAssets, setLoadingAssets] = useState(false);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string | undefined>();
|
|
const [projectPage, setProjectPage] = useState(1);
|
|
const [assetPage, setAssetPage] = useState(1);
|
|
|
|
const loadProjects = async () => {
|
|
setLoadingProjects(true);
|
|
try {
|
|
const res = await adminGetPrivatePortraitProjects({ keyword: keyword.trim() || undefined, page: projectPage, pageSize: 20 });
|
|
setProjects(res.items || []);
|
|
setProjectTotal(res.total || 0);
|
|
} catch (err: any) {
|
|
message.error(err?.message || '加载真人素材项目失败');
|
|
} finally {
|
|
setLoadingProjects(false);
|
|
}
|
|
};
|
|
|
|
const loadAssets = async () => {
|
|
setLoadingAssets(true);
|
|
try {
|
|
const res = await adminGetPrivatePortraitAssets({ projectId: selectedProjectId, keyword: keyword.trim() || undefined, page: assetPage, pageSize: 20 });
|
|
setAssets(res.items || []);
|
|
setAssetTotal(res.total || 0);
|
|
} catch (err: any) {
|
|
message.error(err?.message || '加载真人素材失败');
|
|
} finally {
|
|
setLoadingAssets(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { loadProjects(); }, [projectPage]);
|
|
useEffect(() => { loadAssets(); }, [selectedProjectId, assetPage]);
|
|
|
|
const projectColumns: ColumnsType<PrivatePortraitProject> = [
|
|
{ title: '项目名称', dataIndex: 'name', width: 180, render: (v, r) => <Button type="link" onClick={() => { setSelectedProjectId(r.id); setAssetPage(1); }}>{v}</Button> },
|
|
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => <Text code>{v || '-'}</Text> },
|
|
{ title: '状态', dataIndex: 'status', width: 100, render: (v) => <Tag color={statusColor(v)}>{v}</Tag> },
|
|
{ title: 'ProjectName', dataIndex: 'remoteProjectName', width: 240, render: (v) => <Text code copyable>{v || '-'}</Text> },
|
|
{ title: '素材数', dataIndex: 'assetCount', width: 100 },
|
|
{ title: 'Active', dataIndex: 'activeAssetCount', width: 100 },
|
|
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (v) => v || '-' },
|
|
];
|
|
|
|
const assetColumns: ColumnsType<PrivatePortraitAsset> = [
|
|
{ title: '素材', dataIndex: 'name', width: 180, render: (v, r) => v || r.remoteAssetId || '-' },
|
|
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => <Text code>{v || '-'}</Text> },
|
|
{ title: '本地项目', dataIndex: 'projectName', width: 160 },
|
|
{ title: 'AssetId', dataIndex: 'remoteAssetId', width: 240, render: (v) => <Text code copyable>{v}</Text> },
|
|
{ title: 'GroupId', dataIndex: 'remoteGroupId', width: 240, render: (v) => <Text code copyable>{v}</Text> },
|
|
{ title: 'ProjectName', dataIndex: 'remoteProjectName', width: 240, render: (v) => <Text code copyable>{v || '-'}</Text> },
|
|
{ title: '类型', dataIndex: 'assetType', width: 90 },
|
|
{ title: '状态', dataIndex: 'status', width: 110, render: (v) => <Tag color={statusColor(v)}>{v}</Tag> },
|
|
{ title: '错误', dataIndex: 'errorMessage', width: 240, ellipsis: true, render: (v) => v || '-' },
|
|
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (v) => v || '-' },
|
|
];
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<div>
|
|
<Title level={3} style={{ margin: 0 }}>真人素材库</Title>
|
|
<Text type="secondary">只读排查页:查看用户项目组、Asset Group 与 Asset 状态。</Text>
|
|
</div>
|
|
<Space>
|
|
<Input
|
|
allowClear
|
|
prefix={<SearchOutlined />}
|
|
placeholder="搜索项目/素材"
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
onPressEnter={() => { setProjectPage(1); setAssetPage(1); loadProjects(); loadAssets(); }}
|
|
style={{ width: 260 }}
|
|
/>
|
|
<Button icon={<ReloadOutlined />} onClick={() => { loadProjects(); loadAssets(); }}>刷新</Button>
|
|
</Space>
|
|
</Space>
|
|
|
|
<Card title="真人素材项目组" style={{ marginBottom: 16 }}>
|
|
<Table
|
|
rowKey="id"
|
|
columns={projectColumns}
|
|
dataSource={projects}
|
|
loading={loadingProjects}
|
|
pagination={{ current: projectPage, pageSize: 20, total: projectTotal, onChange: setProjectPage }}
|
|
size="small"
|
|
scroll={{ x: 900 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Card
|
|
title={selectedProjectId ? '项目素材明细' : '全部素材明细'}
|
|
extra={selectedProjectId ? <Button size="small" onClick={() => setSelectedProjectId(undefined)}>查看全部</Button> : null}
|
|
>
|
|
<Table
|
|
rowKey="id"
|
|
columns={assetColumns}
|
|
dataSource={assets}
|
|
loading={loadingAssets}
|
|
pagination={{ current: assetPage, pageSize: 20, total: assetTotal, onChange: setAssetPage }}
|
|
size="small"
|
|
scroll={{ x: 1600 }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminPrivatePortraitProjects;
|