虚拟素材库集成到真人素材库

This commit is contained in:
2026-07-07 17:42:31 +08:00
parent 141a2051ba
commit 35ffa317e7
10 changed files with 792 additions and 737 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-4aokWt-a.js"></script> <script type="module" crossorigin src="/assets/index-C6k2TIpY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css"> <link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
</head> </head>
<body> <body>
+2 -2
View File
@@ -35,7 +35,6 @@ import CreativePlazaPage from './pages/CreativePlazaPage';
import TeamManagementPage from './pages/TeamManagementPage'; import TeamManagementPage from './pages/TeamManagementPage';
import JoinTeamPage from './pages/JoinTeamPage'; import JoinTeamPage from './pages/JoinTeamPage';
import PrivatePortraitAuthorizeResult from './pages/PrivatePortraitAuthorizeResult'; import PrivatePortraitAuthorizeResult from './pages/PrivatePortraitAuthorizeResult';
import PrivatePortraitVirtualMaterialPage from './pages/PrivatePortraitVirtualMaterialPage';
import { useAuthStore } from './store/useAuthStore'; import { useAuthStore } from './store/useAuthStore';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { user, loading, checkAuth } = useAuthStore(); const { user, loading, checkAuth } = useAuthStore();
@@ -95,6 +94,7 @@ const App = () => {
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/join-team" element={<JoinTeamPage />} /> <Route path="/join-team" element={<JoinTeamPage />} />
<Route path="/private-portrait-authorized" element={<PrivatePortraitAuthorizeResult />} />
<Route <Route
path="/" path="/"
element={ element={
@@ -125,7 +125,7 @@ const App = () => {
<Route path="authorization" element={<AuthorizationPage />} /> <Route path="authorization" element={<AuthorizationPage />} />
<Route path="authoriza-waiting" element={<AuthorizationWaitingPage />} /> <Route path="authoriza-waiting" element={<AuthorizationWaitingPage />} />
<Route path="materials" element={<MaterialListPage />} /> <Route path="materials" element={<MaterialListPage />} />
<Route path="materials/private-portrait-virtual" element={<PrivatePortraitVirtualMaterialPage />} /> <Route path="materials/private-portrait-virtual" element={<Navigate to="/generated?filterType=private_portrait&portraitTab=aigc_virtual" replace />} />
<Route path="consume" element={<ConsumePage />} /> <Route path="consume" element={<ConsumePage />} />
<Route path="popular" element={<PopularPage />} /> <Route path="popular" element={<PopularPage />} />
<Route path="authacc" element={<AuthAccountPage />} /> <Route path="authacc" element={<AuthAccountPage />} />
@@ -1,2 +1,4 @@
export { default as PrivatePortraitLibraryPanel } from './library/LibraryPanel'; export { default as PrivatePortraitLibraryPanel } from './library/LibraryPanel';
export { default as PrivatePortraitRealPersonLibraryPanel } from './library/RealPersonLibraryPanel';
export { default as PrivatePortraitVirtualMaterialPanel } from './library/VirtualMaterialPanel';
export { default as PrivatePortraitAssetPicker } from './picker/AssetPicker'; export { default as PrivatePortraitAssetPicker } from './picker/AssetPicker';
@@ -1,187 +1,56 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Button, Card, Col, Form, Input, Modal, QRCode, Row, Space, Spin, Typography, message } from 'antd'; import { Tabs, Typography } from 'antd';
import { CheckCircleOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { useSearchParams } from 'react-router-dom';
import type { PrivatePortraitProject, PrivatePortraitValidateSession } from '../../../types'; import RealPersonLibraryPanel from './RealPersonLibraryPanel';
import { createPrivatePortraitProject, getPrivatePortraitProjects, getPrivatePortraitValidateSession } from '../../../api'; import VirtualMaterialPanel from './VirtualMaterialPanel';
import PrivatePortraitProjectList from './ProjectList';
import PrivatePortraitProjectDetail from './ProjectDetail';
const VALIDATE_SUCCESS_STATUS = 'group_active'; const { Title, Text } = Typography;
const POLL_INTERVAL_FALLBACK = 2000;
type PrivatePortraitTabKey = 'real_person' | 'aigc_virtual';
const normalizeTabKey = (value?: string | null): PrivatePortraitTabKey => (
value === 'aigc_virtual' ? 'aigc_virtual' : 'real_person'
);
const PrivatePortraitLibraryPanel: React.FC = () => { const PrivatePortraitLibraryPanel: React.FC = () => {
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]); const [searchParams, setSearchParams] = useSearchParams();
const [selected, setSelected] = useState<PrivatePortraitProject | null>(null); const [activeKey, setActiveKey] = useState<PrivatePortraitTabKey>(() => normalizeTabKey(searchParams.get('portraitTab')));
const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [createdProject, setCreatedProject] = useState<PrivatePortraitProject | null>(null);
const [validateSession, setValidateSession] = useState<PrivatePortraitValidateSession | null>(null);
const [polling, setPolling] = useState(false);
const [form] = Form.useForm();
const timerRef = useRef<number | null>(null);
const clearPollTimer = () => { const items = useMemo(() => [
if (timerRef.current) { {
window.clearInterval(timerRef.current); key: 'real_person',
timerRef.current = null; 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 });
}; };
const resetCreateModal = () => {
clearPollTimer();
setCreateOpen(false);
setCreating(false);
setPolling(false);
setCreatedProject(null);
setValidateSession(null);
form.resetFields();
};
const loadProjects = async () => {
setLoading(true);
try {
const res = await getPrivatePortraitProjects({ pageSize: 100, status: 'active' });
setProjects(res.items);
setSelected((prev) => prev ? (res.items.find((item) => item.id === prev.id) || res.items[0] || null) : (res.items[0] || null));
} catch (e: any) {
message.error(e?.message || '加载真人素材项目组失败');
} finally {
setLoading(false);
}
};
useEffect(() => { loadProjects(); }, []);
useEffect(() => () => clearPollTimer(), []);
const finishCreateSuccess = async (projectId: string) => {
clearPollTimer();
setPolling(false);
message.success('真人认证完成,项目组已创建成功');
setCreateOpen(false);
setValidateSession(null);
setCreatedProject(null);
form.resetFields();
const res = await getPrivatePortraitProjects({ pageSize: 100, status: 'active' });
setProjects(res.items);
setSelected(res.items.find((item) => item.id === projectId) || res.items[0] || null);
};
const startPolling = (sessionId: string, projectId: string, intervalMs: number) => {
clearPollTimer();
setPolling(true);
const run = async () => {
try {
const next = await getPrivatePortraitValidateSession(sessionId);
setValidateSession(next);
if (next.status === VALIDATE_SUCCESS_STATUS) {
await finishCreateSuccess(projectId);
return;
}
if (['callback_failed', 'failed', 'expired'].includes(next.status)) {
clearPollTimer();
setPolling(false);
message.error(next.errorMessage || '真人认证未完成,请重新创建项目组');
}
} catch (e: any) {
clearPollTimer();
setPolling(false);
message.error(e?.message || '轮询真人认证状态失败');
}
};
timerRef.current = window.setInterval(run, Math.max(1000, intervalMs || POLL_INTERVAL_FALLBACK));
void run();
};
const handleCreate = async () => {
const values = await form.validateFields();
setCreating(true);
try {
const callbackRedirectUrl = `${window.location.origin}/private-portrait-authorized`;
const res = await createPrivatePortraitProject({ ...values, callbackRedirectUrl });
setCreatedProject(res.project);
setValidateSession(res.validateSession);
message.success('请使用手机扫码完成人脸认证');
if (res.validateSession?.id) {
startPolling(res.validateSession.id, res.project.id, res.pollIntervalMs || POLL_INTERVAL_FALLBACK);
}
} catch (e: any) {
message.error(e?.message || '创建项目组认证二维码失败');
} finally {
setCreating(false);
}
};
const h5Link = validateSession?.h5Link || '';
const isSuccess = validateSession?.status === VALIDATE_SUCCESS_STATUS;
return ( return (
<div style={{ padding: 16 }}> <div style={{ padding: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<div> <Title level={4} style={{ margin: 0 }}></Title>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> <Text type="secondary"> AIGC Asset Group</Text>
<Typography.Text type="secondary"></Typography.Text>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={loadProjects} loading={loading}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}></Button>
</Space>
</div> </div>
<Row gutter={16}> <Tabs
<Col xs={24} md={7} lg={6}> activeKey={activeKey}
<Card title="项目组" style={{ borderRadius: 12 }}> onChange={handleTabChange}
<PrivatePortraitProjectList items={projects} selectedId={selected?.id} onSelect={setSelected} /> items={items}
</Card> destroyInactiveTabPane={false}
</Col> />
<Col xs={24} md={17} lg={18}>
{selected ? (
<PrivatePortraitProjectDetail project={selected} onDeleted={() => { setSelected(null); loadProjects(); }} onChanged={loadProjects} />
) : (
<Card style={{ borderRadius: 12, textAlign: 'center', color: '#94a3b8' }}></Card>
)}
</Col>
</Row>
<Modal
title="新建真人素材项目组"
open={createOpen}
onCancel={resetCreateModal}
footer={validateSession ? [<Button key="close" onClick={resetCreateModal}></Button>] : undefined}
onOk={validateSession ? undefined : handleCreate}
okText="开始认证并创建"
confirmLoading={creating}
maskClosable={!polling}
>
{!validateSession ? (
<Form form={form} layout="vertical">
<Form.Item label="项目组名称" name="name" rules={[{ required: true, message: '请输入项目组名称' }]}>
<Input placeholder="例如:达人A、客户B、张三人像" />
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={3} placeholder="可选" />
</Form.Item>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
</Typography.Paragraph>
</Form>
) : (
<Space direction="vertical" align="center" size={16} style={{ width: '100%' }}>
{isSuccess ? (
<CheckCircleOutlined style={{ fontSize: 54, color: '#22c55e' }} />
) : h5Link ? (
<QRCode value={h5Link} size={220} />
) : (
<Spin />
)}
<div style={{ textAlign: 'center' }}>
<Typography.Title level={5} style={{ marginBottom: 8 }}>{createdProject?.name || '真人素材项目组'}</Typography.Title>
<Typography.Text type={isSuccess ? 'success' : 'secondary'}>
{isSuccess ? '认证成功,项目组正在刷新' : '请使用手机扫码完成人脸认证,成功后回到电脑端查看项目组。'}
</Typography.Text>
</div>
{h5Link && !isSuccess && <Typography.Text copyable style={{ wordBreak: 'break-all' }}>{h5Link}</Typography.Text>}
</Space>
)}
</Modal>
</div> </div>
); );
}; };
@@ -0,0 +1,189 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, Card, Col, Form, Input, Modal, QRCode, Row, Space, Spin, Typography, message } from 'antd';
import { CheckCircleOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import type { PrivatePortraitProject, PrivatePortraitValidateSession } from '../../../types';
import { createPrivatePortraitProject, getPrivatePortraitProjects, getPrivatePortraitValidateSession } from '../../../api';
import PrivatePortraitProjectList from './ProjectList';
import PrivatePortraitProjectDetail from './ProjectDetail';
const VALIDATE_SUCCESS_STATUS = 'group_active';
const POLL_INTERVAL_FALLBACK = 2000;
const RealPersonLibraryPanel: React.FC = () => {
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]);
const [selected, setSelected] = useState<PrivatePortraitProject | null>(null);
const [loading, setLoading] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [creating, setCreating] = useState(false);
const [createdProject, setCreatedProject] = useState<PrivatePortraitProject | null>(null);
const [validateSession, setValidateSession] = useState<PrivatePortraitValidateSession | null>(null);
const [polling, setPolling] = useState(false);
const [form] = Form.useForm();
const timerRef = useRef<number | null>(null);
const clearPollTimer = () => {
if (timerRef.current) {
window.clearInterval(timerRef.current);
timerRef.current = null;
}
};
const resetCreateModal = () => {
clearPollTimer();
setCreateOpen(false);
setCreating(false);
setPolling(false);
setCreatedProject(null);
setValidateSession(null);
form.resetFields();
};
const loadProjects = async () => {
setLoading(true);
try {
const res = await getPrivatePortraitProjects({ pageSize: 100, status: 'active' });
setProjects(res.items);
setSelected((prev) => prev ? (res.items.find((item) => item.id === prev.id) || res.items[0] || null) : (res.items[0] || null));
} catch (e: any) {
message.error(e?.message || '加载真人素材项目组失败');
} finally {
setLoading(false);
}
};
useEffect(() => { loadProjects(); }, []);
useEffect(() => () => clearPollTimer(), []);
const finishCreateSuccess = async (projectId: string) => {
clearPollTimer();
setPolling(false);
message.success('真人认证完成,项目组已创建成功');
setCreateOpen(false);
setValidateSession(null);
setCreatedProject(null);
form.resetFields();
const res = await getPrivatePortraitProjects({ pageSize: 100, status: 'active' });
setProjects(res.items);
setSelected(res.items.find((item) => item.id === projectId) || res.items[0] || null);
};
const startPolling = (sessionId: string, projectId: string, intervalMs: number) => {
clearPollTimer();
setPolling(true);
const run = async () => {
try {
const next = await getPrivatePortraitValidateSession(sessionId);
setValidateSession(next);
if (next.status === VALIDATE_SUCCESS_STATUS) {
await finishCreateSuccess(projectId);
return;
}
if (['callback_failed', 'failed', 'expired'].includes(next.status)) {
clearPollTimer();
setPolling(false);
message.error(next.errorMessage || '真人认证未完成,请重新创建项目组');
}
} catch (e: any) {
clearPollTimer();
setPolling(false);
message.error(e?.message || '轮询真人认证状态失败');
}
};
timerRef.current = window.setInterval(run, Math.max(1000, intervalMs || POLL_INTERVAL_FALLBACK));
void run();
};
const handleCreate = async () => {
const values = await form.validateFields();
setCreating(true);
try {
const callbackRedirectUrl = `${window.location.origin}/private-portrait-authorized`;
const res = await createPrivatePortraitProject({ ...values, callbackRedirectUrl });
setCreatedProject(res.project);
setValidateSession(res.validateSession);
message.success('请使用手机扫码完成人脸认证');
if (res.validateSession?.id) {
startPolling(res.validateSession.id, res.project.id, res.pollIntervalMs || POLL_INTERVAL_FALLBACK);
}
} catch (e: any) {
message.error(e?.message || '创建项目组认证二维码失败');
} finally {
setCreating(false);
}
};
const h5Link = validateSession?.h5Link || '';
const isSuccess = validateSession?.status === VALIDATE_SUCCESS_STATUS;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={loadProjects} loading={loading}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}></Button>
</Space>
</div>
<Row gutter={16}>
<Col xs={24} md={7} lg={6}>
<Card title="项目组" style={{ borderRadius: 12 }}>
<PrivatePortraitProjectList items={projects} selectedId={selected?.id} onSelect={setSelected} />
</Card>
</Col>
<Col xs={24} md={17} lg={18}>
{selected ? (
<PrivatePortraitProjectDetail project={selected} onDeleted={() => { setSelected(null); loadProjects(); }} onChanged={loadProjects} />
) : (
<Card style={{ borderRadius: 12, textAlign: 'center', color: '#94a3b8' }}></Card>
)}
</Col>
</Row>
<Modal
title="新建真人素材项目组"
open={createOpen}
onCancel={resetCreateModal}
footer={validateSession ? [<Button key="close" onClick={resetCreateModal}></Button>] : undefined}
onOk={validateSession ? undefined : handleCreate}
okText="开始认证并创建"
confirmLoading={creating}
maskClosable={!polling}
>
{!validateSession ? (
<Form form={form} layout="vertical">
<Form.Item label="项目组名称" name="name" rules={[{ required: true, message: '请输入项目组名称' }]}>
<Input placeholder="例如:达人A、客户B、张三人像" />
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={3} placeholder="可选" />
</Form.Item>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
</Typography.Paragraph>
</Form>
) : (
<Space direction="vertical" align="center" size={16} style={{ width: '100%' }}>
{isSuccess ? (
<CheckCircleOutlined style={{ fontSize: 54, color: '#22c55e' }} />
) : h5Link ? (
<QRCode value={h5Link} size={220} />
) : (
<Spin />
)}
<div style={{ textAlign: 'center' }}>
<Typography.Title level={5} style={{ marginBottom: 8 }}>{createdProject?.name || '真人素材项目组'}</Typography.Title>
<Typography.Text type={isSuccess ? 'success' : 'secondary'}>
{isSuccess ? '认证成功,项目组正在刷新' : '请使用手机扫码完成人脸认证,成功后回到电脑端查看项目组。'}
</Typography.Text>
</div>
{h5Link && !isSuccess && <Typography.Text copyable style={{ wordBreak: 'break-all' }}>{h5Link}</Typography.Text>}
</Space>
)}
</Modal>
</div>
);
};
export default RealPersonLibraryPanel;
@@ -21,7 +21,6 @@ import {
} from 'antd'; } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface'; import type { UploadFile } from 'antd/es/upload/interface';
import { import {
ArrowLeftOutlined,
CloudSyncOutlined, CloudSyncOutlined,
DeleteOutlined, DeleteOutlined,
EyeOutlined, EyeOutlined,
@@ -31,7 +30,6 @@ import {
UploadOutlined, UploadOutlined,
VideoCameraOutlined, VideoCameraOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { import {
createPrivatePortraitVirtualAsset, createPrivatePortraitVirtualAsset,
createPrivatePortraitVirtualProject, createPrivatePortraitVirtualProject,
@@ -43,10 +41,10 @@ import {
syncPrivatePortraitVirtualAsset, syncPrivatePortraitVirtualAsset,
uploadImage, uploadImage,
uploadVideo, uploadVideo,
} from '../api'; } from '../../../api';
import type { PrivatePortraitAsset, PrivatePortraitConfig, PrivatePortraitProject } from '../types'; import type { PrivatePortraitAsset, PrivatePortraitConfig, PrivatePortraitProject } from '../../../types';
const { Text, Title, Paragraph } = Typography; const { Text, Paragraph } = Typography;
type AssetTypeFilter = 'Image' | 'Video' | undefined; type AssetTypeFilter = 'Image' | 'Video' | undefined;
@@ -162,9 +160,8 @@ const TypeTag: React.FC<{ type?: string | null }> = ({ type }) => {
return <Tag color={config?.color || 'default'} icon={config?.icon}>{config?.label || value}</Tag>; return <Tag color={config?.color || 'default'} icon={config?.icon}>{config?.label || value}</Tag>;
}; };
const PrivatePortraitVirtualMaterialPage: React.FC = () => { const VirtualMaterialPanel: React.FC = () => {
const { message } = App.useApp(); const { message } = App.useApp();
const navigate = useNavigate();
const [config, setConfig] = useState<PrivatePortraitConfig | null>(null); const [config, setConfig] = useState<PrivatePortraitConfig | null>(null);
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]); const [projects, setProjects] = useState<PrivatePortraitProject[]>([]);
const [selectedProjectId, setSelectedProjectId] = useState<string>(); const [selectedProjectId, setSelectedProjectId] = useState<string>();
@@ -252,7 +249,7 @@ const PrivatePortraitVirtualMaterialPage: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
reloadAll(); void reloadAll();
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -413,12 +410,7 @@ const PrivatePortraitVirtualMaterialPage: React.FC = () => {
}; };
return ( return (
<div style={{ minHeight: '94vh' }}> <div>
<Space style={{ marginBottom: 16 }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/materials')}></Button>
<Title level={4} style={{ margin: 0 }}></Title>
</Space>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}> <Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={24} md={8}> <Col xs={24} md={8}>
<Card style={{ borderRadius: 16, background: 'linear-gradient(135deg,#f5f3ff,#fff)' }}> <Card style={{ borderRadius: 16, background: 'linear-gradient(135deg,#f5f3ff,#fff)' }}>
@@ -626,4 +618,4 @@ const PrivatePortraitVirtualMaterialPage: React.FC = () => {
); );
}; };
export default PrivatePortraitVirtualMaterialPage; export default VirtualMaterialPanel;
+5 -2
View File
@@ -18,6 +18,7 @@ import {
LoadingOutlined, LoadingOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useSearchParams } from 'react-router-dom';
import { PrivatePortraitLibraryPanel } from '../components/privatePortrait'; import { PrivatePortraitLibraryPanel } from '../components/privatePortrait';
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest, deleteHistory, deleteResourcesMaterial } from '../api'; import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest, deleteHistory, deleteResourcesMaterial } from '../api';
@@ -50,7 +51,9 @@ const GeneratedRecord: React.FC = () => {
setIsPageLoaded(true); setIsPageLoaded(true);
}); });
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait'>('project'); const [searchParams] = useSearchParams();
const initialFilterType = searchParams.get('filterType') === 'private_portrait' ? 'private_portrait' : 'project';
const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'private_portrait'>(initialFilterType);
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video'); const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video');
const [recordlist, setRecordList] = useState<any[]>([]); const [recordlist, setRecordList] = useState<any[]>([]);
const [Pagebreak, setPagebreak] = useState<any>({ const [Pagebreak, setPagebreak] = useState<any>({
@@ -1028,7 +1031,7 @@ const GeneratedRecord: React.FC = () => {
}} }}
icon={<UserOutlined />} icon={<UserOutlined />}
> >
</Button> </Button>
</Space> </Space>
</div> </div>
+1 -1
View File
@@ -438,7 +438,7 @@ const MaterialListPage: React.FC = () => {
<div style={{ display: 'flex', gap: 12 }}> <div style={{ display: 'flex', gap: 12 }}>
<Button <Button
icon={<RobotOutlined />} icon={<RobotOutlined />}
onClick={() => navigate('/materials/private-portrait-virtual')} onClick={() => navigate('/generated?filterType=private_portrait&portraitTab=aigc_virtual')}
style={{ style={{
borderRadius: 12, borderRadius: 12,
fontSize: 14, fontSize: 14,