import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; import ChinaRegionPicker from '../components/ChinaRegionPicker'; import OssUploadField from '../components/OssUploadField'; import { request } from '../lib/api'; import { toastError, toastSuccess } from '../lib/toast'; import { resolveRegionBinding } from '../lib/china-region'; import { checkStorePhoneAvailable, sendStorePhoneSms } from '../lib/storePhone'; import { fetchPartnerCities, type OpenCityOption } from '../lib/upload'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { clearAllStoreDrafts, defaultStoreForm, loadStoreDraft, saveStoreDraft, type StoreDraftForm, validateStoreStep1, validateStoreStep2, validateStoreStep3, patchEnvPhotoAt, } from '../lib/storeDraft'; const STEPS = ['基本信息', '照片上传', '结算资质'] as const; type FieldErrors = { phone?: string; storeSmsCode?: string; }; function isPhoneConflictMessage(message: string) { return message.includes('手机号已绑定'); } function isPhoneValidationMessage(message: string) { return message.includes('联系电话') || message.includes('手机号') || message.includes('验证码'); } export default function StoreCreatePage() { 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 [cities, setCities] = useState([]); const [citiesError, setCitiesError] = useState(''); const [smsCooldown, setSmsCooldown] = useState(0); const [smsHint, setSmsHint] = useState(''); const draftSaveDisabledRef = useRef(false); function reportFormError(message: string) { setSubmitError(message); } const stepFromUrl = Number(params.get('step') || 0); const step = stepFromUrl >= 1 && stepFromUrl <= 3 ? 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(() => { void fetchPartnerCities() .then((items) => { setCities(items); if (!items.length) setCitiesError('暂无开城城市,请联系总部在后台配置'); }) .catch((e) => { const msg = e instanceof Error ? e.message : '加载开城城市失败'; setCitiesError(msg); }); }, []); 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(''); let nextPatch = patch; if ('phone' in patch) { setFieldErrors((prev) => ({ ...prev, phone: undefined, storeSmsCode: undefined })); setSmsHint(''); nextPatch = { ...patch, storeSmsCode: '' }; } if ('storeSmsCode' in patch) { setFieldErrors((prev) => ({ ...prev, storeSmsCode: undefined })); } setForm((prev) => ({ ...prev, ...nextPatch })); } function patchEnvPhotoUrl(index: number, url: string) { setForm((prev) => ({ ...prev, envPhotoUrls: patchEnvPhotoAt(prev.envPhotoUrls, index, url), })); setSubmitError(''); } 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 sendStorePhoneCode() { const phone = form.phone.trim(); if (!/^1\d{10}$/.test(phone)) { setFieldErrors({ phone: '请先填写正确的11位手机号' }); return; } setSmsHint(''); setFieldErrors((prev) => ({ ...prev, phone: undefined })); try { const phoneCheck = await checkStorePhoneAvailable(phone); if (!phoneCheck.available) { setFieldErrors({ phone: phoneCheck.message ?? '该手机号不可用于门店账号' }); return; } const res = await sendStorePhoneSms(phone); setSmsHint(`验证码已发送至 ${res.maskedPhone}`); setSmsCooldown(60); const timer = setInterval(() => { setSmsCooldown((s) => { if (s <= 1) { clearInterval(timer); return 0; } return s - 1; }); }, 1000); } catch (e) { const msg = e instanceof Error ? e.message : '验证码发送失败'; setFieldErrors({ phone: msg }); } } async function handleNext() { if (step === 1) { const msg = validateStoreStep1(form); if (msg) { reportFormError(msg); return; } } if (step === 2) { const msg = validateStoreStep2(form); if (msg) { reportFormError(msg); return; } } goStep(step + 1); } async function submit() { const msg = validateStoreStep3(form); if (msg) { if (isPhoneValidationMessage(msg)) { if (msg.includes('验证码')) { setFieldErrors({ storeSmsCode: msg }); } else { setFieldErrors({ phone: msg }); } return; } reportFormError(msg); 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, 3); 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(), smsCode: form.storeSmsCode.trim(), district: form.district.trim(), address: form.address.trim(), intro: form.intro.trim() || undefined, coverUrl: form.coverUrl.trim() || undefined, envPhotoUrls: envPhotoUrls.length ? envPhotoUrls : undefined, contractUrl: form.contractUrl.trim() || undefined, bankAccountName: form.bankAccountName.trim(), bankAccountNo: form.bankAccountNo.replace(/\s/g, ''), bankBranch: form.bankBranch.trim(), ...(confirmBindExisting ? { confirmBindExisting: true } : {}), }), }); draftSaveDisabledRef.current = true; clearAllStoreDrafts(accountId); setForm(defaultStoreForm()); setFieldErrors({}); setSubmitError(''); setSmsHint(''); 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; } if (/验证码/.test(message)) { setFieldErrors({ storeSmsCode: message }); goStep(3); return; } setSubmitError(message); toastError(message); } finally { setSubmitting(false); } } const progress = step === 1 ? 0 : step === 2 ? 50 : 100; const nextDisabled = submitting; return (
navigate('/stores')} /> {(submitError || citiesError) && (

{submitError || citiesError}

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

门店基本信息

{regionBindingHint && (

{regionBindingHint}

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