import React, { useEffect, useMemo, useState } from 'react';
import { Button, Card, Image, Input, Select, Space, Statistic, Table, Tag, Tooltip, Typography, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { CopyOutlined, PlayCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { adminGetPrivatePortraitAssets, adminGetPrivatePortraitProjects } from '../api';
import type { PrivatePortraitAsset, PrivatePortraitProject } from '../types';
import { formatDate } from '../utils/formatDate';
import { apiUrl } from '../utils/resourceUrl';
const { Title, Text } = Typography;
const PAGE_SIZE = 20;
const LIBRARY_OPTIONS = [
{ label: '全部素材库', value: '' },
{ label: '真人认证', value: 'real_person' },
{ label: '虚拟人像', value: 'aigc_virtual' },
];
const ASSET_TYPE_OPTIONS = [
{ label: '全部类型', value: '' },
{ label: '图片', value: 'Image' },
{ label: '视频', value: 'Video' },
];
const PROJECT_STATUS_OPTIONS = [
{ label: '全部项目状态', value: '' },
{ label: '可用', value: 'active' },
{ label: '认证中', value: 'validating' },
{ label: '认证失败', value: 'validate_failed' },
{ label: '创建远端组中', value: 'creating_remote_group' },
{ label: '创建远端组失败', value: 'create_group_failed' },
{ label: '已删除', value: 'deleted' },
];
const ASSET_STATUS_OPTIONS = [
{ label: '全部素材状态', value: '' },
{ label: '创建中', value: 'creating' },
{ label: '处理中', value: 'Processing' },
{ label: '可用', value: 'Active' },
{ label: '失败', value: 'Failed' },
{ label: '本地已删', value: 'local_deleted' },
{ label: '远端已删', value: 'remote_deleted' },
{ label: '删除失败', value: 'delete_failed' },
];
const libraryLabel = (value?: string | null) => {
if (value === 'real_person') return '真人认证';
if (value === 'aigc_virtual') return '虚拟人像';
return value || '-';
};
const libraryColor = (value?: string | null) => {
if (value === 'real_person') return 'purple';
if (value === 'aigc_virtual') return 'cyan';
return 'default';
};
const statusColor = (status?: string) => {
if (status === 'Active' || status === 'active') return 'green';
if (status === 'Processing' || status === 'creating_remote_group' || status === 'validating' || status === 'creating') return 'blue';
if (status === 'Failed' || status === 'failed' || status === 'delete_failed' || status === 'validate_failed' || status === 'create_group_failed') return 'red';
if (status?.includes('deleted')) return 'default';
return 'default';
};
const assetTypeLabel = (value?: string | null) => {
if (value === 'Image') return '图片';
if (value === 'Video') return '视频';
return value || '-';
};
const shortId = (value?: string | null, keep = 10) => {
if (!value) return '-';
if (value.length <= keep * 2 + 3) return value;
return `${value.slice(0, keep)}...${value.slice(-keep)}`;
};
const copyText = async (value?: string | null) => {
if (!value) return;
await navigator.clipboard?.writeText(value);
message.success('已复制');
};
const toPreviewUrl = (value?: string | null) => {
if (!value) return '';
const trimmed = String(value).trim();
if (!trimmed || trimmed.startsWith('asset://')) return '';
return apiUrl(trimmed);
};
const PreviewCell: React.FC<{ asset: PrivatePortraitAsset }> = ({ asset }) => {
const url = toPreviewUrl(asset.displayUrl || asset.previewUrl || asset.remoteUrl || asset.sourceUrl);
const cover = toPreviewUrl(asset.videoCoverUrl || asset.previewUrl || asset.displayUrl || asset.sourceUrl);
if (asset.assetType === 'Video') {
return (
{cover ?

:
}
{url ?
: null}
);
}
if (url) return ;
return ;
};
const AdminPrivatePortraitProjects: React.FC = () => {
const [projects, setProjects] = useState([]);
const [assets, setAssets] = useState([]);
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 [libraryType, setLibraryType] = useState('');
const [assetType, setAssetType] = useState('');
const [projectStatus, setProjectStatus] = useState('');
const [assetStatus, setAssetStatus] = useState('');
const [selectedProjectId, setSelectedProjectId] = useState();
const [projectPage, setProjectPage] = useState(1);
const [assetPage, setAssetPage] = useState(1);
const filters = useMemo(() => ({
keyword: keyword.trim() || undefined,
libraryType: libraryType || undefined,
assetType: assetType || undefined,
projectStatus: projectStatus || undefined,
assetStatus: assetStatus || undefined,
}), [keyword, libraryType, assetType, projectStatus, assetStatus]);
const loadProjects = async () => {
setLoadingProjects(true);
try {
const res = await adminGetPrivatePortraitProjects({
keyword: filters.keyword,
libraryType: filters.libraryType,
status: filters.projectStatus,
page: projectPage,
pageSize: PAGE_SIZE,
});
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: filters.keyword,
libraryType: filters.libraryType,
assetType: filters.assetType,
status: filters.assetStatus,
page: assetPage,
pageSize: PAGE_SIZE,
});
setAssets(res.items || []);
setAssetTotal(res.total || 0);
} catch (err: any) {
message.error(err?.message || '加载私域人像素材失败');
} finally {
setLoadingAssets(false);
}
};
useEffect(() => { loadProjects(); }, [projectPage, filters.libraryType, filters.projectStatus]);
useEffect(() => { loadAssets(); }, [selectedProjectId, assetPage, filters.libraryType, filters.assetType, filters.assetStatus]);
const searchAll = () => {
setProjectPage(1);
setAssetPage(1);
setTimeout(() => { loadProjects(); loadAssets(); }, 0);
};
const resetFilters = () => {
setKeyword('');
setLibraryType('');
setAssetType('');
setProjectStatus('');
setAssetStatus('');
setSelectedProjectId(undefined);
setProjectPage(1);
setAssetPage(1);
};
const projectSummary = useMemo(() => {
return projects.reduce((acc, item) => {
acc.asset += item.assetCount || 0;
acc.image += item.imageAssetCount || 0;
acc.video += item.videoAssetCount || 0;
acc.active += item.activeAssetCount || 0;
return acc;
}, { asset: 0, image: 0, video: 0, active: 0 });
}, [projects]);
const projectColumns: ColumnsType = [
{ title: '项目名称', dataIndex: 'name', width: 200, fixed: 'left', render: (v, r) => },
{ title: '类型', dataIndex: 'libraryType', width: 110, render: (v) => {libraryLabel(v)} },
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => {v || '-'} },
{ title: '状态', dataIndex: 'status', width: 130, render: (v) => {v} },
{ title: '素材', width: 170, render: (_, r) => 总 {r.assetCount || 0}图 {r.imageAssetCount || 0}视 {r.videoAssetCount || 0} },
{ title: 'Active', dataIndex: 'activeAssetCount', width: 90 },
{ title: 'ProjectName', dataIndex: 'remoteProjectName', width: 180, render: (v) => {v || '-'} },
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: formatDate },
{ title: '更新时间', dataIndex: 'updatedAt', width: 170, render: formatDate },
];
const assetColumns: ColumnsType = [
{ title: '预览', width: 100, fixed: 'left', render: (_, r) => },
{ title: '素材名称', dataIndex: 'name', width: 180, render: (v, r) => {v || '未命名素材'}
{assetTypeLabel(r.assetType)}
},
{ title: '库类型', dataIndex: 'libraryType', width: 110, render: (v) => {libraryLabel(v)} },
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => {v || '-'} },
{ title: '本地项目', dataIndex: 'projectName', width: 160, render: (v) => v || '-' },
{ title: '状态', dataIndex: 'status', width: 120, render: (v) => {v} },
{ title: 'AssetId', dataIndex: 'remoteAssetId', width: 190, render: (v) => {shortId(v)}{v ? } onClick={() => copyText(v)} /> : null} },
{ title: 'GroupId', dataIndex: 'remoteGroupId', width: 190, render: (v) => {shortId(v)}{v ? } onClick={() => copyText(v)} /> : null} },
{ title: '轮询', width: 160, render: (_, r) => 次数:{r.pollCount || 0}
下次:{formatDate((r as any).nextPollAt)}
},
{ title: '错误', dataIndex: 'errorMessage', width: 240, ellipsis: true, render: (v) => v ? {v} : '-' },
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: formatDate },
];
return (
);
};
export default AdminPrivatePortraitProjects;