优化商户功能导航与表格

This commit is contained in:
yml2213
2026-07-31 15:08:26 +08:00
parent db4a00f25d
commit 71cde7e32b
6 changed files with 259 additions and 202 deletions
+6
View File
@@ -38,6 +38,12 @@ function AppRoutes() {
> >
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="merchant-center" element={<MerchantCenter />} /> <Route path="merchant-center" element={<MerchantCenter />} />
<Route path="merchant-products" element={<MerchantCenter fixedTab="products" title="商品" />} />
<Route path="merchant-orders" element={<MerchantCenter fixedTab="orders" title="履约订单" />} />
<Route path="merchant-wallet" element={<MerchantCenter fixedTab="wallet" title="钱包" />} />
<Route path="merchant-members" element={<MerchantCenter fixedTab="members" title="成员" />} />
<Route path="merchant-callbacks" element={<MerchantCenter fixedTab="callbacks" title="回调" />} />
<Route path="merchant-api-keys" element={<MerchantCenter fixedTab="api" title="API 密钥" />} />
<Route <Route
path="platform-merchants" path="platform-merchants"
element={ element={
+55 -26
View File
@@ -54,53 +54,39 @@ const adminSections: SidebarSection[] = [
key: 'products', key: 'products',
label: '商品管理', label: '商品管理',
icon: <AppstoreOutlined />, icon: <AppstoreOutlined />,
children: [{ key: 'platform-products', label: '平台授权商品', disabled: true }], children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }],
}, },
{ {
key: 'shops', key: 'shops',
label: '店铺管理', label: '商户管理',
icon: <ShopOutlined />, icon: <ShopOutlined />,
children: [ children: [{ key: 'shop-list', label: '商户列表', path: '/platform-merchants' }],
{ key: 'shop-list', label: '店铺列表', path: '/platform-merchants' },
{ key: 'shop-products', label: '店铺商品绑定', disabled: true },
],
}, },
{ {
key: 'orders', key: 'orders',
label: '订单管理', label: '订单管理',
icon: <OrderedListOutlined />, icon: <OrderedListOutlined />,
children: [ children: [{ key: 'fulfillment-orders', label: '履约订单', path: '/merchant-orders' }],
{ key: 'auto-orders', label: '自动发货订单', disabled: true },
{ key: 'manual-verify', label: '手动核销/反核销', disabled: true },
{ key: 'after-sales', label: '售后订单', disabled: true },
{ key: 'manual-orders', label: '手动处理订单', disabled: true },
],
}, },
{ {
key: 'staff', key: 'staff',
label: '员工管理', label: '员工管理',
icon: <TeamOutlined />, icon: <TeamOutlined />,
children: [ children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }],
{ key: 'staff-list', label: '员工列表', disabled: true },
{ key: 'positions', label: '岗位列表', disabled: true },
{ key: 'operation-logs', label: '操作日志', disabled: true },
],
}, },
{ {
key: 'funds', key: 'funds',
label: '资金', label: '资金',
icon: <WalletOutlined />, icon: <WalletOutlined />,
children: [ children: [{ key: 'merchant-wallet', label: '钱包', path: '/merchant-wallet' }],
{ key: 'point-details', label: '积分明细', disabled: true },
{ key: 'recharge-requests', label: '充值申请', disabled: true },
],
}, },
{ {
key: 'open-api', key: 'open-api',
label: '开放 API', label: '开放 API',
icon: <ApiOutlined />, icon: <ApiOutlined />,
children: [ children: [
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }, { key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
{ key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' },
{ key: 'api-docs', label: '对接文档', path: '/open-api' }, { key: 'api-docs', label: '对接文档', path: '/open-api' },
{ key: 'api-debug', label: '调用调试', path: '/api-debug' }, { key: 'api-debug', label: '调用调试', path: '/api-debug' },
], ],
@@ -114,17 +100,60 @@ const merchantSections: SidebarSection[] = [
icon: <DashboardOutlined />, icon: <DashboardOutlined />,
path: '/', path: '/',
}, },
{
key: 'products',
label: '商品管理',
icon: <AppstoreOutlined />,
children: [{ key: 'merchant-products', label: '商品', path: '/merchant-products' }],
},
{
key: 'orders',
label: '订单管理',
icon: <OrderedListOutlined />,
children: [{ key: 'fulfillment-orders', label: '履约订单', path: '/merchant-orders' }],
},
{
key: 'funds',
label: '资金',
icon: <WalletOutlined />,
children: [{ key: 'merchant-wallet', label: '钱包', path: '/merchant-wallet' }],
},
{
key: 'staff',
label: '员工管理',
icon: <TeamOutlined />,
children: [{ key: 'merchant-members', label: '成员', path: '/merchant-members' }],
},
{ {
key: 'open-api', key: 'open-api',
label: '开放 API', label: '开放 API',
icon: <ApiOutlined />, icon: <ApiOutlined />,
children: [{ key: 'api-keys', label: 'API 密钥', path: '/merchant-center' }], children: [
{ key: 'api-keys', label: 'API 密钥', path: '/merchant-api-keys' },
{ key: 'api-callbacks', label: '回调', path: '/merchant-callbacks' },
],
}, },
] ]
function getSelectedKey(pathname: string) { function getSelectedKey(pathname: string, search: string) {
if (pathname === '/') return 'dashboard' if (pathname === '/') return 'dashboard'
if (pathname.startsWith('/merchant-center')) return 'api-keys' if (pathname.startsWith('/merchant-products')) return 'merchant-products'
if (pathname.startsWith('/merchant-orders')) return 'fulfillment-orders'
if (pathname.startsWith('/merchant-wallet')) return 'merchant-wallet'
if (pathname.startsWith('/merchant-members')) return 'merchant-members'
if (pathname.startsWith('/merchant-callbacks')) return 'api-callbacks'
if (pathname.startsWith('/merchant-api-keys')) return 'api-keys'
if (pathname.startsWith('/merchant-center')) {
const tab = new URLSearchParams(search).get('tab')
const tabKeys: Record<string, string> = {
products: 'merchant-products',
orders: 'fulfillment-orders',
wallet: 'merchant-wallet',
members: 'merchant-members',
callbacks: 'api-callbacks',
}
return tab ? tabKeys[tab] || 'merchant-products' : 'merchant-products'
}
if (pathname.startsWith('/open-api')) return 'api-docs' if (pathname.startsWith('/open-api')) return 'api-docs'
if (pathname.startsWith('/api-debug')) return 'api-debug' if (pathname.startsWith('/api-debug')) return 'api-debug'
if (pathname.startsWith('/platform-merchants')) return 'shop-list' if (pathname.startsWith('/platform-merchants')) return 'shop-list'
@@ -145,7 +174,7 @@ export default function MainLayout() {
const isDocsPage = location.pathname.startsWith('/open-api') const isDocsPage = location.pathname.startsWith('/open-api')
const sections = useMemo(() => (isAdmin ? adminSections : merchantSections), [isAdmin]) const sections = useMemo(() => (isAdmin ? adminSections : merchantSections), [isAdmin])
const selectedKey = getSelectedKey(location.pathname) const selectedKey = getSelectedKey(location.pathname, location.search)
const [openKeys, setOpenKeys] = useState<string[]>(() => getInitialOpenKeys(sections, selectedKey)) const [openKeys, setOpenKeys] = useState<string[]>(() => getInitialOpenKeys(sections, selectedKey))
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -238,7 +238,7 @@ export default function ApiDebugger() {
API API
</Title> </Title>
<Paragraph type="secondary"> <Paragraph type="secondary">
API AppKey Secret使 Web Crypto API API AppKey Secret使 Web Crypto API
</Paragraph> </Paragraph>
<Card <Card
+1 -1
View File
@@ -199,7 +199,7 @@ export default function Dashboard() {
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={12}> <Col span={12}>
<Statistic <Statistic
title="API 客户端" title="API 密钥"
value={stats?.active_api_client_count ?? 0} value={stats?.active_api_client_count ?? 0}
prefix={<ApiOutlined />} prefix={<ApiOutlined />}
suffix={`/ ${numberText(stats?.api_client_count)}`} suffix={`/ ${numberText(stats?.api_client_count)}`}
+194 -172
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { import {
Button, Button,
Descriptions, Descriptions,
@@ -81,10 +82,19 @@ const testOrderStatusOptions = [
{ value: 'failed', label: '发货失败,可重试' }, { value: 'failed', label: '发货失败,可重试' },
] ]
export default function MerchantCenter() { type MerchantCenterTab = 'products' | 'orders' | 'wallet' | 'api' | 'callbacks' | 'members'
interface MerchantCenterProps {
fixedTab?: MerchantCenterTab
title?: string
}
export default function MerchantCenter({ fixedTab, title = '商户中心' }: MerchantCenterProps = {}) {
const location = useLocation()
const routeTab = useMemo(() => fixedTab ?? tabFromSearch(location.search), [fixedTab, location.search])
const [merchant, setMerchant] = useState<Merchant | null>(null) const [merchant, setMerchant] = useState<Merchant | null>(null)
const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>() const [merchantRole, setMerchantRole] = useState<MerchantMember['role']>()
const [activeTab, setActiveTab] = useState('products') const [activeTab, setActiveTab] = useState<MerchantCenterTab>(routeTab ?? 'products')
const [products, setProducts] = useState<PageResult<MerchantProduct>>({ list: [], total: 0, page: 1, size: 10 }) 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 [orders, setOrders] = useState<PageResult<FulfillmentOrder>>({ list: [], total: 0, page: 1, size: 10 })
const [ledger, setLedger] = useState<PageResult<WalletLedgerEntry>>({ list: [], total: 0, page: 1, size: 10 }) const [ledger, setLedger] = useState<PageResult<WalletLedgerEntry>>({ list: [], total: 0, page: 1, size: 10 })
@@ -252,8 +262,8 @@ export default function MerchantCenter() {
setLoading(true) setLoading(true)
try { try {
const current = await loadCurrent() const current = await loadCurrent()
const nextTab = resolveEnabledTab(activeTab, current.merchant.features) const nextTab = fixedTab ?? resolveEnabledTab(routeTab ?? activeTab, current.merchant.features)
if (nextTab !== activeTab) { if (!fixedTab && nextTab !== activeTab) {
setActiveTab(nextTab) setActiveTab(nextTab)
} }
await loadActiveTab(current.role, nextTab) await loadActiveTab(current.role, nextTab)
@@ -262,12 +272,18 @@ export default function MerchantCenter() {
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [activeTab, loadActiveTab, loadCurrent]) }, [activeTab, fixedTab, loadActiveTab, loadCurrent, routeTab])
useEffect(() => { useEffect(() => {
loadAll() loadAll()
}, [loadAll]) }, [loadAll])
useEffect(() => {
if (routeTab && activeTab !== routeTab) {
setActiveTab(routeTab)
}
}, [activeTab, routeTab])
const openProductCreate = () => { const openProductCreate = () => {
setEditingProduct(null) setEditingProduct(null)
productForm.resetFields() productForm.resetFields()
@@ -380,7 +396,7 @@ export default function MerchantCenter() {
}) })
setApiCredential(credential) setApiCredential(credential)
setApiClientOpen(false) setApiClientOpen(false)
message.success('API 客户端已创建') message.success('API 密钥已创建')
loadAPIClients() loadAPIClients()
} catch (e) { } catch (e) {
message.error(e instanceof Error ? e.message : '创建失败') message.error(e instanceof Error ? e.message : '创建失败')
@@ -459,27 +475,39 @@ export default function MerchantCenter() {
] ]
const orderColumns: ColumnsType<FulfillmentOrder> = [ const orderColumns: ColumnsType<FulfillmentOrder> = [
{ title: '平台订单号', dataIndex: 'order_no', width: 210, render: (v) => <Typography.Text copyable>{v}</Typography.Text> }, {
{ title: '商户单号', dataIndex: 'client_order_no', width: 160, ellipsis: true }, title: '平台订单号',
{ title: 'SKU', dataIndex: 'product_sku', width: 150, render: (v) => <Typography.Text code>{v}</Typography.Text> }, dataIndex: 'order_no',
{ title: '商品', dataIndex: 'product_name', ellipsis: true }, width: 190,
{ title: '基础金额', dataIndex: 'base_amount', width: 100, render: money }, ellipsis: true,
{ title: '手续费', dataIndex: 'service_fee_amount', width: 100, render: money }, render: (v) => <Typography.Text copyable ellipsis style={{ maxWidth: '100%' }}>{v}</Typography.Text>,
{ title: '扣款合计', dataIndex: 'amount', width: 100, render: money }, },
{ title: '支付', dataIndex: 'payment_status', width: 90, render: paymentStatusTag }, { title: '商户单号', dataIndex: 'client_order_no', width: 140, ellipsis: true },
{ title: '履约', dataIndex: 'fulfillment_status', width: 100, render: fulfillmentStatusTag }, {
{ title: '上游单号', dataIndex: 'provider_order_no', width: 140, ellipsis: true, render: (v) => v || '-' }, title: 'SKU',
dataIndex: 'product_sku',
width: 130,
ellipsis: true,
render: (v) => <Typography.Text code ellipsis style={{ maxWidth: '100%' }}>{v}</Typography.Text>,
},
{ title: '商品', dataIndex: 'product_name', width: 220, ellipsis: true },
{ title: '基础金额', dataIndex: 'base_amount', width: 88, render: money },
{ title: '手续费', dataIndex: 'service_fee_amount', width: 82, render: money },
{ title: '扣款合计', dataIndex: 'amount', width: 88, render: money },
{ title: '支付', dataIndex: 'payment_status', width: 76, render: paymentStatusTag },
{ title: '履约', dataIndex: 'fulfillment_status', width: 86, render: fulfillmentStatusTag },
{ title: '上游单号', dataIndex: 'provider_order_no', width: 120, ellipsis: true, render: (v) => v || '-' },
{ {
title: '链接有效期', title: '链接有效期',
dataIndex: 'delivery_link_expires_at', dataIndex: 'delivery_link_expires_at',
width: 160, width: 140,
render: (_, record) => record.delivery_link_revoked_at ? <Tag color="red"></Tag> : formatDateTime(record.delivery_link_expires_at), render: (_, record) => record.delivery_link_revoked_at ? <Tag color="red"></Tag> : formatDateTime(record.delivery_link_expires_at),
}, },
{ title: '时间', dataIndex: 'created_at', width: 180, render: formatDateTime }, { title: '时间', dataIndex: 'created_at', width: 150, render: formatDateTime },
{ {
title: '发货链接', title: '发货链接',
key: 'delivery_link', key: 'delivery_link',
width: 220, width: 150,
render: (_, record) => record.delivery_link_revoked_at ? ( render: (_, record) => record.delivery_link_revoked_at ? (
<Button type="link" size="small" onClick={() => restoreDeliveryLink(record.order_no)}></Button> <Button type="link" size="small" onClick={() => restoreDeliveryLink(record.order_no)}></Button>
) : ( ) : (
@@ -547,12 +575,141 @@ export default function MerchantCenter() {
{ title: '状态', dataIndex: 'status', width: 90, render: (v) => (v === 1 ? <Tag color="green"></Tag> : <Tag></Tag>) }, { title: '状态', dataIndex: 'status', width: 90, render: (v) => (v === 1 ? <Tag color="green"></Tag> : <Tag></Tag>) },
] ]
const productContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> SKU </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={openProductCreate}></Button>}
</Space>
<Table
rowKey="id"
loading={loading}
columns={productColumns}
dataSource={products.list}
tableLayout="fixed"
pagination={pageConfig(products, loadProducts)}
/>
</Space>
)
const orderContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> UID</Typography.Text>
{canManage && (
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={openTestOrderCreate}>
</Button>
</Space>
)}
</Space>
<Table
rowKey="id"
loading={loading}
columns={orderColumns}
dataSource={orders.list}
tableLayout="fixed"
scroll={{ x: 1660 }}
pagination={pageConfig(orders, loadOrders)}
/>
</Space>
)
const walletContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Descriptions size="small" bordered column={3} style={{ flex: 1 }}>
<Descriptions.Item label="可用余额">{money(wallet?.available_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="冻结余额">{money(wallet?.frozen_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="币种">{wallet?.currency || 'POINT'}</Descriptions.Item>
</Descriptions>
{canFinance && (
<Button
icon={<WalletOutlined />}
onClick={() => {
walletForm.resetFields()
walletForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` })
setWalletOpen(true)
}}
>
</Button>
)}
</Space>
{canFinance ? (
<Table
rowKey="id"
loading={loading}
columns={ledgerColumns}
dataSource={ledger.list}
tableLayout="fixed"
pagination={pageConfig(ledger, loadWallet)}
/>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</Space>
)
const apiKeyContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> API Secret </Typography.Text>
{canManage && <Button type="primary" icon={<ApiOutlined />} onClick={() => {
apiClientForm.resetFields()
apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
setApiClientOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" />
</Space>
)
const callbackContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> outbox </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
callbackForm.resetFields()
callbackForm.setFieldsValue({ events: ['order.fulfillment.updated'] })
setCallbackOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={callbackColumns} dataSource={callbacks} tableLayout="fixed" />
</Space>
)
const memberContent = (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"></Typography.Text>
{merchantRole === 'owner' && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
memberForm.resetFields()
memberForm.setFieldsValue({ role: 'operator' })
setMemberOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={memberColumns} dataSource={members} tableLayout="fixed" />
</Space>
)
const tabItems = [
{ key: 'products', label: '商品', disabled: !hasFeature('products'), children: productContent },
{ key: 'orders', label: '履约订单', disabled: !hasFeature('orders'), children: orderContent },
{ key: 'wallet', label: '钱包', disabled: !hasFeature('wallet'), children: walletContent },
{ key: 'api', label: 'API 密钥', disabled: !hasFeature('api'), children: apiKeyContent },
{ key: 'callbacks', label: '回调', disabled: !hasFeature('callbacks'), children: callbackContent },
{ key: 'members', label: '成员', children: memberContent },
]
const fixedTabContent = tabItems.find((item) => item.key === fixedTab)?.children
return ( return (
<div> <div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<div> <div>
<Typography.Title level={4} style={{ margin: 0 }}> <Typography.Title level={4} style={{ margin: 0 }}>
{title}
</Typography.Title> </Typography.Title>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
{merchant ? `${merchant.name} / ${merchant.code}` : '加载中'} · {merchantRole ? roleText(merchantRole) : '-'} {merchant ? `${merchant.name} / ${merchant.code}` : '加载中'} · {merchantRole ? roleText(merchantRole) : '-'}
@@ -563,154 +720,13 @@ export default function MerchantCenter() {
</Button> </Button>
</Space> </Space>
<Tabs {fixedTab ? fixedTabContent : (
activeKey={activeTab} <Tabs
onChange={setActiveTab} activeKey={activeTab}
items={[ onChange={(key) => setActiveTab(key as MerchantCenterTab)}
{ items={tabItems}
key: 'products', />
label: '商品', )}
disabled: !hasFeature('products'),
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> SKU </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={openProductCreate}></Button>}
</Space>
<Table
rowKey="id"
loading={loading}
columns={productColumns}
dataSource={products.list}
tableLayout="fixed"
pagination={pageConfig(products, loadProducts)}
/>
</Space>
),
},
{
key: 'orders',
label: '履约订单',
disabled: !hasFeature('orders'),
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> UID</Typography.Text>
{canManage && (
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={openTestOrderCreate}>
</Button>
</Space>
)}
</Space>
<Table
rowKey="id"
loading={loading}
columns={orderColumns}
dataSource={orders.list}
tableLayout="fixed"
scroll={{ x: 1500 }}
pagination={pageConfig(orders, loadOrders)}
/>
</Space>
),
},
{
key: 'wallet',
label: '钱包',
disabled: !hasFeature('wallet'),
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Descriptions size="small" bordered column={3} style={{ flex: 1 }}>
<Descriptions.Item label="可用余额">{money(wallet?.available_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="冻结余额">{money(wallet?.frozen_balance ?? 0)}</Descriptions.Item>
<Descriptions.Item label="币种">{wallet?.currency || 'POINT'}</Descriptions.Item>
</Descriptions>
{canFinance && (
<Button
icon={<WalletOutlined />}
onClick={() => {
walletForm.resetFields()
walletForm.setFieldsValue({ idempotency_key: `manual-${Date.now()}` })
setWalletOpen(true)
}}
>
</Button>
)}
</Space>
{canFinance ? (
<Table
rowKey="id"
loading={loading}
columns={ledgerColumns}
dataSource={ledger.list}
tableLayout="fixed"
pagination={pageConfig(ledger, loadWallet)}
/>
) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</Space>
),
},
{
key: 'api',
label: 'API 客户端',
disabled: !hasFeature('api'),
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"></Typography.Text>
{canManage && <Button type="primary" icon={<ApiOutlined />} onClick={() => {
apiClientForm.resetFields()
apiClientForm.setFieldsValue({ signature_version: 'v1', scopes: ['products:read', 'orders:read', 'orders:write'] })
setApiClientOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={apiClientColumns} dataSource={apiClients} tableLayout="fixed" />
</Space>
),
},
{
key: 'callbacks',
label: '回调',
disabled: !hasFeature('callbacks'),
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> outbox </Typography.Text>
{canManage && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
callbackForm.resetFields()
callbackForm.setFieldsValue({ events: ['order.fulfillment.updated'] })
setCallbackOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={callbackColumns} dataSource={callbacks} tableLayout="fixed" />
</Space>
),
},
{
key: 'members',
label: '成员',
children: (
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"></Typography.Text>
{merchantRole === 'owner' && <Button type="primary" icon={<PlusOutlined />} onClick={() => {
memberForm.resetFields()
memberForm.setFieldsValue({ role: 'operator' })
setMemberOpen(true)
}}></Button>}
</Space>
<Table rowKey="id" loading={loading} columns={memberColumns} dataSource={members} tableLayout="fixed" />
</Space>
),
},
]}
/>
<Modal title={editingProduct ? '编辑商品' : '新增商品'} open={productOpen} onOk={submitProduct} onCancel={() => setProductOpen(false)} destroyOnClose width={620}> <Modal title={editingProduct ? '编辑商品' : '新增商品'} open={productOpen} onOk={submitProduct} onCancel={() => setProductOpen(false)} destroyOnClose width={620}>
<Form form={productForm} layout="vertical" style={{ marginTop: 16 }}> <Form form={productForm} layout="vertical" style={{ marginTop: 16 }}>
@@ -847,7 +863,7 @@ export default function MerchantCenter() {
</Form> </Form>
</Modal> </Modal>
<Modal title="新增 API 客户端" open={apiClientOpen} onOk={submitAPIClient} onCancel={() => setApiClientOpen(false)} destroyOnClose> <Modal title="新增 API 密钥" open={apiClientOpen} onOk={submitAPIClient} onCancel={() => setApiClientOpen(false)} destroyOnClose>
<Form form={apiClientForm} layout="vertical" style={{ marginTop: 16 }}> <Form form={apiClientForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}> <Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input /> <Input />
@@ -942,9 +958,15 @@ function featuresToList(features?: string) {
return list.length > 0 ? list : ['products', 'orders', 'wallet', 'api', 'callbacks'] return list.length > 0 ? list : ['products', 'orders', 'wallet', 'api', 'callbacks']
} }
function resolveEnabledTab(current: string, features?: string) { function tabFromSearch(search: string): MerchantCenterTab | null {
const tab = new URLSearchParams(search).get('tab')
const allowedTabs: MerchantCenterTab[] = ['products', 'orders', 'wallet', 'api', 'callbacks', 'members']
return allowedTabs.includes(tab as MerchantCenterTab) ? tab as MerchantCenterTab : null
}
function resolveEnabledTab(current: MerchantCenterTab, features?: string): MerchantCenterTab {
const enabled = new Set(featuresToList(features)) const enabled = new Set(featuresToList(features))
const tabFeatures: Record<string, string | null> = { const tabFeatures: Record<MerchantCenterTab, string | null> = {
products: 'products', products: 'products',
orders: 'orders', orders: 'orders',
wallet: 'wallet', wallet: 'wallet',
@@ -956,7 +978,7 @@ function resolveEnabledTab(current: string, features?: string) {
if (feature === null || enabled.has(feature)) { if (feature === null || enabled.has(feature)) {
return current return current
} }
return ['products', 'orders', 'wallet', 'api', 'callbacks'].find((key) => enabled.has(key)) || 'members' return (['products', 'orders', 'wallet', 'api', 'callbacks'] as MerchantCenterTab[]).find((key) => enabled.has(key)) || 'members'
} }
function money(value?: number | null) { function money(value?: number | null) {
+2 -2
View File
@@ -295,7 +295,7 @@ function OverviewSection() {
title="概述" title="概述"
desc={ desc={
<> <>
API / API API API / API
<Text code>X-App-Key</Text> + HMAC-SHA256 <Text code>X-App-Key</Text> + HMAC-SHA256
<Text code>/api/open/v1</Text> API <Text code>/api/open/v1</Text> API
@@ -309,7 +309,7 @@ function OverviewSection() {
message="一句话上手" message="一句话上手"
description={ description={
<ol style={{ margin: 0, paddingLeft: 20 }}> <ol style={{ margin: 0, paddingLeft: 20 }}>
<li> / API <Text code>AppKey</Text> <Text code>AppSecret</Text></li> <li> API / API <Text code>AppKey</Text> <Text code>AppSecret</Text></li>
<li> <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> </li> <li> <Text code>X-App-Key / X-Timestamp / X-Nonce / X-Sign</Text> </li>
<li> <Text code>sku</Text></li> <li> <Text code>sku</Text></li>
<li> <Text code>order_no</Text> </li> <li> <Text code>order_no</Text> </li>