133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
import React, { useMemo, useState, useEffect } from 'react';
|
|
import { Card, Col, Row, Tabs, Typography, message } from 'antd';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { getPrivatePortraitConfig, getPrivatePortraitProjects, getPrivatePortraitVirtualConfig, getPrivatePortraitVirtualProjects } from '../../../api';
|
|
import type { PrivatePortraitConfig, PrivatePortraitProject } from '../../../types';
|
|
import RealPersonLibraryPanel from './RealPersonLibraryPanel';
|
|
import VirtualMaterialPanel from './VirtualMaterialPanel';
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
|
|
type PrivatePortraitTabKey = 'real_person' | 'aigc_virtual';
|
|
|
|
const normalizeTabKey = (value?: string | null): PrivatePortraitTabKey => (
|
|
value === 'aigc_virtual' ? 'aigc_virtual' : 'real_person'
|
|
);
|
|
|
|
const PrivatePortraitLibraryPanel: React.FC = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [activeKey, setActiveKey] = useState<PrivatePortraitTabKey>(() => normalizeTabKey(searchParams.get('portraitTab')));
|
|
const [config, setConfig] = useState<PrivatePortraitConfig | null>(null);
|
|
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]);
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string>();
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
try {
|
|
if (activeKey === 'aigc_virtual') {
|
|
const [configRes, projectsRes] = await Promise.all([
|
|
getPrivatePortraitVirtualConfig(),
|
|
getPrivatePortraitVirtualProjects({ page: 1, pageSize: 100, status: 'active' }),
|
|
]);
|
|
setConfig(configRes);
|
|
const projectList = projectsRes.items || [];
|
|
setProjects(projectList);
|
|
setSelectedProjectId((prev) => prev && projectList.some((item) => item.id === prev) ? prev : projectList[0]?.id);
|
|
} else {
|
|
const [configRes, projectsRes] = await Promise.all([
|
|
getPrivatePortraitConfig(),
|
|
getPrivatePortraitProjects({ pageSize: 100, status: 'active' }),
|
|
]);
|
|
setConfig(configRes);
|
|
const projectList = projectsRes.items || [];
|
|
setProjects(projectList);
|
|
setSelectedProjectId((prev) => prev && projectList.some((item) => item.id === prev) ? prev : projectList[0]?.id);
|
|
}
|
|
} catch (err: any) {
|
|
message.error(err?.message || '加载数据失败');
|
|
}
|
|
};
|
|
void loadData();
|
|
}, [activeKey]);
|
|
|
|
const selectedProject = useMemo(
|
|
() => projects.find((item) => item.id === selectedProjectId) || null,
|
|
[projects, selectedProjectId],
|
|
);
|
|
|
|
const quotaText = useMemo(() => {
|
|
if (!config) return '额度加载中';
|
|
return `已用 ${config.usedAssetCount || 0} / ${config.assetLimit || 0} 个素材,剩余 ${config.remainingAssetCount || 0}`;
|
|
}, [config]);
|
|
|
|
const items = useMemo(() => [
|
|
{
|
|
key: 'real_person',
|
|
label: '真人素材库',
|
|
children: <RealPersonLibraryPanel />,
|
|
},
|
|
{
|
|
key: 'aigc_virtual',
|
|
label: '虚拟素材库',
|
|
children: <VirtualMaterialPanel />,
|
|
},
|
|
], []);
|
|
|
|
const handleTabChange = (key: string) => {
|
|
const nextKey = normalizeTabKey(key);
|
|
setActiveKey(nextKey);
|
|
|
|
const nextParams = new URLSearchParams(searchParams);
|
|
nextParams.set('filterType', 'private_portrait');
|
|
nextParams.set('portraitTab', nextKey);
|
|
setSearchParams(nextParams, { replace: true });
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{/* <div style={{ marginBottom: 16 }}>
|
|
<Title level={4} style={{ margin: 0 }}>私域素材库</Title>
|
|
<Text type="secondary">统一管理真人素材和虚拟素材。真人素材需先完成人脸认证后才可上传素材,虚拟素材如需使用需先上传。</Text>
|
|
</div> */}
|
|
{/* <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
|
<Col xs={24} md={8}>
|
|
<Card style={{ borderRadius: 16, background: 'linear-gradient(135deg,#f5f3ff,#fff)' }}>
|
|
<Text type="secondary">素材总额度</Text>
|
|
<div style={{ fontSize: 24, fontWeight: 700, color: '#4f46e5', marginTop: 8 }}>{quotaText}</div>
|
|
<Paragraph style={{ margin: '8px 0 0', color: '#64748b' }}>真人/虚拟共用,图片/视频共用;音频暂不开放。</Paragraph>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={8}>
|
|
<Card style={{ borderRadius: 16 }}>
|
|
<Text type="secondary">项目组</Text>
|
|
<div style={{ fontSize: 24, fontWeight: 700, color: '#1e293b', marginTop: 8 }}>{projects.length}</div>
|
|
<Paragraph style={{ margin: '8px 0 0', color: '#64748b' }}>
|
|
{activeKey === 'aigc_virtual'
|
|
? '虚拟素材如需使用需先上传'
|
|
: '真人项目组需完成人脸认证后才可上传素材。'}
|
|
</Paragraph>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={8}>
|
|
<Card style={{ borderRadius: 16 }}>
|
|
<Text type="secondary">当前项目素材</Text>
|
|
<div style={{ fontSize: 24, fontWeight: 700, color: '#1e293b', marginTop: 8 }}>{selectedProject?.assetCount || 0}</div>
|
|
<Paragraph style={{ margin: '8px 0 0', color: '#64748b' }}>仅 入库成功 状态素材可在 AI 创作中引用。</Paragraph>
|
|
</Card>
|
|
</Col>
|
|
</Row> */}
|
|
<Tabs
|
|
activeKey={activeKey}
|
|
onChange={handleTabChange}
|
|
items={items}
|
|
destroyOnHidden={false}
|
|
tabBarExtraContent={
|
|
<div style={{ fontSize: 14, fontWeight: 700, color: '#4f46e5', marginTop: 8 }}>{quotaText}。真人/虚拟共用,图片/视频共用;音频暂不开放。</div>
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PrivatePortraitLibraryPanel;
|