虚拟素材库集成到真人素材库
This commit is contained in:
-506
File diff suppressed because one or more lines are too long
+506
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
+7
-15
@@ -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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user