@@ -1,458 +1,465 @@
import { useCallback , useEffect , useState } from 'react' ;
import {
Button ,
Checkbox ,
Drawer ,
Form ,
Input ,
InputNumber ,
Modal ,
Select ,
Space ,
Table ,
Tabs ,
Typography ,
message ,
} from 'antd' ;
import type { ColumnsType } from 'antd/es/table' ;
import {
CITY_PARTNER_SCOPE_LABELS ,
CITY_PARTNER_STATUS_LABELS ,
CityPartnerScopeType ,
CityPartnerStatus ,
PARTNER_PERMISSION_KEYS ,
PARTNER_PERMISSION_LABELS ,
PARTNER_STAFF_ROLE_LABELS ,
type PartnerPermissionKey ,
} from '@dukang/shared-types' ;
import { request , type Paginated } from '../lib/api' ;
import { districtCodeLabel , formatDistrictLabels } from '../lib/china-region ' ;
import { fmtTime } from '../lib/constants ' ;
import CityDistrictMultiSelect from './CityDistrictMultiSelect ';
import PartnerSubAccountList from './PartnerSubAccountLis t' ;
type PartnerRow = {
id : string ;
companyName : string ;
phone : string ;
name : string ;
scopeType? : string ;
districtCodes? : string [ ] | null ;
orderCommissionRat e? : number ;
redeemCommissionRate? : number ;
accountCount : number ;
createdAt : string ;
} ;
type PartnerDetail = PartnerRow & {
address? : string ;
districtCodes? : string [ ] | null ;
bindingStatu s? : string ;
bankAccountName? : string | null ;
bankAccountNo? : string | null ;
bankBranch ? : string | null ;
m anagedWarehouseId ? : string | null ;
m anagedWarehouseName ? : string | null ;
contactPhone ? : string | null ;
children? : Array < {
id : string ;
phone : string ;
name : string ;
staffRole? : string ;
permissions? : string [ ] ;
status : string ;
} > ;
} ;
type Props = {
cityId : string ;
cityCode? : string ;
maxPartnerCommissionRate? : number ;
onChanged ? : ( ) = > void ;
} ;
const SCOPE_OPTIONS = Object . entries ( CITY_PARTNER_SCOPE_LABELS ) . map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
const BINDING_OPTIONS = Object . entries ( CITY_PARTNER_STATUS_LABELS ) . map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
const PERM_OPTIONS = PARTNER_PERMISSION_KEYS . map ( ( k : PartnerPermissionKey ) = > ( {
value : k ,
label : PARTNER_PERMISSION_LABELS [ k ] ,
} ) ) ;
const STAFF_ROLE_OPTIONS = Object . entries ( PARTNER_STAFF_ROLE_LABELS )
. filter ( ( [ value ] ) = > value !== 'PARTNER' )
. map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
function flattenDistrictCodes ( values : string [ ] | string [ ] [ ] | undefined ) : string [ ] {
if ( ! values ? . length ) return [ ] ;
if ( Array . isArray ( values [ 0 ] ) ) {
return ( values as string [ ] [ ] ) . map ( ( path ) = > path [ path . length - 1 ] ) . filter ( Boolean ) ;
}
return values as string [ ] ;
}
function commissionSumError (
orderPercent : number ,
redeemPercent : number ,
maxRate : number ,
) : string | null {
// API 可能把 Prisma Decimal 序列化为字符串;`"0.05" + 1e-9` 会变成字符串拼接导致误判超限
const max = Number ( maxRate ) ;
const sum = Number ( orderPercent ) / 100 + Number ( redeemPercent ) / 100 ;
if ( ! Number . isFinite ( max ) || ! Number . isFinite ( sum ) ) return '佣金比例无效' ;
if ( sum > max + 1 e - 9 ) {
return ` 订单佣金与核销佣金合计不得超过 ${ ( max * 100 ) . toFixed ( 2 ) } %(当前 ${ ( sum * 100 ) . toFixed ( 2 ) } %) ` ;
}
return null ;
}
function formatApiError ( err : unknown ) : string | null {
if ( err && typeof err === 'object' && 'errorFields' in err ) return null ;
if ( ! ( err instanceof Error ) ) return '操作失败' ;
return err . message . replace ( /\b(\d{6})\b/g , ( code ) = > {
const label = districtCodeLabel ( code ) ;
return label !== code ? ` ${ label } ( ${ code } ) ` : code ;
} ) ;
}
export default function CityPartnersPanel ( {
cityId ,
cityCode ,
maxPartnerCommissionRate = 0.05 ,
onChanged ,
} : Props ) {
const [ editForm ] = Form . useForm ( ) ;
const [ createForm ] = Form . useForm ( ) ;
const [ subForm ] = Form . useForm ( ) ;
const [ subEditForm ] = Form . useForm ( ) ;
const [ partners , setPartners ] = useState < PartnerRow [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ detail , setDetail ] = useState < PartnerDetail | null > ( null ) ;
const [ drawerOpen , setDrawerOpen ] = useState ( false ) ;
const [ createOpen , setCreateOpen ] = useState ( false ) ;
const [ subOpen , setSubOpen ] = useState ( false ) ;
const [ subEditOpen , setSubEditOpen ] = useState ( false ) ;
const [ subEditId , setSubEditId ] = useState < string | null > ( null ) ;
const [ createScopeType , setCreateScopeType ] = useState < CityPartnerScopeType > ( CityPartnerScopeType . CITY_WIDE ) ;
const [ editScopeType , setEditScopeType ] = useState < CityPartnerScopeType > ( CityPartnerScopeType . CITY_WIDE ) ;
const loadPartners = useCallback ( async ( ) = > {
setLoading ( true ) ;
try {
const res = await request < Paginated < PartnerRow > > ( ` /admin/partners?cityId= ${ cityId } &pageSize=100 ` ) ;
setPartners ( res . items ) ;
} finally {
setLoading ( false ) ;
}
} , [ cityId ] ) ;
useEffect ( ( ) = > {
void loadPartners ( ) ;
} , [ loadPartners ] ) ;
async function openPartner ( id : string ) {
const d = await request < PartnerDetail > ( ` /admin/partners/ ${ id } ` ) ;
setDetail ( d ) ;
setEditScopeType ( ( d . scopeType as CityPartnerScopeType ) || CityPartnerScopeType . CITY_WIDE ) ;
editForm . setFieldsValue ( {
name : d.name ,
phone : d.phone ,
companyName : d.companyName ,
contactPhone : d.contactPhone ? ? d . phone ,
address : d.address ? ? '' ,
scopeType : d.scopeType ,
districtCodes : d.districtCodes ? ? [ ] ,
orderCommissionRate : ( d . orderCommissionRate ? ? 0 ) * 100 ,
redeemCommissionRate : ( d . redeemCommissionRate ? ? 0.03 ) * 100 ,
bindingStatus : d.bindingStatus ? ? CityPartnerStatus . ACTIVE ,
bankAccountName : d.bankAccountName ? ? '' ,
bankAccountNo : d.bankAccountNo ? ? '' ,
bankBranch : d.bankBranch ? ? '' ,
} ) ;
setDrawerOpen ( true ) ;
}
async function savePartner() {
if ( ! detail ) return ;
try {
const v = await editForm . validateFields ( ) ;
const err = commissionSumError (
Number ( v . orderCommissionRate ? ? 0 ) ,
Number ( v . redeemCommissionRate ? ? 0 ) ,
maxPartnerCommissionRate ,
) ;
if ( err ) {
message . error ( err ) ;
return ;
}
await request ( ` /admin/partners/ ${ detail . id } ` , {
method : 'PUT' ,
body : JSON.stringify ( {
. . . v ,
orderCommissionRate : Number ( v . orderCommissionRate ? ? 0 ) / 100 ,
redeemCommissionRate : Number ( v . redeemCommissionRate ? ? 3 ) / 100 ,
districtCodes : editScopeType === CityPartnerScopeType . DISTRICT ? flattenDistrictCodes ( v . districtCodes ) : undefined ,
} ) ,
} ) ;
message . success ( '已保存' ) ;
setDrawerOpen ( false ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} catch ( e ) {
const msg = formatApiError ( e ) ;
if ( msg ) message . error ( msg ) ;
}
}
async function deleteSubAccount ( subId : string ) {
await request ( ` /admin/partner-accounts/ ${ subId } ` , { method : 'DELETE' } ) ;
message . success ( '子账号已删除' ) ;
if ( detail ) void openPartner ( detail . id ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
}
const columns : ColumnsType < PartnerRow > = [
{
title : '区县' ,
dataIndex : 'districtCodes' ,
width : 160 ,
render : ( codes : string [ ] | null | undefined , row ) = >
row . scopeType === CityPartnerScopeType . CITY_WIDE ? '全城' : formatDistrictLabels ( codes ) ,
} ,
{ title : '公司名' , dataIndex : 'companyName' } ,
{ title : '主账号 ' , dataIndex : 'phone' , width : 120 } ,
{
title : '管辖' ,
dataIndex : 'scopeType ' ,
width : 100 ,
render : ( v ) = > ( v ? CITY_PARTNER_SCOPE_LABELS [ v as keyof typeof CITY_PARTNER_SCOPE_LABELS ] || v : '—' ) ,
} ,
{
title : '佣金' ,
width : 120 ,
render : ( _ , row ) = > ` ${ Math . round ( ( row . orderCommissionRate ? ? 0 ) * 100 ) } % / ${ Math . round ( ( row . redeemCommissionRate ? ? 0.03 ) * 100 ) } % ` ,
} ,
{ title : '子账号' , dataIndex : 'accountCount' , width : 70 , render : ( n ) = > Math . max ( 0 , n - 1 ) } ,
{ title : '创建 ' , dataIndex : 'createdA t' , width : 150 , render : fmtTime } ,
{
title : '操作' ,
width : 80 ,
render : ( _ , row ) = > (
< Button type = "link" size = "small" onClick = { ( ) = > void openPartner ( row . id ) } >
管 理
< / Button >
) ,
} ,
] ;
return (
< >
< Button
type = "primary"
style = { { marginBottom : 12 } }
onClick = { ( ) = > {
createForm . resetFields ( ) ;
createForm . setFieldsValue ( {
cityId ,
orderCommissionRate : 0 ,
redeemCommissionRate : 3 ,
scopeType : CityPartnerScopeType.CITY_WIDE ,
} ) ;
setCreateScopeType ( CityPartnerScopeType . CITY_WIDE ) ;
setCreateOpen ( true ) ;
} }
>
新 建 合 伙 人
< / Button >
< Table rowKey = "id" size = "small" loading = { loading } columns = { columns } dataSource = { partners } pagination = { false } / >
< Drawer
title = "城市合伙人"
width = { 640 }
open = { drawerOpen }
onClose = { ( ) = > setDrawerOpen ( false ) }
extra = { < Button type = "primary" onClick = { ( ) = > void savePartner ( ) } > 保 存 < / Button > }
>
{ detail && (
< Tabs
items = { [
{
key : 'info' ,
label : '主账号 ' ,
children : (
< Form form = { editForm } layout = "vertical" >
< Form.Item name = "companyName" label = "公司名" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "n ame" label = "主账号姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phon e" label = "登录手机号 " rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "contactP hone" label = "业务联系电话" > < Input / > < / Form.Item >
< Form.Item name = "address " label = "地址 " > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "bindingStatus" label = "绑定状态" rules = { [ { required : true } ] } >
< Select options = { BINDING_OPTIONS } / >
< / Form.Item >
< Form.Item name = "scopeType" label = "管辖类型" rules = { [ { required : true } ] } >
< Select options = { SCOPE_OPTIONS } onChange = { ( v ) = > setEditScopeType ( v ) } / >
< / Form.Item >
{ editScopeType === CityPartnerScopeType . DISTRICT && (
< Form.Item
name = "districtCodes"
label = "区县 "
extra = "仅作标识,可多选当前城市下的区县(不做互斥) "
>
< CityDistrictMultiSelect cityCode = { cityCode } / >
< / Form.Item >
) }
< Space style = { { width : '100%' } } size = "large" >
< Form.Item name = "orderCommissionRate" label = "订单佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< Form.Item name = "redeem CommissionRate" label = "核销 佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< / Space >
< Typography.Text type = "secondary" style = { { display : 'block' , marginBottom : 12 } } >
订 单 + 核 销 合 计 不 得 超 过 { ( maxPartnerCommissionRate * 100 ) . toFixed ( 2 ) } % ( 可 在 城 市 基 本 信 息 中 调 整 )
< / Typography.Text >
< Form.Item label = "管仓仓库" >
< Input
disabled
value = { detail . managedWarehouseName ? ? '未分配(请在仓库管理中设置)' }
/ >
< / Form.Item >
< Form.Item name = "bankAccountName" label = "户名" > < Input / > < / Form.Item >
< Form.Item name = "bankAccountNo " label = "账号 " > < Input / > < / Form.Item >
< Form.Item name = "bankBranch " label = "开户行 " > < Input / > < / Form.Item >
< / Form >
) ,
} ,
{
key : 'staff' ,
label : ` 子账号 ( ${ Math . max ( 0 , ( detail . accountCount ? ? 1 ) - 1 ) } ) ` ,
children : (
< PartnerSubAccountList
subs = { detail . children ? ? [ ] }
onAdd = { ( ) = > {
subForm . resetFields ( ) ;
subForm . setFieldsValue ( { staffRole : 'INTERNAL' , permissions : [ 'store:create' , 'store:manage' ] } ) ;
setSubOpen ( true ) ;
} }
onEdit = { ( row ) = > {
setSubEditId ( row . id ) ;
subEditForm . setFieldsValue ( {
name : row.name ,
phone : row.phone ,
staffRole : row.staffRole ? ? 'INTERNAL' ,
permissions : row.permissions ? ? [ ] ,
status : row.status ,
} ) ;
setSubEditOpen ( true ) ;
} }
onDelete = { ( subId ) = > void deleteSubAccount ( subId ) }
/ >
) ,
} ,
] }
/ >
) }
< / Drawer >
< Modal title = "新建城市合伙人" open = { createOpen } width = { 560 } onCancel = { ( ) = > setCreateOpen ( false ) } onOk = { async ( ) = > {
try {
const v = await createForm . validateFields ( ) ;
const err = commissionSumError (
Number ( v . orderCommissionRate ? ? 0 ) ,
Number ( v . redeemCommissionRate ? ? 3 ) ,
maxPartnerCommissionRate ,
) ;
if ( err ) {
message . error ( err ) ;
return ;
}
await request ( '/admin/partners' , {
method : 'POST' ,
body : JSON.stringify ( {
. . . v ,
cityId ,
orderCommissionRate : Number ( v . orderCommissionRate ? ? 0 ) / 100 ,
redeemCommissionRate : Number ( v . redeemCommissionRate ? ? 3 ) / 100 ,
districtCodes : createScopeType === CityPartnerScopeType . DISTRICT ? flattenDistrictCodes ( v . districtCodes ) : undefined ,
} ) ,
} ) ;
message . success ( '已创建' ) ;
setCreateOpen ( false ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} catch ( e ) {
const msg = formatApiError ( e ) ;
if ( msg ) message . error ( msg ) ;
}
} } >
< Form form = { createForm } layout = "vertical" >
< Form.Item name = "companyName" label = "公司名" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "name" label = "主账号姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "登录手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "address" label = "地址" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "scopeType" label = "管辖类型" rules = { [ { required : true } ] } >
< Select options = { SCOPE_OPTIONS } onChange = { ( v ) = > setCreateScopeType ( v ) } / >
< / Form.Item >
{ createScopeType === CityPartnerScopeType . DISTRICT && (
< Form.Item
name = "districtCodes"
label = "区县"
extra = "选填;仅作标识,可多选当前城市下的区县(不做互斥)"
>
< CityDistrictMultiSelect cityCode = { cityCode } / >
< / Form.Item >
) }
< Space style = { { width : '100%' } } size = "large" >
< Form.Item name = "orderCommissionRate" label = "订单佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< Form.Item name = "redeemCommissionRate" label = "核销佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< / Space >
< Typography.Text type = "secondary" style = { { display : 'block' , marginBottom : 12 } } >
订 单 + 核 销 合 计 不 得 超 过 { ( maxPartnerCommissionRate * 100 ) . toFixed ( 2 ) } %
< / Typography.Text >
< / Form >
< / Modal >
< Modal title = "添加子账号 " open = { subOpen } onCancel = { ( ) = > setSubOpen ( false ) } onOk = { async ( ) = > {
if ( ! detail ) return ;
const v = await subForm . validateFields ( ) ;
await request ( '/admin/partner-accounts' , {
method : 'POST' ,
body : JSON.stringify ( { . . . v , parentAccountId : detail.id } ) ,
} ) ;
message . success ( '已创建' ) ;
setSubOpen ( false ) ;
void openPartner ( detail . id ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} } >
< Form form = { subForm } layout = "vertical" >
< Form.Item name = "name" label = "姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "staffRole" label = "角色" initialValue = "INTERNAL" >
< Select options = { STAFF_ROLE_OPTIONS } / >
< / Form.Item >
< Form.Item name = "permissions" label = "权限 ">
< Checkbox.Group options = { PERM_OPTIONS } / >
< / Form.Item >
< / Form >
< / Modal >
< Modal title = "编辑子账号 " open = { subEditOpen } onCancel = { ( ) = > setSubEditOpen ( false ) } onOk = { async ( ) = > {
if ( ! subEditId || ! detail ) return ;
const v = await subEditForm . validateFields ( ) ;
await request ( ` /admin/partner-accounts/ ${ subEditId } ` , { method : 'PUT' , body : JSON.stringify ( v ) } ) ;
message . success ( '已更新' ) ;
setSubEditOpen ( false ) ;
void openPartner ( detail . id ) ;
} } >
< Form form = { subEditForm } layout = "vertical" >
< Form.Item name = "name" label = "姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "staffRole" label = "角色" > < Select options = { STAFF_ROLE_OPTIONS } / > < / Form.Item >
< Form.Item name = "status" label = "状态" >
< Select options = { [ { value : 'ACTIVE' , label : '启用' } , { value : 'DISABLED' , label : '停用' } ] } / >
< / Form.Item >
< Form.Item name = "permissions" label = "权限" >
< Checkbox.Group options = { PERM_OPTIONS } / >
< / Form.Item >
< / Form >
< / Modal >
< / >
) ;
import { useCallback , useEffect , useState } from 'react' ;
import {
Button ,
Checkbox ,
Drawer ,
Form ,
Input ,
InputNumber ,
Modal ,
Select ,
Space ,
Table ,
Tabs ,
Typography ,
message ,
} from 'antd' ;
import type { ColumnsType } from 'antd/es/table' ;
import {
CITY_PARTNER_SCOPE_LABELS ,
CITY_PARTNER_STATUS_LABELS ,
CityPartnerScopeType ,
CityPartnerStatus ,
PARTNER_PERMISSION_KEYS ,
PARTNER_PERMISSION_LABELS ,
PARTNER_STAFF_ROLE_LABELS ,
type PartnerPermissionKey ,
} from '@dukang/shared-types' ;
import { request , type Paginated } from '../lib/api' ;
import { omitNullFields } from '../lib/omit-null-fields ' ;
import { districtCodeLabel , formatDistrictLabels } from '../lib/china-region ' ;
import { fmtTime } from '../lib/constants ';
import CityDistrictMultiSelect from './CityDistrictMultiSelec t' ;
import PartnerSubAccountList from './PartnerSubAccountList' ;
import PartnerAssocPanel from './PartnerAssocPanel' ;
type PartnerRow = {
id : string ;
companyName : string ;
phone : string ;
name : string ;
scopeTyp e? : string ;
districtCodes? : string [ ] | null ;
orderCommissionRate? : number ;
redeemCommissionRate? : number ;
accountCount : number ;
createdAt : string ;
} ;
type PartnerDetail = PartnerRow & {
addres s? : string ;
districtCodes? : string [ ] | null ;
bindingStatus? : string ;
bankAccountName ? : string | null ;
b ankAccountNo ? : string | null ;
b ankBranch ? : string | null ;
managedWarehouseId ? : string | null ;
managedWarehouseName? : string | null ;
contactPhone? : string | null ;
children? : Array < {
id : string ;
phone : string ;
name : string ;
staffRole? : string ;
permissions? : string [ ] ;
status : string ;
} > ;
} ;
type Props = {
cityId : string ;
cityCode? : string ;
maxPartnerCommissionRate? : number ;
onChanged ? : ( ) = > void ;
} ;
const SCOPE_OPTIONS = Object . entries ( CITY_PARTNER_SCOPE_LABELS ) . map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
const BINDING_OPTIONS = Object . entries ( CITY_PARTNER_STATUS_LABELS ) . map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
const PERM_OPTIONS = PARTNER_PERMISSION_KEYS . map ( ( k : PartnerPermissionKey ) = > ( {
value : k ,
label : PARTNER_PERMISSION_LABELS [ k ] ,
} ) ) ;
const STAFF_ROLE_OPTIONS = Object . entries ( PARTNER_STAFF_ROLE_LABELS )
. filter ( ( [ value ] ) = > value !== 'PARTNER' )
. map ( ( [ value , label ] ) = > ( { value , label } ) ) ;
function flattenDistrictCodes ( values : string [ ] | string [ ] [ ] | undefined ) : string [ ] {
if ( ! values ? . length ) return [ ] ;
if ( Array . isArray ( values [ 0 ] ) ) {
return ( values as string [ ] [ ] ) . map ( ( path ) = > path [ path . length - 1 ] ) . filter ( Boolean ) ;
}
return values as string [ ] ;
}
function commissionSumError (
orderPercent : number ,
redeemPercent : number ,
maxRate : number ,
) : string | null {
// API 可能把 Prisma Decimal 序列化为字符串;`"0.05" + 1e-9` 会变成字符串拼接导致误判超限
const max = Number ( maxRate ) ;
const sum = Number ( orderPercent ) / 100 + Number ( redeemPercent ) / 100 ;
if ( ! Number . isFinite ( max ) || ! Number . isFinite ( sum ) ) return '佣金比例无效' ;
if ( sum > max + 1 e - 9 ) {
return ` 订单佣金与核销佣金合计不得超过 ${ ( max * 100 ) . toFixed ( 2 ) } %(当前 ${ ( sum * 100 ) . toFixed ( 2 ) } %) ` ;
}
return null ;
}
function formatApiError ( err : unknown ) : string | null {
if ( err && typeof err === 'object' && 'errorFields' in err ) return null ;
if ( ! ( err instanceof Error ) ) return '操作失败' ;
return err . message . replace ( /\b(\d{6})\b/g , ( code ) = > {
const label = districtCodeLabel ( code ) ;
return label !== code ? ` ${ label } ( ${ code } ) ` : code ;
} ) ;
}
export default function CityPartnersPanel ( {
cityId ,
cityCode ,
maxPartnerCommissionRate = 0.05 ,
onChanged ,
} : Props ) {
const [ editForm ] = Form . useForm ( ) ;
const [ createForm ] = Form . useForm ( ) ;
const [ subForm ] = Form . useForm ( ) ;
const [ subEditForm ] = Form . useForm ( ) ;
const [ partners , setPartners ] = useState < PartnerRow [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ detail , setDetail ] = useState < PartnerDetail | null > ( null ) ;
const [ drawerOpen , setDrawerOpen ] = useState ( false ) ;
const [ createOpen , setCreateOpen ] = useState ( false ) ;
const [ subOpen , setSubOpen ] = useState ( false ) ;
const [ subEditOpen , setSubEditOpen ] = useState ( false ) ;
const [ subEditId , setSubEditId ] = useState < string | null > ( null ) ;
const [ createScopeType , setCreateScopeType ] = useState < CityPartnerScopeType > ( CityPartnerScopeType . CITY_WIDE ) ;
const [ editScopeType , setEditScopeType ] = useState < CityPartnerScopeType > ( CityPartnerScopeType . CITY_WIDE ) ;
const loadPartners = useCallback ( async ( ) = > {
setLoading ( true ) ;
try {
const res = await request < Paginated < PartnerRow > > ( ` /admin/partners?cityId= ${ cityId } &pageSize=100 ` ) ;
setPartners ( res . items ) ;
} finally {
setLoading ( false ) ;
}
} , [ cityId ] ) ;
useEffect ( ( ) = > {
void loadPartners ( ) ;
} , [ loadPartners ] ) ;
async function openPartner ( id : string ) {
const d = await request < PartnerDetail > ( ` /admin/partners/ ${ id } ` ) ;
setDetail ( d ) ;
setEditScopeType ( ( d . scopeType as CityPartnerScopeType ) || CityPartnerScopeType . CITY_WIDE ) ;
editForm . setFieldsValue ( {
name : d.name ,
phone : d.phone ,
companyName : d.companyName ,
contactPhone : d.contactPhone ? ? d . phone ,
address : d.address ? ? '' ,
scopeType : d.scopeType ,
districtCodes : d.districtCodes ? ? [ ] ,
orderCommissionRate : ( d . orderCommissionRate ? ? 0 ) * 100 ,
redeemCommissionRate : ( d . redeemCommissionRate ? ? 0.03 ) * 100 ,
bindingStatus : d.bindingStatus ? ? CityPartnerStatus . ACTIVE ,
bankAccountName : d.bankAccountName ? ? '' ,
bankAccountNo : d.bankAccountNo ? ? '' ,
bankBranch : d.bankBranch ? ? '' ,
} ) ;
setDrawerOpen ( true ) ;
}
async function savePartner() {
if ( ! detail ) return ;
try {
const v = await editForm . validateFields ( ) ;
const err = commissionSumError (
Number ( v . orderCommissionRate ? ? 0 ) ,
Number ( v . redeemCommissionRate ? ? 0 ) ,
maxPartnerCommissionRate ,
) ;
if ( err ) {
message . error ( err ) ;
return ;
}
await request ( ` /admin/partners/ ${ detail . id } ` , {
method : 'PUT' ,
body : JSON.stringify ( {
. . . omitNullFields ( v ) ,
orderCommissionRate : Number ( v . orderCommissionRate ? ? 0 ) / 100 ,
redeemCommissionRate : Number ( v . redeemCommissionRate ? ? 3 ) / 100 ,
districtCodes : editScopeType === CityPartnerScopeType . DISTRICT ? flattenDistrictCodes ( v . districtCodes ) : undefined ,
} ) ,
} ) ;
message . success ( '已保存' ) ;
setDrawerOpen ( false ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} catch ( e ) {
const msg = formatApiError ( e ) ;
if ( msg ) message . error ( msg ) ;
}
}
async function deleteSubAccount ( subId : string ) {
await request ( ` /admin/partner-accounts/ ${ subId } ` , { method : 'DELETE' } ) ;
message . success ( '子账号已删除' ) ;
if ( detail ) void openPartner ( detail . id ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
}
const columns : ColumnsType < PartnerRow > = [
{
title : '区县' ,
dataIndex : 'districtCodes' ,
width : 160 ,
render : ( codes : string [ ] | null | undefined , row ) = >
row . scopeType === CityPartnerScopeType . CITY_WIDE ? '全城' : formatDistrictLabels ( codes ) ,
} ,
{ title : '公司名 ' , dataIndex : 'companyName' } ,
{ title : '主账号' , dataIndex : 'phone' , width : 120 } ,
{
title : '管辖 ' ,
dataIndex : 'scopeType' ,
width : 100 ,
render : ( v ) = > ( v ? CITY_PARTNER_SCOPE_LABELS [ v as keyof typeof CITY_PARTNER_SCOPE_LABELS ] || v : '—' ) ,
} ,
{
title : '佣金' ,
width : 120 ,
render : ( _ , row ) = > ` ${ Math . round ( ( row . orderCommissionRate ? ? 0 ) * 100 ) } % / ${ Math . round ( ( row . redeemCommissionRate ? ? 0.03 ) * 100 ) } % ` ,
} ,
{ title : '子账号 ' , dataIndex : 'accountCoun t' , width : 70 , render : ( n ) = > Math . max ( 0 , n - 1 ) } ,
{ title : '创建' , dataIndex : 'createdAt' , width : 150 , render : fmtTime } ,
{
title : '操作' ,
width : 80 ,
render : ( _ , row ) = > (
< Button type = "link" size = "small" onClick = { ( ) = > void openPartner ( row . id ) } >
管 理
< / Button >
) ,
} ,
] ;
return (
< >
< Button
type = "primary"
style = { { marginBottom : 12 } }
onClick = { ( ) = > {
createForm . resetFields ( ) ;
createForm . setFieldsValue ( {
cityId ,
orderCommissionRate : 0 ,
redeemCommissionRate : 3 ,
scopeType : CityPartnerScopeType.CITY_WIDE ,
} ) ;
setCreateScopeType ( CityPartnerScopeType . CITY_WIDE ) ;
setCreateOpen ( true ) ;
} }
>
新 建 合 伙 人
< / Button >
< Table rowKey = "id" size = "small" loading = { loading } columns = { columns } dataSource = { partners } pagination = { false } / >
< Drawer
title = "城市合伙人"
width = { 640 }
open = { drawerOpen }
onClose = { ( ) = > setDrawerOpen ( false ) }
extra = { < Button type = "primary" onClick = { ( ) = > void savePartner ( ) } > 保 存 < / Button > }
>
{ detail && (
< Tabs
items = { [
{
key : 'info ' ,
label : '主账号' ,
children : (
< Form form = { editForm } layout = "vertical" >
< Form.Item name = "companyN ame" label = "公司名" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "nam e" label = "主账号姓名 " rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "p hone" label = "登录手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "contactPhone " label = "业务联系电话 " > < Input / > < / Form.Item >
< Form.Item name = "address" label = "地址" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "bindingStatus" label = "绑定状态" rules = { [ { required : true } ] } >
< Select options = { BINDING_OPTIONS } / >
< / Form.Item >
< Form.Item name = "scopeType" label = "管辖类型" rules = { [ { required : true } ] } >
< Select options = { SCOPE_OPTIONS } onChange = { ( v ) = > setEditScopeType ( v ) } / >
< / Form.Item >
{ editScopeType === CityPartnerScopeType . DISTRICT && (
< Form.Item
name = "districtCodes "
label = "区县 "
extra = "仅作标识,可多选当前城市下的区县(不做互斥)"
>
< CityDistrictMultiSelect cityCode = { cityCode } / >
< / Form.Item >
) }
< Space style = { { width : '100%' } } size = "large" >
< Form.Item name = "order CommissionRate" label = "订单 佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< Form.Item name = "redeemCommissionRate" label = "核销佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< / Space >
< Typography.Text type = "secondary" style = { { display : 'block' , marginBottom : 12 } } >
订 单 + 核 销 合 计 不 得 超 过 { ( maxPartnerCommissionRate * 100 ) . toFixed ( 2 ) } % ( 可 在 城 市 基 本 信 息 中 调 整 )
< / Typography.Text >
< Form.Item label = "管仓仓库" >
< Input
disabled
value = { detail . managedWarehouseName ? ? '未分配(请在仓库管理中设置)' }
/ >
< / Form.Item >
< Form.Item name = "bankAccountName " label = "户名 " > < Input / > < / Form.Item >
< Form.Item name = "bankAccountNo " label = "账号 " > < Input / > < / Form.Item >
< Form.Item name = "bankBranch" label = "开户行" > < Input / > < / Form.Item >
< / Form >
) ,
} ,
{
key : 'staff' ,
label : ` 子账号 ( ${ Math . max ( 0 , ( detail . accountCount ? ? 1 ) - 1 ) } ) ` ,
children : (
< PartnerSubAccountList
subs = { detail . children ? ? [ ] }
onAdd = { ( ) = > {
subForm . resetFields ( ) ;
subForm . setFieldsValue ( { staffRole : 'INTERNAL' , permissions : [ 'store:create' , 'store:manage' ] } ) ;
setSubOpen ( true ) ;
} }
onEdit = { ( row ) = > {
setSubEditId ( row . id ) ;
subEditForm . setFieldsValue ( {
name : row.name ,
phone : row.phone ,
staffRole : row.staffRole ? ? 'INTERNAL' ,
permissions : row.permissions ? ? [ ] ,
status : row.status ,
} ) ;
setSubEditOpen ( true ) ;
} }
onDelete = { ( subId ) = > void deleteSubAccount ( subId ) }
/ >
) ,
} ,
{
key : 'assoc' ,
label : '关联码' ,
children : < PartnerAssocPanel partnerId = { detail . id } / > ,
} ,
] }
/ >
) }
< / Drawer >
< Modal title = "新建城市合伙人" open = { createOpen } width = { 560 } onCancel = { ( ) = > setCreateOpen ( false ) } onOk = { async ( ) = > {
try {
const v = await createForm . validateFields ( ) ;
const err = commissionSumError (
Number ( v . orderCommissionRate ? ? 0 ) ,
Number ( v . redeemCommissionRate ? ? 3 ) ,
maxPartnerCommissionRate ,
) ;
if ( err ) {
message . error ( err ) ;
return ;
}
await request ( '/admin/partners' , {
method : 'POST' ,
body : JSON.stringify ( {
. . . v ,
cityId ,
orderCommissionRate : Number ( v . orderCommissionRate ? ? 0 ) / 100 ,
redeemCommissionRate : Number ( v . redeemCommissionRate ? ? 3 ) / 100 ,
districtCodes : createScopeType === CityPartnerScopeType . DISTRICT ? flattenDistrictCodes ( v . districtCodes ) : undefined ,
} ) ,
} ) ;
message . success ( '已创建' ) ;
setCreateOpen ( false ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} catch ( e ) {
const msg = formatApiError ( e ) ;
if ( msg ) message . error ( msg ) ;
}
} } >
< Form form = { createForm } layout = "vertical" >
< Form.Item name = "companyName" label = "公司名" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "name" label = "主账号姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "登录手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "address" label = "地址" > < Input placeholder = "选填" / > < / Form.Item >
< Form.Item name = "scopeType" label = "管辖类型" rules = { [ { required : true } ] } >
< Select options = { SCOPE_OPTIONS } onChange = { ( v ) = > setCreateScopeType ( v ) } / >
< / Form.Item >
{ createScopeType === CityPartnerScopeType . DISTRICT && (
< Form.Item
name = "districtCodes"
label = "区县"
extra = "选填;仅作标识,可多选当前城市下的区县(不做互斥)"
>
< CityDistrictMultiSelect cityCode = { cityCode } / >
< / Form.Item >
) }
< Space style = { { width : '100%' } } size = "large" >
< Form.Item name = "orderCommissionRate" label = "订单佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< Form.Item name = "redeemCommissionRate" label = "核销佣金 %" > < InputNumber min = { 0 } max = { 100 } precision = { 2 } / > < / Form.Item >
< / Space >
< Typography.Text type = "secondary " style = { { display : 'block' , marginBottom : 12 } } >
订 单 + 核 销 合 计 不 得 超 过 { ( maxPartnerCommissionRate * 100 ) . toFixed ( 2 ) } %
< / Typography.Text >
< / Form >
< / Modal >
< Modal title = "添加子账号" open = { subOpen } onCancel = { ( ) = > setSubOpen ( false ) } onOk = { async ( ) = > {
if ( ! detail ) return ;
const v = await subForm . validateFields ( ) ;
await request ( '/admin/partner-accounts' , {
method : 'POST' ,
body : JSON.stringify ( { . . . v , parentAccountId : detail.id } ) ,
} ) ;
message . success ( '已创建' ) ;
setSubOpen ( false ) ;
void openPartner ( detail . id ) ;
void loadPartners ( ) ;
onChanged ? . ( ) ;
} } >
< Form form = { subForm } layout = "vertical ">
< Form.Item name = "name" label = "姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "staffRole" label = "角色" initialValue = "INTERNAL" >
< Select options = { STAFF_ROLE_OPTIONS } / >
< / Form.Item >
< Form.Item name = "permissions " label = "权限" >
< Checkbox.Group options = { PERM_OPTIONS } / >
< / Form.Item >
< / Form >
< / Modal >
< Modal title = "编辑子账号" open = { subEditOpen } onCancel = { ( ) = > setSubEditOpen ( false ) } onOk = { async ( ) = > {
if ( ! subEditId || ! detail ) return ;
const v = await subEditForm . validateFields ( ) ;
await request ( ` /admin/partner-accounts/ ${ subEditId } ` , { method : 'PUT' , body : JSON.stringify ( v ) } ) ;
message . success ( '已更新' ) ;
setSubEditOpen ( false ) ;
void openPartner ( detail . id ) ;
} } >
< Form form = { subEditForm } layout = "vertical" >
< Form.Item name = "name" label = "姓名" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "phone" label = "手机号" rules = { [ { required : true } ] } > < Input / > < / Form.Item >
< Form.Item name = "staffRole" label = "角色" > < Select options = { STAFF_ROLE_OPTIONS } / > < / Form.Item >
< Form.Item name = "status" label = "状态" >
< Select options = { [ { value : 'ACTIVE' , label : '启用' } , { value : 'DISABLED' , label : '停用' } ] } / >
< / Form.Item >
< Form.Item name = "permissions" label = "权限" >
< Checkbox.Group options = { PERM_OPTIONS } / >
< / Form.Item >
< / Form >
< / Modal >
< / >
) ;
}