@@ -1,4 +1,4 @@
import { useState } from 'react' ;
import { useMemo , useState } from 'react' ;
import {
Alert ,
Button ,
@@ -18,13 +18,15 @@ import {
} from 'antd' ;
import type { ColumnsType } from 'antd/es/table' ;
import { request , type Paginated } from '../lib/api' ;
import { STORE_STATUS_LABELS , fmtTime } from '../lib/constants' ;
import { ADMIN_OPTIONS_PAGE_SIZE , STORE_STATUS_LABELS , fmtTime } from '../lib/constants' ;
import {
validateStoreCreateStep1 ,
validateStoreCreateStep3 ,
type StoreCreateForm ,
} from '../lib/storeCreate' ;
import { useAdminList } from '../lib/useAdminList' ;
import { resolveRegionBinding } from '../lib/china-region' ;
import ChinaRegionCascader from '../components/ChinaRegionCascader' ;
import OssUpload from '../components/OssUpload' ;
const CREATE_STEPS = [
@@ -50,7 +52,13 @@ type StoreRow = {
} ;
type PartnerOption = { id : string ; companyName : string } ;
type CityOption = { id : string ; name : string ; code : string ; partnerId? : string | null } ;
type CityOption = {
id : string ;
name : string ;
code : string ;
partnerId? : string | null ;
partner ? : { id : string ; companyName : string } ;
} ;
export default function StoresPage() {
const [ form ] = Form . useForm ( ) ;
@@ -75,16 +83,55 @@ export default function StoresPage() {
const [ createError , setCreateError ] = useState ( '' ) ;
const [ partners , setPartners ] = useState < PartnerOption [ ] > ( [ ] ) ;
const [ cities , setCities ] = useState < CityOption [ ] > ( [ ] ) ;
const [ optionsLoading , setOptionsLoading ] = useState ( false ) ;
const selectedPartnerId = Form . useWatch ( 'partnerId' , createForm ) ;
const selectedRegionCodes = Form . useWatch ( 'regionCodes' , createForm ) ;
const selectedCityId = Form . useWatch ( 'cityId' , createForm ) ;
function bindRegionSelection ( codes : string [ ] , partnerId? : string ) {
const binding = resolveRegionBinding ( codes , cities , partnerId ? ? selectedPartnerId ) ;
if ( ! binding ) {
createForm . setFieldsValue ( { regionCodes : codes , cityId : undefined } ) ;
return ;
}
createForm . setFieldsValue ( {
regionCodes : codes ,
province : binding.region.province ,
city : binding.region.city ,
district : binding.region.district ,
districtCode : binding.region.districtCode ,
cityId : binding.cityId ,
} ) ;
}
const regionBindingHint = useMemo ( ( ) = > {
if ( ! selectedRegionCodes ? . length ) return null ;
const binding = resolveRegionBinding ( selectedRegionCodes , cities , selectedPartnerId ) ;
if ( ! binding ) return null ;
if ( binding . cityId && binding . matchedCity ) {
return ` 已匹配开城城市: ${ binding . matchedCity . name } (区划 ${ binding . cityCode } ,区县 ${ binding . region . districtCode } ) ` ;
}
return ` 区划 ${ binding . cityCode } 暂未开城,请先在「开城 → 开城城市」添加 ` ;
} , [ selectedRegionCodes , cities , selectedPartnerId ] ) ;
async function loadOptions() {
const [ p , c ] = await Promise . all ( [
request < Paginated < PartnerOption > > ( '/admin/partners?pageSize=200' ) ,
request < Paginated < CityOption > > ( '/admin/cities?pageSize=200' ) ,
] ) ;
setPartners ( p . items ) ;
setCities ( c . items ) ;
setOptionsLoading ( true ) ;
try {
const qs = ` pageSize= ${ ADMIN_OPTIONS_PAGE_SIZE } ` ;
const [ p , c ] = await Promise . all ( [
request < Paginated < PartnerOption > > ( ` /admin/partners? ${ qs } ` ) ,
request < Paginated < CityOption > > ( ` /admin/cities? ${ qs } ` ) ,
] ) ;
setPartners ( p . items ) ;
setCities ( c . items ) ;
if ( ! p . items . length ) message . warning ( '暂无开城合伙人,请先在「开城 → 开城合伙人」中创建' ) ;
if ( ! c . items . length ) message . warning ( '暂无开城城市,请先在「开城 → 开城城市」中创建' ) ;
} catch ( e ) {
message . error ( e instanceof Error ? e . message : '加载合伙人/城市失败' ) ;
} finally {
setOptionsLoading ( false ) ;
}
}
function closeCreateModal() {
@@ -113,7 +160,7 @@ export default function StoresPage() {
return ;
}
try {
await createForm . validateFields ( [ 'partnerId' , 'cityId' , 'name' , 'phone' , 'district' , 'address' ] ) ;
await createForm . validateFields ( [ 'partnerId' , 'regionCodes' , 'cityId' , 'name' , 'phone' , 'address' ] ) ;
} catch {
return ;
}
@@ -123,49 +170,53 @@ export default function StoresPage() {
}
async function handleCreateSubmit() {
const values = createForm . getFieldsValue ( ) ;
const step1Msg = validateStoreCreateStep1 ( values ) ;
if ( step1Msg ) {
setCreateError ( step1Msg ) ;
setCreateStep ( 0 ) ;
return ;
}
const step3Msg = validateStoreCreateStep3 ( values ) ;
if ( step3Msg ) {
setCreateError ( step3Msg ) ;
return ;
}
try {
const values = await createForm . validateFields ( ) ;
const step3Msg = validateStoreCreateStep3 ( values ) ;
if ( step3Msg ) {
setCreateError ( step3Msg ) ;
return ;
}
const envPhotoUrls = ( values . envPhotoUrls ? ? [ ] ) . map ( ( u ) = > u ? . trim ( ) ) . filter ( Boolean ) as string [ ] ;
await request ( '/admin/stores' , {
method : 'POST' ,
body : JSON.stringify ( {
partnerId : values.partnerId ,
cityId : values.cityId ,
name : values.name.trim ( ) ,
phone : values.phone.trim ( ) ,
district : values.district.trim ( ) ,
address : values.address.trim ( ) ,
intro : values.intro?.trim ( ) || undefined ,
coverUrl : values.coverUrl?.trim ( ) || undefined ,
envPhotoUrls : envPhotoUrls.length ? envPhotoUrls : undefined ,
contractUrl : values.contract Url?.trim ( ) || undefined ,
bankAccountName : values.bankAccountName.trim ( ) ,
bankAccountNo : values.bankAccountNo.replace ( /\s/g , '' ) ,
bankBranch : values.bankBranch.trim ( ) ,
accountPhone : values.accountPhone?.trim ( ) || undefined ,
accountName : values.accountName?.trim ( ) || undefined ,
} ) ,
} ) ;
message . success ( '门店已创建' ) ;
closeCreateModal ( ) ;
void reload ( ) ;
const envPhotoUrls = ( values . envPhotoUrls ? ? [ ] ) . map ( ( u : string ) = > u ? . trim ( ) ) . filter ( Boolean ) as string [ ] ;
await request ( '/admin/stores' , {
method : 'POST' ,
body : JSON.stringify ( {
partnerId : values.partnerId ,
cityId : values.cityId ,
province : values.province ,
city : values.city ,
name : values.name.trim ( ) ,
phone : values.phone.trim ( ) ,
district : values.district.trim ( ) ,
address : values.address.trim ( ) ,
intro : values.intro?.trim ( ) || undefined ,
coverUrl : values.cover Url?.trim ( ) || undefined ,
envPhotoUrls : envPhotoUrls.length ? envPhotoUrls : undefined ,
contractUrl : values.contractUrl?.trim ( ) || undefined ,
bankAccountName : values.bankAccountName.trim ( ) ,
bankAccountNo : values.bankAccountNo.replace ( /\s/g , '' ) ,
bankBranch : values.bankBranch.trim ( ) ,
accountPhone : values.accountPhone?.trim ( ) || undefined ,
accountName : values.accountName?.trim ( ) || undefined ,
} ) ,
} ) ;
message . success ( '门店已创建' ) ;
closeCreateModal ( ) ;
void reload ( ) ;
} catch ( e ) {
if ( e && typeof e === 'object' && 'errorFields' in e ) {
const fields = e as { errorFields? : Array < { name : string [ ] } > } ;
const first = fields . errorFields ? . [ 0 ] ? . name ? . [ 0 ] ;
if ( first === 'partnerId' || first === 'cityId' || first === 'regionCodes' || first === 'name' || first === 'phone' ) {
setCreateStep ( 0 ) ;
}
return ;
}
message . error ( e instanceof Error ? e . message : '创建失败' ) ;
}
}
const filteredCities = selectedPartnerId
? cities . filter ( ( c ) = > String ( c . partnerId ? ? '' ) === selectedPartnerId )
: cities ;
const columns : ColumnsType < StoreRow > = [
{
title : '封面' , dataIndex : 'coverUrl' , width : 72 ,
@@ -277,33 +328,47 @@ export default function StoresPage() {
{ createError && (
< Alert type = "error" message = { createError } showIcon style = { { marginBottom : 16 } } / >
) }
< Form form = { createForm } layout = "vertical" >
{ createStep === 0 && (
< >
< Form form = { createForm } layout = "vertical" preserve >
< div style = { { display : createStep === 0 ? 'block' : 'none' } } >
< Form.Item name = "partnerId" label = "开城合伙人" rules = { [ { required : true , message : '请选择开城合伙人' } ] } >
< Select
showSearch
loading = { optionsLoading }
optionFilterProp = "label"
placeholder = { optionsLoading ? '加载中…' : '请选择开城合伙人' }
options = { partners . map ( ( p ) = > ( { value : p.id , label : p.companyName } ) ) }
onChange = { ( ) = > createForm . setFieldValue ( 'cityId' , undefined ) }
onChange = { ( partnerId ) = > {
const codes = createForm . getFieldValue ( 'regionCodes' ) as string [ ] | undefined ;
if ( codes ? . length === 3 ) bindRegionSelection ( codes , partnerId ) ;
else createForm . setFieldValue ( 'cityId' , undefined ) ;
} }
/ >
< / Form.Item >
< Form.Item name = "cityId" label = "开城城市" rules = { [ { required : true , message : '请选择开城城市' } ] } >
< Select
showSearch
optionFilterProp = "label"
options = { filteredCities . map ( ( c ) = > ( { value : c.id , label : ` ${ c . name } ( ${ c . code } ) ` } ) ) }
/ >
< Form.Item
name = "regionCodes"
label = "所在地区"
rules = { [ { required : true , message : '请选择省 / 市 / 区县' } ] }
extra = { regionBindingHint ? (
< Typography.Text type = { selectedCityId ? 'secondary' : 'warning' } >
{ regionBindingHint }
< / Typography.Text >
) : null }
>
< ChinaRegionCascader onChange = { ( codes ) = > bindRegionSelection ( codes ) } / >
< / Form.Item >
< Form.Item name = "cityId" hidden rules = { [ { required : true , message : '请选择所在地区以匹配开城城市' } ] } >
< Input / >
< / Form.Item >
< Form.Item name = "province" hidden > < Input / > < / Form.Item >
< Form.Item name = "city" hidden > < Input / > < / Form.Item >
< Form.Item name = "district" hidden > < Input / > < / Form.Item >
< Form.Item name = "districtCode" hidden > < Input / > < / Form.Item >
< Form.Item name = "name" label = "门店名称" rules = { [ { required : true , message : '请填写门店名称' } ] } >
< Input placeholder = "请输入门店名称" / >
< / Form.Item >
< Form.Item name = "phone" label = "联系电话" rules = { [ { required : true , message : '请填写联系电话' } ] } >
< Input placeholder = "11位手机号" / >
< / Form.Item >
< Form.Item name = "district" label = "区县" rules = { [ { required : true , message : '请填写区县' } ] } >
< Input placeholder = "例如:金水区" / >
< / Form.Item >
< Form.Item name = "address" label = "详细地址" rules = { [ { required : true , message : '请填写详细地址' } ] } >
< Input.TextArea rows = { 2 } placeholder = "请输入详细门牌号" / >
< / Form.Item >
@@ -316,10 +381,8 @@ export default function StoresPage() {
< Form.Item name = "accountName" label = "店长姓名" >
< Input placeholder = "默认同门店名" / >
< / Form.Item >
< / >
) }
{ createStep === 1 && (
< >
< / div >
< div style = { { display : createStep === 1 ? 'block' : 'none' } } >
< Typography.Paragraph type = "secondary" >
preV1 照 片 上 传 为 选 填 , 可 直 接 下 一 步 ( 与 合 伙 人 端 一 致 ) 。
< / Typography.Paragraph >
@@ -341,10 +404,8 @@ export default function StoresPage() {
< Form.Item name = "contractUrl" label = "签约合同" >
< OssUpload bizType = "CONTRACT" mediaType = "FILE" accept = "image/*,.pdf" / >
< / Form.Item >
< / >
) }
{ createStep === 2 && (
< >
< / div >
< div style = { { display : createStep === 2 ? 'block' : 'none' } } >
< Form.Item name = "bankAccountName" label = "户主姓名" rules = { [ { required : true , message : '请填写户主姓名' } ] } >
< Input placeholder = "银行卡实名姓名" / >
< / Form.Item >
@@ -359,8 +420,7 @@ export default function StoresPage() {
showIcon
message = "请确保银行卡信息准确,以免影响门店餐费结算。"
/ >
< / >
) }
< / div >
< / Form >
< / Modal >
< / div >