Files
affiliate_dash/frontend/src/pages/MerchantCenter.tsx
T

686 lines
27 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import {
Button,
Form,
Input,
InputNumber,
Modal,
Select,
Switch,
Tabs,
Typography,
message,
} from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import { useAuth } from '../store/auth'
import { merchantApi } from '../api'
import { PageHeader } from '../components/PageHeader'
import type {
ApiClient,
ApiCredential,
CallbackCredential,
CallbackSubscription,
FulfillmentOrder,
Merchant,
MerchantMember,
MerchantRole,
MerchantProduct,
PageResult,
WalletAccount,
WalletLedgerEntry,
} from '../types'
import {
apiClientMax,
defaultCallbackFormValues,
eventsToValue,
featuresToList,
productOptionLabel,
resolveEnabledTab,
rolePermissionOptions,
roleText,
scopeOptions,
tabFromSearch,
} from './merchantCenterUtils'
import type { MerchantCenterProps, MerchantCenterTab } from './merchantCenterUtils'
import {
ApiKeysTab,
CallbacksTab,
MembersTab,
OrdersTab,
ProductsTab,
SecretBlock,
WalletTab,
} from './merchantCenterTabs'
export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) {
const { isAdmin, user } = useAuth()
const location = useLocation()
const navigate = useNavigate()
const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search])
const [merchant, setMerchant] = useState<Merchant | null>(null)
const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>()
const [merchantPermissions, setMerchantPermissions] = useState<string[]>([])
const [activeTab, setActiveTab] = useState<MerchantCenterTab>(routeTab ?? 'products')
const [products, setProducts] = useState<PageResult<MerchantProduct>>({ list: [], total: 0, page: 1, size: 10 })
const [orders, setOrders] = useState<PageResult<FulfillmentOrder>>({ list: [], total: 0, page: 1, size: 10 })
const [orderSource, setOrderSource] = useState<'api' | 'manual'>('api')
const [ledger, setLedger] = useState<PageResult<WalletLedgerEntry>>({ list: [], total: 0, page: 1, size: 10 })
const [wallet, setWallet] = useState<WalletAccount | null>(null)
const [apiClients, setApiClients] = useState<ApiClient[]>([])
const [callback, setCallback] = useState<CallbackSubscription | null>(null)
const [members, setMembers] = useState<MerchantMember[]>([])
const [roles, setRoles] = useState<MerchantRole[]>([])
const [loading, setLoading] = useState(false)
const [apiClientOpen, setApiClientOpen] = useState(false)
const [apiCredential, setApiCredential] = useState<ApiCredential | null>(null)
const [callbackCredential, setCallbackCredential] = useState<CallbackCredential | null>(null)
const [memberOpen, setMemberOpen] = useState(false)
const [editingMember, setEditingMember] = useState<MerchantMember | null>(null)
const [roleOpen, setRoleOpen] = useState(false)
const [editingRole, setEditingRole] = useState<MerchantRole | null>(null)
const [manualOrderOpen, setManualOrderOpen] = useState(false)
const [manualOrderSubmitting, setManualOrderSubmitting] = useState(false)
const [manualOrderProducts, setManualOrderProducts] = useState<MerchantProduct[]>([])
const [manualOrderProductsLoading, setManualOrderProductsLoading] = useState(false)
const [walletFilterForm] = Form.useForm()
const [apiClientForm] = Form.useForm()
const [callbackForm] = Form.useForm()
const [memberForm] = Form.useForm()
const [memberEditForm] = Form.useForm()
const [roleForm] = Form.useForm()
const [manualOrderForm] = Form.useForm()
const hasPermission = useCallback((permission: string) => merchantPermissions.includes(permission), [merchantPermissions])
const canManage = hasPermission('products:manage')
const canOrderManage = hasPermission('orders:manage')
const canAPIManage = hasPermission('api:manage')
const canCallbacksManage = hasPermission('callbacks:manage')
const canFinance = hasPermission('wallet:ledger')
const enabledFeatures = useMemo(() => new Set(featuresToList(merchant?.features)), [merchant?.features])
const hasFeature = useCallback((feature: string) => !merchant || enabledFeatures.has(feature), [enabledFeatures, merchant])
const manualOrderProductOptions = useMemo(() => manualOrderProducts.map((item) => ({
value: item.sku,
label: productOptionLabel(item),
})), [manualOrderProducts])
const copyDeliveryLink = useCallback((orderNo: string) => {
merchantApi.getDeliveryLink(orderNo)
.then((data) => navigator.clipboard.writeText(data.delivery_url).then(() => message.success('发货链接已复制')))
.catch((e) => message.error(e instanceof Error ? e.message : '复制失败'))
}, [])
const openDeliveryLink = useCallback((orderNo: string) => {
merchantApi.getDeliveryLink(orderNo)
.then((data) => window.open(data.delivery_url, '_blank', 'noopener,noreferrer'))
.catch((e) => message.error(e instanceof Error ? e.message : '打开失败'))
}, [])
const patchDeliveryLinkState = useCallback((orderNo: string, patch: Partial<FulfillmentOrder>) => {
setOrders((prev) => ({
...prev,
list: prev.list.map((item) => item.order_no === orderNo ? { ...item, ...patch } : item),
}))
}, [])
const revokeDeliveryLink = useCallback((orderNo: string) => {
merchantApi.revokeDeliveryLink(orderNo)
.then(() => {
const revokedAt = new Date().toISOString()
message.success('发货链接已作废')
patchDeliveryLinkState(orderNo, { delivery_link_revoked_at: revokedAt })
})
.catch((e) => message.error(e instanceof Error ? e.message : '作废失败'))
}, [patchDeliveryLinkState])
const restoreDeliveryLink = useCallback((orderNo: string) => {
merchantApi.restoreDeliveryLink(orderNo)
.then((data) => {
message.success('发货链接已恢复')
patchDeliveryLinkState(orderNo, {
delivery_link_expires_at: data.expires_at,
delivery_link_revoked_at: null,
})
})
.catch((e) => message.error(e instanceof Error ? e.message : '恢复失败'))
}, [patchDeliveryLinkState])
const loadCurrent = useCallback(async () => {
const data = await merchantApi.current()
setMerchant(data.merchant)
setMerchantRole(data.role)
const nextPermissions = [...new Set(data.permissions || [])].sort()
setMerchantPermissions((current) => (
current.length === nextPermissions.length && current.every((value, index) => value === nextPermissions[index])
? current
: nextPermissions
))
return data
}, [])
const loadProducts = useCallback(async (page = products.page, size = products.size) => {
const data = await merchantApi.products({ page, size })
setProducts(data)
}, [products.page, products.size])
const loadOrders = useCallback(async (page = orders.page, size = orders.size, source = orderSource) => {
const data = await merchantApi.orders({ page, size, order_source: source })
setOrders(data)
}, [orderSource, orders.page, orders.size])
const loadWallet = useCallback(async (
page = ledger.page,
size = ledger.size,
permissions = merchantPermissions,
filters?: { reference_no?: string; type?: string },
) => {
const walletData = await merchantApi.wallet()
setWallet(walletData)
if (permissions.includes('wallet:ledger')) {
const ledgerData = await merchantApi.ledger({ page, size, ...filters })
setLedger(ledgerData)
} else {
setLedger({ list: [], total: 0, page, size })
}
}, [ledger.page, ledger.size, merchantPermissions])
const loadAPIClients = useCallback(async () => {
const clientData = await merchantApi.apiClients()
setApiClients(clientData || [])
}, [])
const loadCallbacks = useCallback(async (permissions = merchantPermissions) => {
if (!permissions.includes('callbacks:manage')) {
setCallback(null)
callbackForm.resetFields()
return
}
const callbackData = await merchantApi.callbacks()
setCallback(callbackData || null)
callbackForm.setFieldsValue(callbackData ? {
url: callbackData.url,
events: eventsToValue(callbackData.events),
status: callbackData.status,
} : defaultCallbackFormValues())
}, [callbackForm, merchantPermissions])
const loadMembers = useCallback(async (permissions = merchantPermissions) => {
if (!permissions.includes('members:manage')) {
setMembers([])
setRoles([])
return
}
const [memberData, roleData] = await Promise.all([merchantApi.members(), merchantApi.roles()])
setMembers(memberData || [])
setRoles(roleData || [])
}, [merchantPermissions])
const loadActiveTab = useCallback(async (permissions = merchantPermissions, tab = activeTab) => {
switch (tab) {
case 'products':
await loadProducts()
return
case 'orders':
await loadOrders()
return
case 'wallet':
await loadWallet(undefined, undefined, permissions)
return
case 'api':
await loadAPIClients()
return
case 'callbacks':
await loadCallbacks(permissions)
return
case 'members':
await loadMembers(permissions)
return
}
}, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantPermissions])
const loadAll = useCallback(async () => {
setLoading(true)
try {
const current = await loadCurrent()
const nextTab = fixedTab ?? resolveEnabledTab(routeTab ?? activeTab, current.merchant.features)
if (!fixedTab && nextTab !== activeTab) {
setActiveTab(nextTab)
}
await loadActiveTab(current.permissions || [], nextTab)
} catch (e) {
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
setLoading(false)
}
}, [activeTab, fixedTab, loadActiveTab, loadCurrent, routeTab])
useEffect(() => {
loadAll()
}, [loadAll])
useEffect(() => {
if (routeTab && activeTab !== routeTab) {
setActiveTab(routeTab)
}
}, [activeTab, routeTab])
const openProductToggle = (record: MerchantProduct) => {
merchantApi.updateProductStatus(record.id, record.status === 'active' ? 'inactive' : 'active')
.then(() => {
message.success(record.status === 'active' ? '商品已下架,商户无法再下单' : '商品已上架')
loadProducts()
})
.catch((e) => message.error(e instanceof Error ? e.message : '操作失败'))
}
const handleWalletFilter = async () => {
const values = await walletFilterForm.validateFields()
loadWallet(1, ledger.size, merchantPermissions, {
reference_no: values.reference_no || undefined,
type: values.type || undefined,
})
}
const handleWalletFilterReset = () => {
walletFilterForm.resetFields()
loadWallet(1, ledger.size, merchantPermissions, {})
}
const handleLoadLedger = useCallback((page: number, size: number) => {
const values = walletFilterForm.getFieldsValue()
loadWallet(page, size, merchantPermissions, {
reference_no: values.reference_no || undefined,
type: values.type || undefined,
})
}, [loadWallet, merchantPermissions, walletFilterForm])
const openManualOrderCreate = () => {
manualOrderForm.resetFields()
manualOrderForm.setFieldsValue({
quantity: 1,
})
setManualOrderOpen(true)
setManualOrderProductsLoading(true)
merchantApi.manualOrderProducts()
.then((data) => {
setManualOrderProducts(data || [])
if (data.length > 0) {
manualOrderForm.setFieldsValue({ sku: data[0].sku })
} else {
message.warning('没有可用商品')
}
})
.catch((e) => {
message.error(e instanceof Error ? e.message : '商品加载失败')
})
.finally(() => setManualOrderProductsLoading(false))
}
const submitManualOrder = async () => {
const values = await manualOrderForm.validateFields()
setManualOrderSubmitting(true)
try {
const result = await merchantApi.createManualOrder({
client_order_no: values.client_order_no,
sku: values.sku,
quantity: values.quantity,
buyer_reference: values.buyer_reference,
game_account: values.game_account,
note: values.note,
})
setManualOrderOpen(false)
setOrderSource('manual')
message.success(result.idempotent ? '已找到已有手动订单,未重复扣款' : '手动订单已创建,余额与库存已扣减')
await loadOrders(1, orders.size, 'manual')
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
} finally {
setManualOrderSubmitting(false)
}
}
const changeOrderSource = (source: 'api' | 'manual') => {
setOrderSource(source)
loadOrders(1, orders.size, source).catch((e) => message.error(e instanceof Error ? e.message : '订单加载失败'))
}
const submitAPIClient = async () => {
const values = await apiClientForm.validateFields()
try {
const credential = await merchantApi.createApiClient({
name: values.name,
scopes: (values.scopes || []).join(','),
signature_version: values.signature_version,
expires_at: values.expires_at,
})
setApiCredential(credential)
setApiClientOpen(false)
message.success('API 密钥已创建')
loadAPIClients()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
}
}
const submitCallback = async () => {
const values = await callbackForm.validateFields()
try {
const credential = await merchantApi.saveCallback({
url: values.url,
events: (values.events || []).join(','),
status: values.status,
})
setCallback(credential.subscription)
if (credential.secret) {
setCallbackCredential(credential)
}
message.success('回调配置已保存')
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
}
}
const rotateCallbackSecret = async () => {
const values = await callbackForm.validateFields()
try {
const credential = await merchantApi.saveCallback({
url: values.url,
events: (values.events || []).join(','),
status: values.status,
rotate_secret: true,
})
setCallback(credential.subscription)
if (credential.secret) {
setCallbackCredential(credential)
}
message.success('回调密钥已重置,旧密钥立即失效')
} catch (e) {
message.error(e instanceof Error ? e.message : '重置失败')
}
}
const submitMember = async () => {
const values = await memberForm.validateFields()
try {
await merchantApi.addMember({
username: values.username,
password: values.password,
nickname: values.nickname,
role: values.role,
})
setMemberOpen(false)
message.success('成员已添加')
loadMembers()
} catch (e) {
message.error(e instanceof Error ? e.message : '添加失败')
}
}
const submitMemberEdit = async () => {
if (!editingMember) return
try {
const values = await memberEditForm.validateFields()
await merchantApi.updateMember(editingMember.id, {
role: values.role,
status: values.status,
is_default: values.is_default || false,
})
setEditingMember(null)
message.success('成员已更新')
loadMembers()
} catch (e) {
message.error(e instanceof Error ? e.message : '更新失败')
}
}
const toggleMemberStatus = async (member: MerchantMember) => {
try {
await merchantApi.updateMember(member.id, {
role: member.role,
status: member.status === 1 ? 0 : 1,
is_default: member.is_default,
})
message.success(member.status === 1 ? '员工已停用' : '员工已启用')
loadMembers()
} catch (e) {
message.error(e instanceof Error ? e.message : '操作失败')
}
}
const removeMember = async (member: MerchantMember) => {
try {
await merchantApi.removeMember(member.id)
message.success('员工已从当前商户移除')
loadMembers()
} catch (e) {
message.error(e instanceof Error ? e.message : '移除失败')
}
}
const submitRole = async () => {
const values = await roleForm.validateFields()
try {
if (editingRole) {
await merchantApi.updateRole(editingRole.id, { name: values.name, permissions: values.permissions || [] })
} else {
await merchantApi.createRole({ ...values, permissions: values.permissions || [] })
}
setRoleOpen(false)
setEditingRole(null)
message.success(editingRole ? '角色已更新' : '角色已创建')
loadMembers()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
}
}
const toggleAPIClient = async (record: ApiClient) => {
try {
await merchantApi.updateApiClientStatus(record.id, record.status === 'active' ? 'disabled' : 'active')
message.success('状态已更新')
loadAPIClients()
} catch (e) {
message.error(e instanceof Error ? e.message : '更新失败')
}
}
const deleteAPIClient = async (record: ApiClient) => {
try {
await merchantApi.deleteApiClient(record.id)
message.success('密钥已删除')
loadAPIClients()
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
}
}
const tabItems = [
{ key: 'products', label: '商品列表', disabled: !hasFeature('products'), children: (
<ProductsTab loading={loading} products={products} canManage={canManage} onToggleProduct={openProductToggle} onLoadProducts={loadProducts} />
) },
{ key: 'orders', label: '发货订单', disabled: !hasFeature('orders'), children: (
<OrdersTab loading={loading} orders={orders} orderSource={orderSource} canManage={canOrderManage} onCreateManualOrder={openManualOrderCreate} onOrderSourceChange={changeOrderSource} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} />
) },
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: (
<WalletTab loading={loading} wallet={wallet} canFinance={canFinance} ledger={ledger} form={walletFilterForm} onFilter={handleWalletFilter} onResetFilter={handleWalletFilterReset} onLoadLedger={handleLoadLedger} />
) },
{ key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: (
<ApiKeysTab loading={loading} clients={apiClients} clientMax={apiClientMax} isAdmin={isAdmin} canManage={canAPIManage} form={apiClientForm} onOpenCreate={() => setApiClientOpen(true)} onNavigateDocs={() => navigate('/open-api')} onNavigateDebug={() => navigate('/api-debug')} onToggleClient={toggleAPIClient} onDeleteClient={deleteAPIClient} />
) },
{ key: 'callbacks', label: '回调', disabled: !hasFeature('callbacks'), children: (
<CallbacksTab canManage={canCallbacksManage} callback={callback} form={callbackForm} onSubmit={submitCallback} onRotateSecret={rotateCallbackSecret} />
) },
{ key: 'members', label: '成员', children: (
<MembersTab
loading={loading}
members={members}
roles={roles}
canManage={hasPermission('members:manage')}
form={memberForm}
onOpenAdd={() => setMemberOpen(true)}
onOpenCreateRole={() => {
setEditingRole(null)
setRoleOpen(true)
}}
onEditRole={(role) => {
setEditingRole(role)
roleForm.setFieldsValue({
name: role.name,
code: role.code,
permissions: role.permissions.split(',').filter(Boolean),
})
setRoleOpen(true)
}}
onEditMember={(member) => {
setEditingMember(member)
memberEditForm.setFieldsValue({ role: member.role, status: member.status, is_default: member.is_default })
}}
onToggleMemberStatus={toggleMemberStatus}
onRemoveMember={removeMember}
currentUserID={user?.id}
/>
) },
]
const fixedTabContent = tabItems.find((item) => item.key === fixedTab)?.children
const parentCategoryMap: Record<string, string> = {
products: '商品管理',
orders: '订单管理',
wallet: '积分管理',
api: '开放 API',
callbacks: '开放 API',
members: '员工管理',
}
const currentTabKey = fixedTab ?? activeTab
const parentGroup = parentCategoryMap[currentTabKey]
const computedBreadcrumbs = useMemo(() => {
const pageTitle = title === '商品' ? '商品列表' : title
if (parentGroup) {
return [{ title: parentGroup }, { title: pageTitle }]
}
return [{ title: pageTitle }]
}, [parentGroup, title])
return (
<div>
<PageHeader
title={title}
subtitle={merchant ? `${merchant.name} (${merchant.code}) · 当前身份: ${merchantRole ? roleText(merchantRole) : '-'}` : '商户配置与管理中心'}
breadcrumbs={computedBreadcrumbs}
extra={
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadAll}>
刷新
</Button>
}
/>
{fixedTab ? fixedTabContent : (
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as MerchantCenterTab)}
items={tabItems}
/>
)}
<Modal title="创建手动订单" open={manualOrderOpen} confirmLoading={manualOrderSubmitting} onOk={submitManualOrder} onCancel={() => setManualOrderOpen(false)} destroyOnClose width={560}>
<Form form={manualOrderForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="client_order_no" label="商户单号" rules={[{ required: true, message: '请输入商户单号' }, { max: 96, message: '商户单号最长 96 个字符' }]}>
<Input maxLength={96} />
</Form.Item>
<Form.Item name="sku" label="商品" rules={[{ required: true, message: '请选择商品' }]}>
<Select
showSearch
loading={manualOrderProductsLoading}
optionFilterProp="label"
options={manualOrderProductOptions}
/>
</Form.Item>
<Form.Item name="quantity" label="数量" rules={[{ required: true, message: '请输入数量' }]}>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="game_account" label="充值账号 / 游戏 UID" rules={[{ max: 128, message: '账号最长 128 个字符' }]}>
<Input maxLength={128} placeholder="用于后续发货时预填" />
</Form.Item>
<Form.Item name="buyer_reference" label="买家标识" rules={[{ max: 128, message: '买家标识最长 128 个字符' }]}>
<Input maxLength={128} />
</Form.Item>
<Form.Item name="note" label="备注" rules={[{ max: 512 }]}>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
<Modal title="新增 API 密钥" open={apiClientOpen} onOk={submitAPIClient} onCancel={() => setApiClientOpen(false)} destroyOnClose>
<Form form={apiClientForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="名称"
rules={[{ required: true }, { max: 64, message: '名称最长 64 个字符' }]}
>
<Input placeholder="按用途命名,如:下单系统 / 对账脚本" maxLength={64} />
</Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
<Select mode="multiple" options={scopeOptions} />
</Form.Item>
<Form.Item name="signature_version" label="签名版本">
<Select options={[{ value: 'v1', label: 'v1' }]} />
</Form.Item>
<Typography.Text type="secondary">
建议不同用途(下单、查询、对账)分别创建密钥,便于独立禁用与审计。每个商户最多 {apiClientMax} 个。
</Typography.Text>
</Form>
</Modal>
<Modal title="API 密钥" open={!!apiCredential} onCancel={() => setApiCredential(null)} footer={<Button type="primary" onClick={() => setApiCredential(null)}>我已保存</Button>}>
{apiCredential && <SecretBlock appKey={apiCredential.client.app_key} secret={apiCredential.secret} />}
</Modal>
<Modal title="回调密钥" open={!!callbackCredential} onCancel={() => setCallbackCredential(null)} footer={<Button type="primary" onClick={() => setCallbackCredential(null)}>我已保存</Button>}>
{callbackCredential && <SecretBlock secret={callbackCredential.secret} />}
</Modal>
<Modal title="添加员工" open={memberOpen} onOk={submitMember} onCancel={() => setMemberOpen(false)} destroyOnClose>
<Form form={memberForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="username" label="员工用户名" rules={[{ required: true, min: 3, message: '请输入至少 3 位用户名' }]}>
<Input maxLength={64} />
</Form.Item>
<Form.Item name="password" label="初始密码" rules={[{ required: true, min: 6, message: '密码至少 6 位' }]}>
<Input.Password maxLength={128} />
</Form.Item>
<Form.Item name="nickname" label="员工姓名">
<Input maxLength={64} />
</Form.Item>
<Form.Item name="role" label="团队角色" rules={[{ required: true }]}>
<Select options={roles.map((item) => ({ value: item.code, label: item.name }))} />
</Form.Item>
</Form>
</Modal>
<Modal title={editingMember ? `编辑员工:${editingMember.user?.nickname || editingMember.user?.username || ''}` : '编辑员工'} open={!!editingMember} onOk={submitMemberEdit} onCancel={() => setEditingMember(null)} destroyOnClose>
<Form form={memberEditForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item label="账号"><Input value={editingMember?.user?.username} disabled /></Form.Item>
<Form.Item name="role" label="团队角色" rules={[{ required: true }]}>
<Select options={roles.map((item) => ({ value: item.code, label: item.name }))} />
</Form.Item>
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
<Select options={[{ value: 1, label: '启用' }, { value: 0, label: '停用' }]} />
</Form.Item>
<Form.Item name="is_default" label="设为默认商户" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
</Modal>
<Modal title={editingRole ? `编辑角色:${editingRole.name}` : '新建团队角色'} open={roleOpen} onOk={submitRole} onCancel={() => { setRoleOpen(false); setEditingRole(null) }} destroyOnClose>
<Form form={roleForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="name" label="角色名称" rules={[{ required: true }]}><Input maxLength={64} placeholder="例如:客服" /></Form.Item>
{!editingRole && <Form.Item name="code" label="角色编码" rules={[{ required: true, pattern: /^[a-z0-9][a-z0-9-]{1,63}$/, message: '使用 2-64 位小写字母、数字或连字符' }]}><Input maxLength={64} placeholder="例如:support" /></Form.Item>}
<Form.Item name="permissions" label="权限"><Select mode="multiple" options={rolePermissionOptions} /></Form.Item>
</Form>
</Modal>
</div>
)
}