chore: commit pending gateway changes
This commit is contained in:
@@ -0,0 +1,775 @@
|
||||
import { useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
||||
import { Building2, KeyRound, Pencil, Plus, RotateCcw, Trash2, UserRound, UsersRound } from 'lucide-react';
|
||||
import type {
|
||||
GatewayTenant,
|
||||
GatewayTenantUpsertRequest,
|
||||
GatewayUser,
|
||||
GatewayUserUpsertRequest,
|
||||
UserGroup,
|
||||
UserGroupUpsertRequest,
|
||||
} from '@easyai-ai-gateway/contracts';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
ConfirmDialog,
|
||||
FormDialog,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from '../../components/ui';
|
||||
import type { ConsoleData } from '../../app-state';
|
||||
import type { LoadState } from '../../types';
|
||||
|
||||
type TenantForm = {
|
||||
tenantKey: string;
|
||||
source: string;
|
||||
externalTenantId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
defaultUserGroupId: string;
|
||||
planKey: string;
|
||||
billingProfileJson: string;
|
||||
rateLimitPolicyJson: string;
|
||||
authPolicyJson: string;
|
||||
metadataJson: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type UserForm = {
|
||||
userKey: string;
|
||||
source: string;
|
||||
externalUserId: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
avatarUrl: string;
|
||||
password: string;
|
||||
gatewayTenantId: string;
|
||||
tenantId: string;
|
||||
tenantKey: string;
|
||||
defaultUserGroupId: string;
|
||||
roles: string;
|
||||
authProfileJson: string;
|
||||
metadataJson: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type UserGroupForm = {
|
||||
groupKey: string;
|
||||
name: string;
|
||||
description: string;
|
||||
source: string;
|
||||
priority: string;
|
||||
rechargeDiscountPolicyJson: string;
|
||||
billingDiscountPolicyJson: string;
|
||||
rateLimitPolicyJson: string;
|
||||
quotaPolicyJson: string;
|
||||
metadataJson: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const tenantStatuses = ['active', 'disabled', 'locked'];
|
||||
const userStatuses = ['active', 'disabled', 'locked'];
|
||||
const userGroupStatuses = ['active', 'disabled'];
|
||||
const sourceOptions = ['gateway', 'server-main', 'sync'];
|
||||
const roleOptions = [
|
||||
{ label: '普通用户', value: 'user' },
|
||||
{ label: '创作者', value: 'creator' },
|
||||
{ label: '管理', value: 'operator' },
|
||||
{ label: '超级管理员', value: 'manager' },
|
||||
{ label: '管理员', value: 'admin' },
|
||||
];
|
||||
|
||||
export function TenantsPanel(props: IdentityPanelProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState('');
|
||||
const [form, setForm] = useState<TenantForm>(() => defaultTenantForm());
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [pendingDeleteTenant, setPendingDeleteTenant] = useState<GatewayTenant | null>(null);
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
setForm(defaultTenantForm());
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function editTenant(tenant: GatewayTenant) {
|
||||
setEditingId(tenant.id);
|
||||
setLocalError('');
|
||||
setForm(tenantToForm(tenant));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
setDialogOpen(false);
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
}
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setLocalError('');
|
||||
try {
|
||||
await props.onSaveTenant(formToTenantPayload(form), editingId || undefined);
|
||||
closeDialog();
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '保存租户失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTenant(tenant: GatewayTenant) {
|
||||
try {
|
||||
await props.onDeleteTenant(tenant.id);
|
||||
setPendingDeleteTenant(null);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '删除租户失败');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageStack">
|
||||
<IdentityHeader
|
||||
count={props.data.tenants.length}
|
||||
description="租户可独立维护,也可承载 server-main 同步过来的租户标识。"
|
||||
icon={<Building2 size={17} />}
|
||||
message={localError || props.operationMessage}
|
||||
title="租户管理"
|
||||
actionLabel="新增租户"
|
||||
onCreate={openCreateDialog}
|
||||
/>
|
||||
{props.data.tenants.length ? (
|
||||
<Table className="identityDataTable tenantTable">
|
||||
<TableRow>
|
||||
<TableHead>租户</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>默认用户组</TableHead>
|
||||
<TableHead>套餐</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
{props.data.tenants.map((tenant) => (
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell><IdentityName title={tenant.name} subtitle={tenant.tenantKey} /></TableCell>
|
||||
<TableCell>{tenant.source}</TableCell>
|
||||
<TableCell>{groupName(props.data.userGroups, tenant.defaultUserGroupId)}</TableCell>
|
||||
<TableCell>{tenant.planKey || '未设置'}</TableCell>
|
||||
<TableCell><Badge variant={tenant.status === 'active' ? 'success' : 'secondary'}>{tenant.status}</Badge></TableCell>
|
||||
<TableCell><TableActions onEdit={() => editTenant(tenant)} onDelete={() => setPendingDeleteTenant(tenant)} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyIdentity title="暂无租户" description="点击新增租户建立本地租户闭环。" />
|
||||
)}
|
||||
|
||||
<FormDialog
|
||||
ariaLabel={editingId ? '编辑租户' : '新增租户'}
|
||||
className="identityDialog"
|
||||
eyebrow={editingId ? 'Edit Tenant' : 'New Tenant'}
|
||||
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
|
||||
open={dialogOpen}
|
||||
title={editingId ? '编辑租户' : '新增租户'}
|
||||
onClose={closeDialog}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<Label>租户 Key<Input size="sm" value={form.tenantKey} onChange={(event) => setForm({ ...form, tenantKey: event.target.value })} placeholder="default" /></Label>
|
||||
<Label>租户名称<Input size="sm" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} placeholder="默认租户" /></Label>
|
||||
<Label>来源<Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
|
||||
<Label>状态<Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{tenantStatuses.map(option)}</Select></Label>
|
||||
<Label>外部租户 ID<Input size="sm" value={form.externalTenantId} onChange={(event) => setForm({ ...form, externalTenantId: event.target.value })} placeholder="server-main tenant id" /></Label>
|
||||
<Label>套餐 Key<Input size="sm" value={form.planKey} onChange={(event) => setForm({ ...form, planKey: event.target.value })} placeholder="free / pro" /></Label>
|
||||
<Label className="spanTwo">默认用户组<Select size="sm" value={form.defaultUserGroupId} onChange={(event) => setForm({ ...form, defaultUserGroupId: event.target.value })}><option value="">未设置</option>{props.data.userGroups.map((group) => <option value={group.id} key={group.id}>{group.name}</option>)}</Select></Label>
|
||||
<Label className="spanTwo">描述<Input size="sm" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} placeholder="内部备注" /></Label>
|
||||
<JsonField label="计费资料 JSON" value={form.billingProfileJson} onChange={(value) => setForm({ ...form, billingProfileJson: value })} />
|
||||
<JsonField label="限流策略 JSON" value={form.rateLimitPolicyJson} onChange={(value) => setForm({ ...form, rateLimitPolicyJson: value })} />
|
||||
<JsonField label="授权策略 JSON" value={form.authPolicyJson} onChange={(value) => setForm({ ...form, authPolicyJson: value })} />
|
||||
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
|
||||
</FormDialog>
|
||||
<ConfirmDialog
|
||||
confirmLabel="删除租户"
|
||||
description="租户会被软删除,关联用户不会被自动删除。"
|
||||
loading={props.state === 'loading'}
|
||||
open={Boolean(pendingDeleteTenant)}
|
||||
title={`确认删除租户 ${pendingDeleteTenant?.name ?? ''}?`}
|
||||
onCancel={() => setPendingDeleteTenant(null)}
|
||||
onConfirm={() => pendingDeleteTenant ? deleteTenant(pendingDeleteTenant) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UsersPanel(props: IdentityPanelProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState('');
|
||||
const [form, setForm] = useState<UserForm>(() => defaultUserForm(props.data.tenants[0]));
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [pendingDeleteUser, setPendingDeleteUser] = useState<GatewayUser | null>(null);
|
||||
|
||||
const tenantById = useMemo(() => new Map(props.data.tenants.map((tenant) => [tenant.id, tenant])), [props.data.tenants]);
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
setForm(defaultUserForm(props.data.tenants[0]));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function editUser(user: GatewayUser) {
|
||||
setEditingId(user.id);
|
||||
setLocalError('');
|
||||
setForm(userToForm(user));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
setDialogOpen(false);
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
}
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setLocalError('');
|
||||
try {
|
||||
await props.onSaveUser(formToUserPayload(form), editingId || undefined);
|
||||
closeDialog();
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '保存用户失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(user: GatewayUser) {
|
||||
try {
|
||||
await props.onDeleteUser(user.id);
|
||||
setPendingDeleteUser(null);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '删除用户失败');
|
||||
}
|
||||
}
|
||||
|
||||
function selectTenant(gatewayTenantId: string) {
|
||||
const tenant = tenantById.get(gatewayTenantId);
|
||||
setForm({ ...form, gatewayTenantId, tenantKey: tenant?.tenantKey ?? form.tenantKey });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageStack">
|
||||
<IdentityHeader
|
||||
count={props.data.users.length}
|
||||
description="支持本地用户闭环,也保留 server-main 用户同步字段。"
|
||||
icon={<UserRound size={17} />}
|
||||
message={localError || props.operationMessage}
|
||||
title="用户管理"
|
||||
actionLabel="新增用户"
|
||||
onCreate={openCreateDialog}
|
||||
/>
|
||||
{props.data.users.length ? (
|
||||
<Table className="identityDataTable userTable">
|
||||
<TableRow>
|
||||
<TableHead>用户</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>租户</TableHead>
|
||||
<TableHead>用户组</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
{props.data.users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell><IdentityName title={user.displayName || user.username} subtitle={user.email || user.username} /></TableCell>
|
||||
<TableCell>{roleLabel(user.roles)}</TableCell>
|
||||
<TableCell>{tenantName(props.data.tenants, user.gatewayTenantId, user.tenantKey)}</TableCell>
|
||||
<TableCell>{groupName(props.data.userGroups, user.defaultUserGroupId)}</TableCell>
|
||||
<TableCell>{user.source}</TableCell>
|
||||
<TableCell><Badge variant={user.status === 'active' ? 'success' : 'secondary'}>{user.status}</Badge></TableCell>
|
||||
<TableCell><TableActions onEdit={() => editUser(user)} onDelete={() => setPendingDeleteUser(user)} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyIdentity title="暂无用户" description="可先添加本地用户,后续再与 server-main 同步关系对齐。" />
|
||||
)}
|
||||
|
||||
<FormDialog
|
||||
ariaLabel={editingId ? '编辑用户' : '新增用户'}
|
||||
className="identityDialog"
|
||||
eyebrow={editingId ? 'Edit User' : 'New User'}
|
||||
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
|
||||
open={dialogOpen}
|
||||
title={editingId ? '编辑用户' : '新增用户'}
|
||||
onClose={closeDialog}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<Label>用户名<Input size="sm" value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} placeholder="user@example.com" /></Label>
|
||||
<Label>用户 Key<Input size="sm" value={form.userKey} onChange={(event) => setForm({ ...form, userKey: event.target.value })} placeholder="留空自动生成" /></Label>
|
||||
<Label>显示名称<Input size="sm" value={form.displayName} onChange={(event) => setForm({ ...form, displayName: event.target.value })} placeholder="王小明" /></Label>
|
||||
<Label>邮箱<Input size="sm" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} placeholder="name@example.com" /></Label>
|
||||
<Label>手机号<Input size="sm" value={form.phone} onChange={(event) => setForm({ ...form, phone: event.target.value })} /></Label>
|
||||
<Label>头像 URL<Input size="sm" value={form.avatarUrl} onChange={(event) => setForm({ ...form, avatarUrl: event.target.value })} /></Label>
|
||||
<Label>来源<Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
|
||||
<Label>状态<Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{userStatuses.map(option)}</Select></Label>
|
||||
<Label>租户<Select size="sm" value={form.gatewayTenantId} onChange={(event) => selectTenant(event.target.value)}><option value="">未设置</option>{props.data.tenants.map((tenant) => <option value={tenant.id} key={tenant.id}>{tenant.name}</option>)}</Select></Label>
|
||||
<Label>租户 Key<Input size="sm" value={form.tenantKey} onChange={(event) => setForm({ ...form, tenantKey: event.target.value })} /></Label>
|
||||
<Label>外部用户 ID<Input size="sm" value={form.externalUserId} onChange={(event) => setForm({ ...form, externalUserId: event.target.value })} /></Label>
|
||||
<Label>业务租户 ID<Input size="sm" value={form.tenantId} onChange={(event) => setForm({ ...form, tenantId: event.target.value })} /></Label>
|
||||
<Label>默认用户组<Select size="sm" value={form.defaultUserGroupId} onChange={(event) => setForm({ ...form, defaultUserGroupId: event.target.value })}><option value="">未设置</option>{props.data.userGroups.map((group) => <option value={group.id} key={group.id}>{group.name}</option>)}</Select></Label>
|
||||
<Label>登录密码<Input size="sm" value={form.password} type="password" onChange={(event) => setForm({ ...form, password: event.target.value })} placeholder={editingId ? '留空不修改' : '可为空'} /></Label>
|
||||
<Label>
|
||||
角色
|
||||
<Select size="sm" value={form.roles} onChange={(event) => setForm({ ...form, roles: event.target.value })}>
|
||||
{roleOptions.map((role) => <option value={role.value} key={role.value}>{role.label}</option>)}
|
||||
</Select>
|
||||
</Label>
|
||||
<JsonField label="授权资料 JSON" value={form.authProfileJson} onChange={(value) => setForm({ ...form, authProfileJson: value })} />
|
||||
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
|
||||
</FormDialog>
|
||||
<ConfirmDialog
|
||||
confirmLabel="删除用户"
|
||||
description="用户会被软删除,已创建的任务记录会保留。"
|
||||
loading={props.state === 'loading'}
|
||||
open={Boolean(pendingDeleteUser)}
|
||||
title={`确认删除用户 ${pendingDeleteUser?.username ?? ''}?`}
|
||||
onCancel={() => setPendingDeleteUser(null)}
|
||||
onConfirm={() => pendingDeleteUser ? deleteUser(pendingDeleteUser) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserGroupsPanel(props: IdentityPanelProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState('');
|
||||
const [form, setForm] = useState<UserGroupForm>(() => defaultUserGroupForm());
|
||||
const [localError, setLocalError] = useState('');
|
||||
const [pendingDeleteGroup, setPendingDeleteGroup] = useState<UserGroup | null>(null);
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
setForm(defaultUserGroupForm());
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function editGroup(group: UserGroup) {
|
||||
setEditingId(group.id);
|
||||
setLocalError('');
|
||||
setForm(userGroupToForm(group));
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
setDialogOpen(false);
|
||||
setEditingId('');
|
||||
setLocalError('');
|
||||
}
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setLocalError('');
|
||||
try {
|
||||
await props.onSaveUserGroup(formToUserGroupPayload(form), editingId || undefined);
|
||||
closeDialog();
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '保存用户组失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGroup(group: UserGroup) {
|
||||
try {
|
||||
await props.onDeleteUserGroup(group.id);
|
||||
setPendingDeleteGroup(null);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '删除用户组失败');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageStack">
|
||||
<IdentityHeader
|
||||
count={props.data.userGroups.length}
|
||||
description="用户组承载充值折扣、计费折扣、限流和额度策略。"
|
||||
icon={<UsersRound size={17} />}
|
||||
message={localError || props.operationMessage}
|
||||
title="用户组管理"
|
||||
actionLabel="新增用户组"
|
||||
onCreate={openCreateDialog}
|
||||
/>
|
||||
{props.data.userGroups.length ? (
|
||||
<Table className="identityDataTable groupTable">
|
||||
<TableRow>
|
||||
<TableHead>用户组</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>优先级</TableHead>
|
||||
<TableHead>折扣</TableHead>
|
||||
<TableHead>限流</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
{props.data.userGroups.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell><IdentityName title={group.name} subtitle={group.groupKey} /></TableCell>
|
||||
<TableCell>{group.source}</TableCell>
|
||||
<TableCell>{group.priority}</TableCell>
|
||||
<TableCell>{discountSummary(group)}</TableCell>
|
||||
<TableCell>{policyKeys(group.rateLimitPolicy).join(', ') || '未设置'}</TableCell>
|
||||
<TableCell><Badge variant={group.status === 'active' ? 'success' : 'secondary'}>{group.status}</Badge></TableCell>
|
||||
<TableCell><TableActions onEdit={() => editGroup(group)} onDelete={() => setPendingDeleteGroup(group)} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyIdentity title="暂无用户组" description="可先新增默认用户组,用来配置折扣和并发控制。" />
|
||||
)}
|
||||
|
||||
<FormDialog
|
||||
ariaLabel={editingId ? '编辑用户组' : '新增用户组'}
|
||||
className="identityDialog"
|
||||
eyebrow={editingId ? 'Edit User Group' : 'New User Group'}
|
||||
footer={<DialogFooter editing={Boolean(editingId)} loading={props.state === 'loading'} onCancel={closeDialog} />}
|
||||
open={dialogOpen}
|
||||
title={editingId ? '编辑用户组' : '新增用户组'}
|
||||
onClose={closeDialog}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<Label>用户组 Key<Input size="sm" value={form.groupKey} onChange={(event) => setForm({ ...form, groupKey: event.target.value })} placeholder="default" /></Label>
|
||||
<Label>名称<Input size="sm" value={form.name} onChange={(event) => setForm({ ...form, name: event.target.value })} placeholder="默认用户组" /></Label>
|
||||
<Label>来源<Select size="sm" value={form.source} onChange={(event) => setForm({ ...form, source: event.target.value })}>{sourceOptions.map(option)}</Select></Label>
|
||||
<Label>状态<Select size="sm" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>{userGroupStatuses.map(option)}</Select></Label>
|
||||
<Label>优先级<Input size="sm" value={form.priority} inputMode="numeric" onChange={(event) => setForm({ ...form, priority: event.target.value })} /></Label>
|
||||
<Label className="spanTwo">描述<Input size="sm" value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} /></Label>
|
||||
<JsonField label="充值折扣策略 JSON" value={form.rechargeDiscountPolicyJson} onChange={(value) => setForm({ ...form, rechargeDiscountPolicyJson: value })} />
|
||||
<JsonField label="计费折扣策略 JSON" value={form.billingDiscountPolicyJson} onChange={(value) => setForm({ ...form, billingDiscountPolicyJson: value })} />
|
||||
<JsonField label="限流策略 JSON" value={form.rateLimitPolicyJson} onChange={(value) => setForm({ ...form, rateLimitPolicyJson: value })} />
|
||||
<JsonField label="额度策略 JSON" value={form.quotaPolicyJson} onChange={(value) => setForm({ ...form, quotaPolicyJson: value })} />
|
||||
<JsonField label="元数据 JSON" value={form.metadataJson} onChange={(value) => setForm({ ...form, metadataJson: value })} />
|
||||
</FormDialog>
|
||||
<ConfirmDialog
|
||||
confirmLabel="删除用户组"
|
||||
description="删除后租户和用户上引用该用户组的默认策略会失效。"
|
||||
loading={props.state === 'loading'}
|
||||
open={Boolean(pendingDeleteGroup)}
|
||||
title={`确认删除用户组 ${pendingDeleteGroup?.name ?? ''}?`}
|
||||
onCancel={() => setPendingDeleteGroup(null)}
|
||||
onConfirm={() => pendingDeleteGroup ? deleteGroup(pendingDeleteGroup) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type IdentityPanelProps = {
|
||||
data: ConsoleData;
|
||||
operationMessage: string;
|
||||
state: LoadState;
|
||||
onDeleteTenant: (tenantId: string) => Promise<void>;
|
||||
onDeleteUser: (userId: string) => Promise<void>;
|
||||
onDeleteUserGroup: (groupId: string) => Promise<void>;
|
||||
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
|
||||
onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>;
|
||||
onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function IdentityHeader(props: {
|
||||
actionLabel: string;
|
||||
count: number;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
message?: string;
|
||||
title: string;
|
||||
onCreate: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="identityHeaderTitle">
|
||||
<div className="iconBox">{props.icon}</div>
|
||||
<div>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
<p className="mutedText">{props.description}</p>
|
||||
</div>
|
||||
<Badge variant="secondary">{props.count}</Badge>
|
||||
</div>
|
||||
<Button type="button" onClick={props.onCreate}><Plus size={15} />{props.actionLabel}</Button>
|
||||
</CardHeader>
|
||||
{props.message && <CardContent><p className="formMessage">{props.message}</p></CardContent>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function IdentityName(props: { title: string; subtitle: string }) {
|
||||
return (
|
||||
<span className="identityTableName">
|
||||
<strong>{props.title}</strong>
|
||||
<small>{props.subtitle}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function TableActions(props: { onDelete: () => void; onEdit: () => void }) {
|
||||
return (
|
||||
<span className="tableActions">
|
||||
<Button type="button" variant="outline" size="icon" title="修改" onClick={props.onEdit}><Pencil size={14} /></Button>
|
||||
<Button type="button" variant="destructive" size="icon" title="删除" onClick={props.onDelete}><Trash2 size={14} /></Button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyIdentity(props: { title: string; description: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="emptyState">
|
||||
<KeyRound size={18} />
|
||||
<strong>{props.title}</strong>
|
||||
<span>{props.description}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter(props: { editing: boolean; loading: boolean; onCancel: () => void }) {
|
||||
return (
|
||||
<>
|
||||
<Button type="button" variant="outline" onClick={props.onCancel}><RotateCcw size={15} />取消</Button>
|
||||
<Button type="submit" disabled={props.loading}>{props.editing ? <Pencil size={15} /> : <Plus size={15} />}{props.editing ? '保存修改' : '新增'}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function JsonField(props: { label: string; value: string; onChange: (value: string) => void }) {
|
||||
return (
|
||||
<Label className="spanTwo">
|
||||
{props.label}
|
||||
<Textarea size="sm" rows={3} value={props.value} placeholder='{"rules":[]}' onChange={(event) => props.onChange(event.target.value)} />
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
function option(value: string) {
|
||||
return <option value={value} key={value}>{value}</option>;
|
||||
}
|
||||
|
||||
function defaultTenantForm(): TenantForm {
|
||||
return {
|
||||
tenantKey: `tenant-${Date.now().toString(36)}`,
|
||||
source: 'gateway',
|
||||
externalTenantId: '',
|
||||
name: '默认租户',
|
||||
description: '',
|
||||
defaultUserGroupId: '',
|
||||
planKey: '',
|
||||
billingProfileJson: '{}',
|
||||
rateLimitPolicyJson: '{}',
|
||||
authPolicyJson: '{}',
|
||||
metadataJson: '{}',
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
function tenantToForm(tenant: GatewayTenant): TenantForm {
|
||||
return {
|
||||
tenantKey: tenant.tenantKey,
|
||||
source: tenant.source,
|
||||
externalTenantId: tenant.externalTenantId ?? '',
|
||||
name: tenant.name,
|
||||
description: tenant.description ?? '',
|
||||
defaultUserGroupId: tenant.defaultUserGroupId ?? '',
|
||||
planKey: tenant.planKey ?? '',
|
||||
billingProfileJson: stringifyJson(tenant.billingProfile),
|
||||
rateLimitPolicyJson: stringifyJson(tenant.rateLimitPolicy),
|
||||
authPolicyJson: stringifyJson(tenant.authPolicy),
|
||||
metadataJson: stringifyJson(tenant.metadata),
|
||||
status: tenant.status,
|
||||
};
|
||||
}
|
||||
|
||||
function formToTenantPayload(form: TenantForm): GatewayTenantUpsertRequest {
|
||||
return {
|
||||
tenantKey: form.tenantKey.trim(),
|
||||
source: form.source,
|
||||
externalTenantId: form.externalTenantId.trim() || undefined,
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim() || undefined,
|
||||
defaultUserGroupId: form.defaultUserGroupId || undefined,
|
||||
planKey: form.planKey.trim() || undefined,
|
||||
billingProfile: parseJsonObject(form.billingProfileJson, '计费资料 JSON'),
|
||||
rateLimitPolicy: parseJsonObject(form.rateLimitPolicyJson, '限流策略 JSON'),
|
||||
authPolicy: parseJsonObject(form.authPolicyJson, '授权策略 JSON'),
|
||||
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
|
||||
status: form.status,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultUserForm(tenant?: GatewayTenant): UserForm {
|
||||
return {
|
||||
userKey: '',
|
||||
source: 'gateway',
|
||||
externalUserId: '',
|
||||
username: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
avatarUrl: '',
|
||||
password: '',
|
||||
gatewayTenantId: tenant?.id ?? '',
|
||||
tenantId: '',
|
||||
tenantKey: tenant?.tenantKey ?? '',
|
||||
defaultUserGroupId: '',
|
||||
roles: 'user',
|
||||
authProfileJson: '{}',
|
||||
metadataJson: '{}',
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
function userToForm(user: GatewayUser): UserForm {
|
||||
return {
|
||||
userKey: user.userKey,
|
||||
source: user.source,
|
||||
externalUserId: user.externalUserId ?? '',
|
||||
username: user.username,
|
||||
displayName: user.displayName ?? '',
|
||||
email: user.email ?? '',
|
||||
phone: user.phone ?? '',
|
||||
avatarUrl: user.avatarUrl ?? '',
|
||||
password: '',
|
||||
gatewayTenantId: user.gatewayTenantId ?? '',
|
||||
tenantId: user.tenantId ?? '',
|
||||
tenantKey: user.tenantKey ?? '',
|
||||
defaultUserGroupId: user.defaultUserGroupId ?? '',
|
||||
roles: roleValue(user.roles),
|
||||
authProfileJson: stringifyJson(user.authProfile),
|
||||
metadataJson: stringifyJson(user.metadata),
|
||||
status: user.status,
|
||||
};
|
||||
}
|
||||
|
||||
function formToUserPayload(form: UserForm): GatewayUserUpsertRequest {
|
||||
return {
|
||||
userKey: form.userKey.trim() || undefined,
|
||||
source: form.source,
|
||||
externalUserId: form.externalUserId.trim() || undefined,
|
||||
username: form.username.trim(),
|
||||
displayName: form.displayName.trim() || undefined,
|
||||
email: form.email.trim() || undefined,
|
||||
phone: form.phone.trim() || undefined,
|
||||
avatarUrl: form.avatarUrl.trim() || undefined,
|
||||
password: form.password || undefined,
|
||||
gatewayTenantId: form.gatewayTenantId || undefined,
|
||||
tenantId: form.tenantId.trim() || undefined,
|
||||
tenantKey: form.tenantKey.trim() || undefined,
|
||||
defaultUserGroupId: form.defaultUserGroupId || undefined,
|
||||
roles: [roleValue([form.roles])],
|
||||
authProfile: parseJsonObject(form.authProfileJson, '授权资料 JSON'),
|
||||
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
|
||||
status: form.status,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultUserGroupForm(): UserGroupForm {
|
||||
return {
|
||||
groupKey: `group-${Date.now().toString(36)}`,
|
||||
name: '默认用户组',
|
||||
description: '',
|
||||
source: 'gateway',
|
||||
priority: '100',
|
||||
rechargeDiscountPolicyJson: '{}',
|
||||
billingDiscountPolicyJson: '{}',
|
||||
rateLimitPolicyJson: '{"rules":[]}',
|
||||
quotaPolicyJson: '{}',
|
||||
metadataJson: '{}',
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
function userGroupToForm(group: UserGroup): UserGroupForm {
|
||||
return {
|
||||
groupKey: group.groupKey,
|
||||
name: group.name,
|
||||
description: group.description ?? '',
|
||||
source: group.source,
|
||||
priority: String(group.priority),
|
||||
rechargeDiscountPolicyJson: stringifyJson(group.rechargeDiscountPolicy),
|
||||
billingDiscountPolicyJson: stringifyJson(group.billingDiscountPolicy),
|
||||
rateLimitPolicyJson: stringifyJson(group.rateLimitPolicy),
|
||||
quotaPolicyJson: stringifyJson(group.quotaPolicy),
|
||||
metadataJson: stringifyJson(group.metadata),
|
||||
status: group.status,
|
||||
};
|
||||
}
|
||||
|
||||
function formToUserGroupPayload(form: UserGroupForm): UserGroupUpsertRequest {
|
||||
return {
|
||||
groupKey: form.groupKey.trim(),
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim() || undefined,
|
||||
source: form.source,
|
||||
priority: Number(form.priority) || 100,
|
||||
rechargeDiscountPolicy: parseJsonObject(form.rechargeDiscountPolicyJson, '充值折扣策略 JSON'),
|
||||
billingDiscountPolicy: parseJsonObject(form.billingDiscountPolicyJson, '计费折扣策略 JSON'),
|
||||
rateLimitPolicy: parseJsonObject(form.rateLimitPolicyJson, '限流策略 JSON'),
|
||||
quotaPolicy: parseJsonObject(form.quotaPolicyJson, '额度策略 JSON'),
|
||||
metadata: parseJsonObject(form.metadataJson, '元数据 JSON'),
|
||||
status: form.status,
|
||||
};
|
||||
}
|
||||
|
||||
function tenantName(tenants: GatewayTenant[], tenantId?: string, fallback?: string) {
|
||||
if (!tenantId) return fallback || '未设置';
|
||||
return tenants.find((tenant) => tenant.id === tenantId)?.name ?? fallback ?? tenantId;
|
||||
}
|
||||
|
||||
function groupName(groups: UserGroup[], groupId?: string) {
|
||||
if (!groupId) return '未设置';
|
||||
return groups.find((group) => group.id === groupId)?.name ?? groupId;
|
||||
}
|
||||
|
||||
function roleValue(roles?: string[]) {
|
||||
if (roles?.includes('manager')) return 'manager';
|
||||
if (roles?.includes('admin')) return 'admin';
|
||||
if (roles?.includes('operator')) return 'operator';
|
||||
if (roles?.includes('creator')) return 'creator';
|
||||
return 'user';
|
||||
}
|
||||
|
||||
function roleLabel(roles?: string[]) {
|
||||
return roleOptions.find((role) => role.value === roleValue(roles))?.label ?? '普通用户';
|
||||
}
|
||||
|
||||
function discountSummary(group: UserGroup) {
|
||||
const billing = group.billingDiscountPolicy?.discountFactor ?? group.billingDiscountPolicy?.factor;
|
||||
const recharge = group.rechargeDiscountPolicy?.discountFactor ?? group.rechargeDiscountPolicy?.factor;
|
||||
const parts = [];
|
||||
if (billing) parts.push(`计费 ${billing}`);
|
||||
if (recharge) parts.push(`充值 ${recharge}`);
|
||||
return parts.join(' / ') || '未设置';
|
||||
}
|
||||
|
||||
function policyKeys(value?: Record<string, unknown>) {
|
||||
if (!value) return [];
|
||||
return Object.keys(value).slice(0, 3);
|
||||
}
|
||||
|
||||
function stringifyJson(value: unknown) {
|
||||
if (!value || (typeof value === 'object' && Object.keys(value as Record<string, unknown>).length === 0)) return '{}';
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function parseJsonObject(value: string, label: string): Record<string, unknown> | undefined {
|
||||
const text = value.trim();
|
||||
if (!text || text === '{}') return undefined;
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error(`${label} 必须是 JSON 对象`);
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
Reference in New Issue
Block a user