@@ -2,33 +2,24 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation , useNavigate } from 'react-router-dom'
import {
Button ,
Descriptions ,
Form ,
Input ,
InputNumber ,
Modal ,
Select ,
Space ,
Tabs ,
Tag ,
Typography ,
message ,
} from 'antd'
import {
CopyOutlined ,
LinkOutlined ,
ReloadOutlined ,
} from '@ant-design/icons'
import { ReloadOutlined } from '@ant-design/icons'
import { useAuth } from '../store/auth'
import { merchantApi } from '../api'
import { formatDateTime } from '../utils/time'
import { PageHeader } from '../components/PageHeader'
import type {
ApiClient ,
ApiCredential ,
CallbackCredential ,
CallbackSubscription ,
CreateTestOrderResult ,
FulfillmentOrder ,
Merchant ,
MerchantMember ,
@@ -48,7 +39,6 @@ import {
roleText ,
scopeOptions ,
tabFromSearch ,
testOrderStatusOptions ,
} from './merchantCenterUtils'
import type { MerchantCenterProps , MerchantCenterTab } from './merchantCenterUtils'
import {
@@ -71,6 +61,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
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 [ ] > ( [ ] )
@@ -81,24 +72,24 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
const [ apiCredential , setApiCredential ] = useState < ApiCredential | null > ( null )
const [ callbackCredential , setCallbackCredential ] = useState < CallbackCredential | null > ( null )
const [ memberOpen , setMemberOpen ] = useState ( false )
const [ testOrderOpen , setTestOrderOpen ] = useState ( false )
const [ testOrderProducts , setTestOrderProducts ] = useState < MerchantProduct [ ] > ( [ ] )
const [ testOrderProductsLoading , setTestOrderProductsLoading ] = useState ( false )
const [ testOrderResult , setTestOrderResult ] = useState < CreateTestOrderResult | 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 [ testOrderForm ] = Form . useForm ( )
const [ manualOrderForm ] = Form . useForm ( )
const canManage = merchantRole === 'owner' || merchantRole === 'operator'
const canFinance = merchantRole === 'owner' || merchantRole === 'finance'
const enabledFeatures = useMemo ( ( ) = > new Set ( featuresToList ( merchant ? . features ) ) , [ merchant ? . features ] )
const hasFeature = useCallback ( ( feature : string ) = > ! merchant || enabledFeatures . has ( feature ) , [ enabledFeatures , merchant ] )
const testOrderProductOptions = useMemo ( ( ) = > testOrderProducts . map ( ( item ) = > ( {
const manualOrderProductOptions = useMemo ( ( ) = > manualOrderProducts . map ( ( item ) = > ( {
value : item.sku ,
label : productOptionLabel ( item ) ,
} ) ) , [ testOrderProducts ] )
} ) ) , [ manualOrderProducts ] )
const copyDeliveryLink = useCallback ( ( orderNo : string ) = > {
merchantApi . getDeliveryLink ( orderNo )
@@ -117,10 +108,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
. . . prev ,
list : prev.list.map ( ( item ) = > item . order_no === orderNo ? { . . . item , . . . patch } : item ) ,
} ) )
setTestOrderResult ( ( prev ) = > prev ? . order . order_no === orderNo ? {
. . . prev ,
order : { . . . prev . order , . . . patch } ,
} : prev )
} , [ ] )
const revokeDeliveryLink = useCallback ( ( orderNo : string ) = > {
@@ -157,10 +144,10 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
setProducts ( data )
} , [ products . page , products . size ] )
const loadOrders = useCallback ( async ( page = orders . page , size = orders . size ) = > {
const data = await merchantApi . orders ( { page , 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 )
} , [ orders . page , orders . size ] )
} , [ orderSource , orders . page , orders . size ] )
const loadWallet = useCallback ( async (
page = ledger . page ,
@@ -286,21 +273,25 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
} )
} , [ loadWallet , merchantRole , walletFilterForm ] )
const openTestOrderCreate = ( ) = > {
testOrderForm . resetFields ( )
testOrderForm . setFieldsValue ( {
buyer_reference : '测试买家' ,
note : '联调测试订单' ,
order_status : 'paid' ,
const newManualClientOrderNo = ( ) = > {
const suffix = window . crypto ? . randomUUID ? . ( ) . replaceAll ( '-' , '' ) || ` ${ Date . now ( ) } ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } `
return ` manual- ${ suffix . slice ( 0 , 32 ) } `
}
const openManualOrderCreate = ( ) = > {
manualOrderForm . resetFields ( )
manualOrderForm . setFieldsValue ( {
client_order_no : newManualClientOrderNo ( ) ,
quantity : 1 ,
} )
setTestOrderOpen ( true )
setTestOrderProductsLoading ( true )
setManualOrderOpen ( true )
setManualOrderProductsLoading ( true )
merchantApi . products ( { page : 1 , size : 100 } )
. then ( ( data ) = > {
const activeProducts = ( data . list || [ ] ) . filter ( ( item ) = > item . status === 'active' )
setTestOrderProducts ( activeProducts )
setManualOrderProducts ( activeProducts )
if ( activeProducts . length > 0 ) {
testOrderForm . setFieldsValue ( { sku : activeProducts [ 0 ] . sku } )
manualOrderForm . setFieldsValue ( { sku : activeProducts [ 0 ] . sku } )
} else {
message . warning ( '没有可用商品' )
}
@@ -308,27 +299,37 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
. catch ( ( e ) = > {
message . error ( e instanceof Error ? e . message : '商品加载失败' )
} )
. finally ( ( ) = > setTestOrderProductsLoading ( false ) )
. finally ( ( ) = > setManualOrderProductsLoading ( false ) )
}
const submitTestOrder = async ( ) = > {
const values = await testOrderForm . validateFields ( )
const submitManualOrder = async ( ) = > {
const values = await manualOrderForm . validateFields ( )
setManualOrderSubmitting ( true )
try {
const result = await merchantApi . createTestOrder ( {
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 ,
order_status : values.order_status ,
} )
setTestOrderOpen ( false )
setTestOrderResult ( result )
message . success ( '测试订单已创建 ')
loadOrders ( )
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 {
@@ -425,7 +426,7 @@ 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 } canManage = { canManage } onCreateTestOrder = { openTestOrderCreate } onLoadOrders = { loadOrders } onCopyLink = { copyDeliveryLink } onOpenLink = { openDeliveryLink } onRevokeLink = { revokeDeliveryLink } onRestoreLink = { restoreDeliveryLink } / >
< OrdersTab loading = { loading } orders = { orders } orderSource = { orderSource } canManage = { canManage } 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 } / >
@@ -483,21 +484,27 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
/ >
) }
< Modal title = "创建测试 订单" open = { testOrderOpen } onOk = { submitTestOrder } onCancel = { ( ) = > setTestOrderOpen ( false ) } destroyOnClose width = { 560 } >
< Form form = { testOrderForm } layout = "vertical" style = { { marginTop : 16 } } >
< Form.Item name = "sku " label = "商品皮肤 " rules = { [ { required : true , message : '请选择商品 ' } ] } >
< 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 = { testOrderProductsLoading }
loading = { manualOrderProductsLoading }
optionFilterProp = "label"
options = { testOrderProductOptions }
options = { manualOrderProductOptions }
/ >
< / Form.Item >
< Form.Item name = "buyer_reference " label = "买家名称 " rules = { [ { max : 128 } ] } >
< Input / >
< Form.Item name = "quantity " label = "数量 " rules = { [ { required : true , message : '请输入数量' } ] } >
< InputNumber min = { 1 } precision = { 0 } style = { { width : '100%' } } / >
< / Form.Item >
< Form.Item name = "order_status" label = "订单状态 " rules = { [ { required : true } ] } >
< Select options = { testOrderStatusOptions } / >
< 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 } / >
@@ -505,60 +512,6 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer
< / Form >
< / Modal >
< Modal
title = "测试订单"
open = { ! ! testOrderResult }
onCancel = { ( ) = > setTestOrderResult ( null ) }
footer = { (
< Space >
< Button onClick = { ( ) = > setTestOrderResult ( null ) } > 关 闭 < / Button >
{ testOrderResult ? . order . delivery_link_revoked_at ? (
< Button onClick = { ( ) = > restoreDeliveryLink ( testOrderResult ? . order . order_no || '' ) } > 取 消 作 废 < / Button >
) : (
< >
< Button
icon = { < CopyOutlined / > }
onClick = { ( ) = > copyDeliveryLink ( testOrderResult ? . order . order_no || '' ) }
>
复 制 发 货 链 接
< / Button >
< Button type = "primary" icon = { < LinkOutlined / > } onClick = { ( ) = > openDeliveryLink ( testOrderResult ? . order . order_no || '' ) } >
打 开 发 货 页
< / Button >
< Button danger onClick = { ( ) = > revokeDeliveryLink ( testOrderResult ? . order . order_no || '' ) } > 作 废 链 接 < / Button >
< / >
) }
< / Space >
) }
>
{ testOrderResult && (
< Descriptions size = "small" bordered column = { 1 } style = { { marginTop : 16 } } >
< Descriptions.Item label = "订单号" >
< Typography.Text copyable > { testOrderResult . order . order_no } < / Typography.Text >
< / Descriptions.Item >
< Descriptions.Item label = "SKU" >
< Typography.Text code > { testOrderResult . order . product_sku } < / Typography.Text >
< / Descriptions.Item >
< Descriptions.Item label = "可发货" >
< Tag color = { testOrderResult . can_ship ? 'green' : 'red' } >
{ testOrderResult . can_ship ? 'can_ship=true' : 'can_ship=false' }
< / Tag >
< / Descriptions.Item >
{ ! testOrderResult . can_ship && (
< Descriptions.Item label = "不可发货原因" >
< Typography.Text type = "secondary" > { testOrderResult . cannot_ship_reason || '-' } < / Typography.Text >
< / Descriptions.Item >
) }
< Descriptions.Item label = "链接有效期" >
{ testOrderResult . order . delivery_link_revoked_at ? < Tag color = "red" > 已 作 废 < / Tag > : formatDateTime ( testOrderResult . order . delivery_link_expires_at ) }
< / Descriptions.Item >
< Descriptions.Item label = "链接状态" >
< Typography.Text type = "secondary" > 由 后 端 签 名 生 成 , 可 复 制 、 打 开 或 作 废 。 < / Typography.Text >
< / Descriptions.Item >
< / Descriptions >
) }
< / Modal >
< Modal title = "新增 API 密钥" open = { apiClientOpen } onOk = { submitAPIClient } onCancel = { ( ) = > setApiClientOpen ( false ) } destroyOnClose >
< Form form = { apiClientForm } layout = "vertical" style = { { marginTop : 16 } } >
< Form.Item