微信支付
This commit is contained in:
@@ -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.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();
|
||||
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.coverUrl?.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>
|
||||
|
||||
Reference in New Issue
Block a user