真人素材库本地完成
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Input, Space, Table, Tag, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { adminGetPrivatePortraitAssets, adminGetPrivatePortraitProjects } from '../api';
|
||||
import type { PrivatePortraitAsset, PrivatePortraitProject } from '../types';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const statusColor = (status?: string) => {
|
||||
if (status === 'Active' || status === 'active') return 'green';
|
||||
if (status === 'Processing') return 'blue';
|
||||
if (status === 'Failed' || status === 'failed' || status === 'delete_failed') return 'red';
|
||||
if (status?.includes('deleted')) return 'default';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const AdminPrivatePortraitProjects: React.FC = () => {
|
||||
const [projects, setProjects] = useState<PrivatePortraitProject[]>([]);
|
||||
const [assets, setAssets] = useState<PrivatePortraitAsset[]>([]);
|
||||
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 [selectedProjectId, setSelectedProjectId] = useState<string | undefined>();
|
||||
const [projectPage, setProjectPage] = useState(1);
|
||||
const [assetPage, setAssetPage] = useState(1);
|
||||
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true);
|
||||
try {
|
||||
const res = await adminGetPrivatePortraitProjects({ keyword: keyword.trim() || undefined, page: projectPage, pageSize: 20 });
|
||||
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: keyword.trim() || undefined, page: assetPage, pageSize: 20 });
|
||||
setAssets(res.items || []);
|
||||
setAssetTotal(res.total || 0);
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '加载真人素材失败');
|
||||
} finally {
|
||||
setLoadingAssets(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { loadProjects(); }, [projectPage]);
|
||||
useEffect(() => { loadAssets(); }, [selectedProjectId, assetPage]);
|
||||
|
||||
const projectColumns: ColumnsType<PrivatePortraitProject> = [
|
||||
{ title: '项目名称', dataIndex: 'name', width: 180, render: (v, r) => <Button type="link" onClick={() => { setSelectedProjectId(r.id); setAssetPage(1); }}>{v}</Button> },
|
||||
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => <Text code>{v || '-'}</Text> },
|
||||
{ title: '状态', dataIndex: 'status', width: 100, render: (v) => <Tag color={statusColor(v)}>{v}</Tag> },
|
||||
{ title: 'ProjectName', dataIndex: 'remoteProjectName', width: 240, render: (v) => <Text code copyable>{v || '-'}</Text> },
|
||||
{ title: '素材数', dataIndex: 'assetCount', width: 100 },
|
||||
{ title: 'Active', dataIndex: 'activeAssetCount', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (v) => v || '-' },
|
||||
];
|
||||
|
||||
const assetColumns: ColumnsType<PrivatePortraitAsset> = [
|
||||
{ title: '素材', dataIndex: 'name', width: 180, render: (v, r) => v || r.remoteAssetId || '-' },
|
||||
{ title: '用户ID', dataIndex: 'userId', width: 170, render: (v) => <Text code>{v || '-'}</Text> },
|
||||
{ title: '本地项目', dataIndex: 'projectName', width: 160 },
|
||||
{ title: 'AssetId', dataIndex: 'remoteAssetId', width: 240, render: (v) => <Text code copyable>{v}</Text> },
|
||||
{ title: 'GroupId', dataIndex: 'remoteGroupId', width: 240, render: (v) => <Text code copyable>{v}</Text> },
|
||||
{ title: 'ProjectName', dataIndex: 'remoteProjectName', width: 240, render: (v) => <Text code copyable>{v || '-'}</Text> },
|
||||
{ title: '类型', dataIndex: 'assetType', width: 90 },
|
||||
{ title: '状态', dataIndex: 'status', width: 110, render: (v) => <Tag color={statusColor(v)}>{v}</Tag> },
|
||||
{ title: '错误', dataIndex: 'errorMessage', width: 240, ellipsis: true, render: (v) => v || '-' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: (v) => v || '-' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div>
|
||||
<Title level={3} style={{ margin: 0 }}>真人素材库</Title>
|
||||
<Text type="secondary">只读排查页:查看用户项目组、Asset Group 与 Asset 状态。</Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索项目/素材"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={() => { setProjectPage(1); setAssetPage(1); loadProjects(); loadAssets(); }}
|
||||
style={{ width: 260 }}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { loadProjects(); loadAssets(); }}>刷新</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Card title="真人素材项目组" style={{ marginBottom: 16 }}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={projectColumns}
|
||||
dataSource={projects}
|
||||
loading={loadingProjects}
|
||||
pagination={{ current: projectPage, pageSize: 20, total: projectTotal, onChange: setProjectPage }}
|
||||
size="small"
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={selectedProjectId ? '项目素材明细' : '全部素材明细'}
|
||||
extra={selectedProjectId ? <Button size="small" onClick={() => setSelectedProjectId(undefined)}>查看全部</Button> : null}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={assetColumns}
|
||||
dataSource={assets}
|
||||
loading={loadingAssets}
|
||||
pagination={{ current: assetPage, pageSize: 20, total: assetTotal, onChange: setAssetPage }}
|
||||
size="small"
|
||||
scroll={{ x: 1600 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPrivatePortraitProjects;
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Progress, Select, Space, Switch, Table, Tabs, Tag, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined,
|
||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined, PictureOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
adjustCredits,
|
||||
adminGetPrivatePortraitConfig,
|
||||
adminUpdatePrivatePortraitConfig,
|
||||
createUser,
|
||||
deleteUserResourceCapacity,
|
||||
getAdminUsers,
|
||||
@@ -22,7 +24,7 @@ import {
|
||||
updateSystemConfig,
|
||||
updateUserMenus,
|
||||
} from '../api';
|
||||
import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types';
|
||||
import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
|
||||
const TEAM_UNASSIGNED_VALUE = '__none__';
|
||||
@@ -71,14 +73,18 @@ const AdminUsers: React.FC = () => {
|
||||
const [resetPwdModal, setResetPwdModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
const [capacityModal, setCapacityModal] = useState<{ open: boolean; user: AdminUser | null; detail: AdminUserResourceCapacityOut | null }>({ open: false, user: null, detail: null });
|
||||
const [teamModal, setTeamModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
const [portraitModal, setPortraitModal] = useState<{ open: boolean; user: AdminUser | null; config: PrivatePortraitConfig | null }>({ open: false, user: null, config: null });
|
||||
const [capacityLoading, setCapacityLoading] = useState(false);
|
||||
const [capacitySaving, setCapacitySaving] = useState(false);
|
||||
const [teamSaving, setTeamSaving] = useState(false);
|
||||
const [portraitLoading, setPortraitLoading] = useState(false);
|
||||
const [portraitSaving, setPortraitSaving] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
const [resetPwdForm] = Form.useForm();
|
||||
const [capacityForm] = Form.useForm();
|
||||
const [teamForm] = Form.useForm();
|
||||
const [portraitForm] = Form.useForm();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
@@ -183,6 +189,7 @@ const AdminUsers: React.FC = () => {
|
||||
credits: values.credits || 0,
|
||||
user_type: userType,
|
||||
frontend_user_kind: values.frontend_user_kind || 'external',
|
||||
private_portrait_image_limit: userType === 'frontend' ? Number(values.private_portrait_image_limit ?? 5) : 0,
|
||||
});
|
||||
message.success('用户创建成功');
|
||||
setCreateModal(false);
|
||||
@@ -266,6 +273,42 @@ const AdminUsers: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openPortraitModal = async (user: AdminUser) => {
|
||||
setPortraitLoading(true);
|
||||
setPortraitModal({ open: true, user, config: null });
|
||||
portraitForm.setFieldsValue({ privatePortraitImageLimit: user.privatePortraitImageLimit ?? 5 });
|
||||
try {
|
||||
const config = await adminGetPrivatePortraitConfig(user.id);
|
||||
portraitForm.setFieldsValue({ privatePortraitImageLimit: config.imageLimit });
|
||||
setPortraitModal({ open: true, user, config });
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载真人素材库配置失败');
|
||||
setPortraitModal({ open: false, user: null, config: null });
|
||||
} finally {
|
||||
setPortraitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSavePortraitConfig = async () => {
|
||||
const { user } = portraitModal;
|
||||
if (!user) return;
|
||||
try {
|
||||
const values = await portraitForm.validateFields();
|
||||
const limit = Number(values.privatePortraitImageLimit ?? 0);
|
||||
setPortraitSaving(true);
|
||||
const config = await adminUpdatePrivatePortraitConfig(user.id, limit);
|
||||
message.success(limit > 0 ? `真人素材库已开启,限制 ${limit} 张` : '真人素材库已关闭');
|
||||
setPortraitModal({ open: false, user: null, config });
|
||||
portraitForm.resetFields();
|
||||
load();
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) return;
|
||||
message.error(e?.message || '保存真人素材库配置失败');
|
||||
} finally {
|
||||
setPortraitSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openTeamModal = (user: AdminUser) => {
|
||||
teamForm.setFieldsValue({ teamId: user.teamId || '' });
|
||||
setTeamModal({ open: true, user });
|
||||
@@ -380,6 +423,13 @@ const AdminUsers: React.FC = () => {
|
||||
title: '团队', dataIndex: 'teamName', width: 140,
|
||||
render: (v: string | null | undefined) => v ? <Tag color="blue">{v}</Tag> : <Typography.Text type="secondary">未分配</Typography.Text>,
|
||||
}] : []),
|
||||
...(!isAdminTab ? [{
|
||||
title: '真人素材库', dataIndex: 'privatePortraitImageLimit', width: 150,
|
||||
render: (v: number) => {
|
||||
const limit = Number(v || 0);
|
||||
return limit > 0 ? <Tag color="purple">开启:{limit} 张</Tag> : <Tag>未开启</Tag>;
|
||||
},
|
||||
}] : []),
|
||||
...(!isAdminTab ? [{
|
||||
title: '资源容量', dataIndex: 'resourceCapacity', width: 230,
|
||||
render: (capacity: ResourceCapacityUsage | null | undefined) => {
|
||||
@@ -425,7 +475,7 @@ const AdminUsers: React.FC = () => {
|
||||
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 460, fixed: 'right' as const,
|
||||
title: '操作', key: 'action', width: 540, fixed: 'right' as const,
|
||||
render: (_: any, r: AdminUser) => (
|
||||
<Space size={4} wrap>
|
||||
{!isAdminTab && (
|
||||
@@ -440,6 +490,12 @@ const AdminUsers: React.FC = () => {
|
||||
容量设置
|
||||
</Button>
|
||||
)}
|
||||
{!isAdminTab && (
|
||||
<Button type="link" size="small" icon={<PictureOutlined />}
|
||||
onClick={() => openPortraitModal(r)}>
|
||||
真人素材
|
||||
</Button>
|
||||
)}
|
||||
{!isAdminTab && (
|
||||
<Button type="link" size="small" icon={<TeamOutlined />}
|
||||
onClick={() => openTeamModal(r)}>
|
||||
@@ -727,6 +783,37 @@ const AdminUsers: React.FC = () => {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={<Space><PictureOutlined />真人素材库设置 - {portraitModal.user?.username}</Space>}
|
||||
open={portraitModal.open}
|
||||
confirmLoading={portraitSaving}
|
||||
onOk={handleSavePortraitConfig}
|
||||
onCancel={() => { setPortraitModal({ open: false, user: null, config: null }); portraitForm.resetFields(); }}
|
||||
okText="保存" cancelText="取消" width={520}
|
||||
>
|
||||
<Card loading={portraitLoading} variant="outlined" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Typography.Text>
|
||||
当前状态:{portraitModal.config?.enabled ? <Tag color="purple">已开启</Tag> : <Tag>未开启</Tag>}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
已用:{portraitModal.config?.usedImageCount ?? '-'} 张;
|
||||
剩余:{portraitModal.config?.enabled ? portraitModal.config.remainingImageCount : 0} 张
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Card>
|
||||
<Form form={portraitForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="privatePortraitImageLimit"
|
||||
label="真人素材库图片上限"
|
||||
extra="0 表示关闭真人素材库;大于 0 表示开启,并限制该用户所有真人素材图片总量。"
|
||||
rules={[{ required: true, message: '请输入真人素材库图片上限' }]}
|
||||
>
|
||||
<InputNumber min={0} max={9999} precision={0} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={<Space><UserOutlined />创建用户</Space>}
|
||||
open={createModal}
|
||||
@@ -768,6 +855,17 @@ const AdminUsers: React.FC = () => {
|
||||
<InputNumber min={0} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
)}
|
||||
{createType === 'frontend' && (
|
||||
<Form.Item
|
||||
name="private_portrait_image_limit"
|
||||
label="真人素材库图片上限"
|
||||
initialValue={5}
|
||||
extra="0 表示关闭真人素材库;大于 0 表示开启并限制该用户所有真人素材图片总量。"
|
||||
rules={[{ required: true, message: '请输入真人素材库图片上限' }]}
|
||||
>
|
||||
<InputNumber min={0} max={9999} precision={0} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="email" label="邮箱">
|
||||
<Input placeholder="选填" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user