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 ? { setProjectStatus(v); setProjectPage(1); }} options={PROJECT_STATUS_OPTIONS} style={{ width: 180 }} />}>
); }; export default AdminPrivatePortraitProjects;