真人素材库本地完成

This commit is contained in:
2026-07-06 15:13:41 +08:00
parent 75d1f7a428
commit ab67bdf19e
52 changed files with 4131 additions and 137 deletions
+101 -3
View File
@@ -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>