feat: support merchant team roles and permissions
This commit is contained in:
@@ -23,6 +23,7 @@ import type {
|
||||
FulfillmentOrder,
|
||||
Merchant,
|
||||
MerchantMember,
|
||||
MerchantRole,
|
||||
MerchantProduct,
|
||||
PageResult,
|
||||
WalletAccount,
|
||||
@@ -33,9 +34,9 @@ import {
|
||||
defaultCallbackFormValues,
|
||||
eventsToValue,
|
||||
featuresToList,
|
||||
memberRoleOptions,
|
||||
productOptionLabel,
|
||||
resolveEnabledTab,
|
||||
rolePermissionOptions,
|
||||
roleText,
|
||||
scopeOptions,
|
||||
tabFromSearch,
|
||||
@@ -58,6 +59,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
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 })
|
||||
@@ -67,11 +69,14 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
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 [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[]>([])
|
||||
@@ -80,10 +85,15 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
const [apiClientForm] = Form.useForm()
|
||||
const [callbackForm] = Form.useForm()
|
||||
const [memberForm] = Form.useForm()
|
||||
const [roleForm] = Form.useForm()
|
||||
const [manualOrderForm] = Form.useForm()
|
||||
|
||||
const canManage = merchantRole === 'owner' || merchantRole === 'operator'
|
||||
const canFinance = merchantRole === 'owner' || merchantRole === 'finance'
|
||||
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) => ({
|
||||
@@ -136,6 +146,12 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
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
|
||||
}, [])
|
||||
|
||||
@@ -152,26 +168,26 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
const loadWallet = useCallback(async (
|
||||
page = ledger.page,
|
||||
size = ledger.size,
|
||||
role = merchantRole,
|
||||
permissions = merchantPermissions,
|
||||
filters?: { reference_no?: string; type?: string },
|
||||
) => {
|
||||
const walletData = await merchantApi.wallet()
|
||||
setWallet(walletData)
|
||||
if (role === 'owner' || role === 'finance') {
|
||||
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, merchantRole])
|
||||
}, [ledger.page, ledger.size, merchantPermissions])
|
||||
|
||||
const loadAPIClients = useCallback(async () => {
|
||||
const clientData = await merchantApi.apiClients()
|
||||
setApiClients(clientData || [])
|
||||
}, [])
|
||||
|
||||
const loadCallbacks = useCallback(async (role = merchantRole) => {
|
||||
if (role !== 'owner' && role !== 'operator') {
|
||||
const loadCallbacks = useCallback(async (permissions = merchantPermissions) => {
|
||||
if (!permissions.includes('callbacks:manage')) {
|
||||
setCallback(null)
|
||||
callbackForm.resetFields()
|
||||
return
|
||||
@@ -183,18 +199,20 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
events: eventsToValue(callbackData.events),
|
||||
status: callbackData.status,
|
||||
} : defaultCallbackFormValues())
|
||||
}, [callbackForm, merchantRole])
|
||||
}, [callbackForm, merchantPermissions])
|
||||
|
||||
const loadMembers = useCallback(async (role = merchantRole) => {
|
||||
if (role !== 'owner' && role !== 'operator') {
|
||||
const loadMembers = useCallback(async (permissions = merchantPermissions) => {
|
||||
if (!permissions.includes('members:manage')) {
|
||||
setMembers([])
|
||||
setRoles([])
|
||||
return
|
||||
}
|
||||
const memberData = await merchantApi.members()
|
||||
const [memberData, roleData] = await Promise.all([merchantApi.members(), merchantApi.roles()])
|
||||
setMembers(memberData || [])
|
||||
}, [merchantRole])
|
||||
setRoles(roleData || [])
|
||||
}, [merchantPermissions])
|
||||
|
||||
const loadActiveTab = useCallback(async (role = merchantRole, tab = activeTab) => {
|
||||
const loadActiveTab = useCallback(async (permissions = merchantPermissions, tab = activeTab) => {
|
||||
switch (tab) {
|
||||
case 'products':
|
||||
await loadProducts()
|
||||
@@ -203,19 +221,19 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
await loadOrders()
|
||||
return
|
||||
case 'wallet':
|
||||
await loadWallet(undefined, undefined, role)
|
||||
await loadWallet(undefined, undefined, permissions)
|
||||
return
|
||||
case 'api':
|
||||
await loadAPIClients()
|
||||
return
|
||||
case 'callbacks':
|
||||
await loadCallbacks(role)
|
||||
await loadCallbacks(permissions)
|
||||
return
|
||||
case 'members':
|
||||
await loadMembers(role)
|
||||
await loadMembers(permissions)
|
||||
return
|
||||
}
|
||||
}, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantRole])
|
||||
}, [activeTab, loadAPIClients, loadCallbacks, loadMembers, loadOrders, loadProducts, loadWallet, merchantPermissions])
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -225,7 +243,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
if (!fixedTab && nextTab !== activeTab) {
|
||||
setActiveTab(nextTab)
|
||||
}
|
||||
await loadActiveTab(current.role, nextTab)
|
||||
await loadActiveTab(current.permissions || [], nextTab)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
} finally {
|
||||
@@ -254,7 +272,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
|
||||
const handleWalletFilter = async () => {
|
||||
const values = await walletFilterForm.validateFields()
|
||||
loadWallet(1, ledger.size, merchantRole, {
|
||||
loadWallet(1, ledger.size, merchantPermissions, {
|
||||
reference_no: values.reference_no || undefined,
|
||||
type: values.type || undefined,
|
||||
})
|
||||
@@ -262,16 +280,16 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
|
||||
const handleWalletFilterReset = () => {
|
||||
walletFilterForm.resetFields()
|
||||
loadWallet(1, ledger.size, merchantRole, {})
|
||||
loadWallet(1, ledger.size, merchantPermissions, {})
|
||||
}
|
||||
|
||||
const handleLoadLedger = useCallback((page: number, size: number) => {
|
||||
const values = walletFilterForm.getFieldsValue()
|
||||
loadWallet(page, size, merchantRole, {
|
||||
loadWallet(page, size, merchantPermissions, {
|
||||
reference_no: values.reference_no || undefined,
|
||||
type: values.type || undefined,
|
||||
})
|
||||
}, [loadWallet, merchantRole, walletFilterForm])
|
||||
}, [loadWallet, merchantPermissions, walletFilterForm])
|
||||
|
||||
const newManualClientOrderNo = () => {
|
||||
const suffix = window.crypto?.randomUUID?.().replaceAll('-', '') || `${Date.now()}${Math.random().toString(36).slice(2)}`
|
||||
@@ -389,9 +407,10 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
const values = await memberForm.validateFields()
|
||||
try {
|
||||
await merchantApi.addMember({
|
||||
user_id: values.user_id,
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
nickname: values.nickname,
|
||||
role: values.role,
|
||||
is_default: values.is_default,
|
||||
})
|
||||
setMemberOpen(false)
|
||||
message.success('成员已添加')
|
||||
@@ -401,6 +420,23 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
@@ -426,19 +462,39 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
<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={canManage} onCreateManualOrder={openManualOrderCreate} onOrderSourceChange={changeOrderSource} onLoadOrders={loadOrders} onCopyLink={copyDeliveryLink} onOpenLink={openDeliveryLink} onRevokeLink={revokeDeliveryLink} onRestoreLink={restoreDeliveryLink} />
|
||||
<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={canManage} form={apiClientForm} onOpenCreate={() => setApiClientOpen(true)} onNavigateDocs={() => navigate('/open-api')} onNavigateDebug={() => navigate('/api-debug')} onToggleClient={toggleAPIClient} onDeleteClient={deleteAPIClient} />
|
||||
<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={canManage} callback={callback} form={callbackForm} onSubmit={submitCallback} onRotateSecret={rotateCallbackSecret} />
|
||||
<CallbacksTab canManage={canCallbacksManage} callback={callback} form={callbackForm} onSubmit={submitCallback} onRotateSecret={rotateCallbackSecret} />
|
||||
) },
|
||||
{ key: 'members', label: '成员', children: (
|
||||
<MembersTab loading={loading} members={members} isOwner={merchantRole === 'owner'} form={memberForm} onOpenAdd={() => setMemberOpen(true)} />
|
||||
<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)
|
||||
}}
|
||||
/>
|
||||
) },
|
||||
]
|
||||
const fixedTabContent = tabItems.find((item) => item.key === fixedTab)?.children
|
||||
@@ -541,17 +597,28 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
|
||||
{callbackCredential && <SecretBlock secret={callbackCredential.secret} />}
|
||||
</Modal>
|
||||
|
||||
<Modal title="添加成员" open={memberOpen} onOk={submitMember} onCancel={() => setMemberOpen(false)} destroyOnClose>
|
||||
<Modal title="添加员工" open={memberOpen} onOk={submitMember} onCancel={() => setMemberOpen(false)} destroyOnClose>
|
||||
<Form form={memberForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="user_id" label="用户 ID" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
|
||||
<Form.Item name="username" label="员工用户名" rules={[{ required: true, min: 3, message: '请输入至少 3 位用户名' }]}>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
|
||||
<Select options={memberRoleOptions} />
|
||||
<Form.Item name="password" label="初始密码" rules={[{ required: true, min: 6, message: '密码至少 6 位' }]}>
|
||||
<Input.Password maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_default" label="默认商户">
|
||||
<Select options={[{ value: true, label: '是' }, { value: false, label: '否' }]} />
|
||||
<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={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>
|
||||
|
||||
Reference in New Issue
Block a user