686 lines
27 KiB
TypeScript
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>
|
|
)
|
|
}
|