import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import ChinaRegionPicker from '../components/ChinaRegionPicker'; import OssUploadField from '../components/OssUploadField'; import MultiOssUploadField from '../components/MultiOssUploadField'; import { request } from '../lib/api'; import { fetchClientConfig } from '../lib/wechat-auth'; import { toastError, toastSuccess } from '../lib/toast'; import { resolveRegionBinding } from '../lib/china-region'; import { checkStorePhoneAvailable } from '../lib/storePhone'; import { fetchPartnerCities, type OpenCityOption } from '../lib/upload'; import { isWechatEnv, weixinSdk } from '../lib/weixin'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import TencentLocPickerOverlay from '../components/TencentLocPickerOverlay'; import { usePartnerPageView } from '../lib/usePageView'; import { clearAllStoreDrafts, defaultStoreForm, loadStoreDraft, saveStoreDraft, type StoreDraftForm, validateStoreStep1, validateStoreStep2, validateStoreStep3, MIN_ENV_PHOTO_COUNT, MAX_CONTRACT_COUNT, } from '../lib/storeDraft'; import StorePackagesForm from '../components/StorePackagesForm'; import { normalizePackageFormItems, validatePackageFormItems } from '../lib/storePackages'; const STEPS = ['基本信息', '照片上传', '结算资质', '门店套餐'] as const; const DEFAULT_PARTNER_ONBOARD_CS_HINT = '使用问题、提现问题等随时可联系【杜康好客】客服'; type StoreCategoryNode = { id: string; name: string; children?: StoreCategoryNode[]; }; type FieldErrors = { phone?: string; contactPhone?: string; }; function isPhoneConflictMessage(message: string) { return message.includes('手机号已绑定'); } function isPhoneValidationMessage(message: string) { return message.includes('联系电话') || message.includes('手机号'); } export default function StoreCreatePage() { usePartnerPageView('partner_store_create_view'); const navigate = useNavigate(); const { account } = usePartnerSession(); const accountId = account?.id; const [params, setParams] = useSearchParams(); const saved = loadStoreDraft(accountId); const [form, setForm] = useState(saved?.form ?? defaultStoreForm()); const [submitError, setSubmitError] = useState(''); const [fieldErrors, setFieldErrors] = useState({}); const [submitting, setSubmitting] = useState(false); const [csAdded, setCsAdded] = useState(false); const [csQrUrl, setCsQrUrl] = useState(null); const [csHint, setCsHint] = useState(DEFAULT_PARTNER_ONBOARD_CS_HINT); const [csConfigLoading, setCsConfigLoading] = useState(false); const [cities, setCities] = useState([]); const [citiesError, setCitiesError] = useState(''); const [categoryTree, setCategoryTree] = useState([]); const [mapPickerOpen, setMapPickerOpen] = useState(false); const [stepErrorModal, setStepErrorModal] = useState(null); const draftSaveDisabledRef = useRef(false); function reportFormError(message: string) { setSubmitError(message); } function reportStepError(message: string) { setSubmitError(message); setStepErrorModal(message); } const stepFromUrl = Number(params.get('step') || 0); const step = stepFromUrl >= 1 && stepFromUrl <= 4 ? stepFromUrl : (saved?.step ?? 1); useEffect(() => { if (!stepFromUrl && saved?.step) { setParams({ step: String(saved.step) }, { replace: true }); } }, [stepFromUrl, saved?.step, setParams]); useEffect(() => { if (draftSaveDisabledRef.current) return; saveStoreDraft({ step, form }, accountId); }, [step, form, accountId]); useEffect(() => { if (step !== 2 || !isWechatEnv()) return; void weixinSdk.init().catch(() => { /* OssUploadField 点击时会再次初始化 */ }); }, [step]); useEffect(() => { if (step !== 4) return; let cancelled = false; setCsConfigLoading(true); void fetchClientConfig() .then((cfg) => { if (cancelled) return; setCsQrUrl((cfg.partnerOnboardCsQrUrl ?? '').trim() || null); setCsHint((cfg.partnerOnboardCsHint ?? '').trim() || DEFAULT_PARTNER_ONBOARD_CS_HINT); }) .catch(() => { if (cancelled) return; setCsQrUrl(null); setCsHint(DEFAULT_PARTNER_ONBOARD_CS_HINT); }) .finally(() => { if (!cancelled) setCsConfigLoading(false); }); return () => { cancelled = true; }; }, [step]); useEffect(() => { void fetchPartnerCities() .then((items) => { setCities(items); if (!items.length) setCitiesError('暂无开城城市,请联系总部在后台配置'); }) .catch((e) => { const msg = e instanceof Error ? e.message : '加载开城城市失败'; setCitiesError(msg); }); }, []); useEffect(() => { void request('PARTNER_H5', '/partner/store-categories') .then((list) => { const tree = Array.isArray(list) ? list : []; setCategoryTree(tree); if (form.categoryId && !form.categoryParentId) { const parent = tree.find((root) => (root.children ?? []).some((child) => child.id === form.categoryId), ); if (parent) patchForm({ categoryParentId: parent.id }); } }) .catch(() => setCategoryTree([])); }, []); const categoryChildren = useMemo(() => { const parent = categoryTree.find((item) => item.id === form.categoryParentId); return Array.isArray(parent?.children) ? parent!.children! : []; }, [categoryTree, form.categoryParentId]); useEffect(() => { if (!cities.length || form.regionCodes.length < 3 || form.cityId) return; const binding = resolveRegionBinding(form.regionCodes, cities); if (!binding) return; patchForm({ cityId: binding.cityId ?? '', province: binding.region.province, city: binding.region.city, district: binding.region.district, }); }, [cities, form.regionCodes, form.cityId]); const regionBindingHint = useMemo(() => { if (!form.regionCodes.length) return null; const binding = resolveRegionBinding(form.regionCodes, cities); if (!binding) return null; if (binding.cityId && binding.matchedCity) { return `已匹配开城城市:${binding.matchedCity.name}`; } return '所选地区暂未开城,请联系总部配置对应区划'; }, [form.regionCodes, cities]); function patchForm(patch: Partial) { setSubmitError(''); if ('phone' in patch || 'contactPhone' in patch) { setFieldErrors((prev) => ({ ...prev, ...('phone' in patch ? { phone: undefined } : {}), ...('contactPhone' in patch ? { contactPhone: undefined } : {}), })); } setForm((prev) => ({ ...prev, ...patch })); } function bindRegionSelection(codes: string[]) { const binding = resolveRegionBinding(codes, cities); if (!binding) { patchForm({ regionCodes: codes, cityId: '', province: '', city: '', district: '' }); return; } patchForm({ regionCodes: codes, cityId: binding.cityId ?? '', province: binding.region.province, city: binding.region.city, district: binding.region.district, }); } function goStep(n: number) { setParams({ step: String(n) }); setSubmitError(''); } async function handleNext() { if (step === 1) { const msg = validateStoreStep1(form); if (msg) { reportStepError(msg); return; } } if (step === 2) { const msg = validateStoreStep2(form); if (msg) { reportStepError(msg); return; } } if (step === 3) { const msg = validateStoreStep3(form); if (msg) { if (isPhoneValidationMessage(msg)) { setFieldErrors( msg.includes('联系电话') ? { contactPhone: msg } : { phone: msg }, ); reportStepError(msg); return; } reportStepError(msg); return; } } goStep(step + 1); } async function submit(skipPackages = false) { const qr = (csQrUrl ?? '').trim(); if (!qr) { reportFormError('客服二维码暂未配置,请联系总部'); return; } if (!csAdded) { reportFormError('请先勾选「我已添加【杜康好客】客服」'); return; } const msg = validateStoreStep3(form); if (msg) { if (isPhoneValidationMessage(msg)) { setFieldErrors( msg.includes('联系电话') ? { contactPhone: msg } : { phone: msg }, ); return; } reportFormError(msg); return; } const packagesToSubmit = skipPackages ? [] : form.packages || []; if (!skipPackages) { const pkgMsg = validatePackageFormItems(packagesToSubmit); if (pkgMsg) { reportFormError(pkgMsg); return; } } const step1Msg = validateStoreStep1(form); if (step1Msg) { reportFormError(step1Msg); goStep(1); return; } if (!/^\d+$/.test(form.cityId.trim())) { reportFormError('所选地区未匹配到开城城市,请重新选择'); goStep(1); return; } const step2Msg = validateStoreStep2(form); if (step2Msg) { reportFormError(step2Msg); goStep(2); return; } setSubmitting(true); setSubmitError(''); setFieldErrors({}); let confirmBindExisting = false; try { const phoneCheck = await checkStorePhoneAvailable(form.phone.trim()); if (!phoneCheck.available) { setFieldErrors({ phone: phoneCheck.message ?? '该手机号已绑定门店,请更换', }); setSubmitting(false); return; } if (phoneCheck.needConfirm) { const ok = window.confirm( phoneCheck.message ?? `该手机号已是门店主账号(已绑 ${phoneCheck.existingStoreCount ?? 0} 家店),确认后将追加绑定新店。是否继续?`, ); if (!ok) { setFieldErrors({ phone: '已取消绑定已有主账号,请更换手机号或确认后继续' }); setSubmitting(false); return; } confirmBindExisting = true; } } catch (e) { reportFormError(e instanceof Error ? e.message : '手机号校验失败'); setSubmitting(false); return; } try { const envPhotoUrls = Array.from( new Set(form.envPhotoUrls.map((u) => u.trim()).filter(Boolean)), ).slice(0, 20); const contractUrls = Array.from( new Set((form.contractUrls ?? []).map((u) => u.trim()).filter(Boolean)), ).slice(0, MAX_CONTRACT_COUNT); const result = await request<{ store: { id: string } }>('PARTNER_H5', '/partner/stores', { method: 'POST', body: JSON.stringify({ cityId: form.cityId, province: form.province, city: form.city, name: form.name.trim(), phone: form.phone.trim(), contactPhone: form.contactPhone.trim() || form.phone.trim(), district: form.district.trim(), address: form.address.trim(), ...(form.latitude.trim() && form.longitude.trim() ? { latitude: Number(form.latitude), longitude: Number(form.longitude), } : {}), openTime: form.openTime.trim(), closeTime: form.closeTime.trim(), ...(form.dualHours ? { openTime2: form.openTime2.trim(), closeTime2: form.closeTime2.trim() } : {}), ...(form.avgPrice.trim() ? { avgPrice: Number(form.avgPrice) } : {}), categoryId: form.categoryId.trim(), intro: form.intro.trim() || undefined, benefitUsageRule: form.benefitUsageRule.trim() || undefined, coverUrl: form.coverUrl.trim() || undefined, envPhotoUrls: envPhotoUrls.length ? envPhotoUrls : undefined, contractUrls: contractUrls.length ? contractUrls : undefined, bankAccountName: form.bankAccountName.trim(), bankAccountNo: form.bankAccountNo.replace(/\s/g, ''), bankBranch: form.bankBranch.trim(), ...(confirmBindExisting ? { confirmBindExisting: true } : {}), }), }); draftSaveDisabledRef.current = true; clearAllStoreDrafts(accountId); const packages = normalizePackageFormItems(packagesToSubmit); if (packages.length) { await request('PARTNER_H5', `/partner/stores/${result.store.id}/packages`, { method: 'PUT', body: JSON.stringify({ packages: packages.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })), }), }); } setForm(defaultStoreForm()); setFieldErrors({}); setSubmitError(''); setParams({ step: '1' }, { replace: true }); toastSuccess('门店录入成功'); navigate(`/stores/${result.store.id}`); } catch (e) { const message = e instanceof Error ? e.message : '提交失败'; if (isPhoneConflictMessage(message)) { setFieldErrors({ phone: message }); return; } setSubmitError(message); toastError(message); } finally { setSubmitting(false); } } const progress = step === 1 ? 0 : step === 2 ? 33 : step === 3 ? 66 : 100; const canSubmitOnboard = !!csQrUrl?.trim() && csAdded && !submitting && !csConfigLoading; const nextDisabled = submitting; return (
{(submitError || citiesError) && (

{submitError || citiesError}

)} {step === 1 && ( <>

门店基本信息

{regionBindingHint && (

{regionBindingHint}

)}
store patchForm({ name: e.target.value })} />