import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Alert, Button, Descriptions, Drawer, Form, Image, Input, Modal, Select, Space, Steps, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; 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 = [ { title: '基本信息' }, { title: '照片上传' }, { title: '结算资质' }, ]; type StoreRow = { id: string; name: string; phone: string; status: string; cityName: string; district: string; address: string; intro: string | null; coverUrl: string | null; createdAt: string; cityRef?: { name: string; code: string }; partner?: { companyName: string }; account?: { phone: string; name: string; status: string }; }; type PartnerOption = { id: string; companyName: string }; type CityOption = { id: string; name: string; code: string; partnerId?: string | null; partner?: { id: string; companyName: string }; }; export default function StoresPage() { const navigate = useNavigate(); const [form] = Form.useForm(); const [editForm] = Form.useForm(); const [createForm] = Form.useForm(); const [filters, setFilters] = useState>({}); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/stores', () => { const qs = new URLSearchParams(); if (filters.name) qs.set('name', filters.name); if (filters.status) qs.set('status', filters.status); if (filters.phone) qs.set('phone', filters.phone); return qs; }, [filters], ); const [detail, setDetail] = useState | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [createStep, setCreateStep] = useState(0); const [createError, setCreateError] = useState(''); const [smsCooldown, setSmsCooldown] = useState(0); const [partners, setPartners] = useState([]); const [cities, setCities] = useState([]); 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() { setOptionsLoading(true); try { const qs = `pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`; const [p, c] = await Promise.all([ request>(`/admin/partners?${qs}`), request>(`/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() { setCreateOpen(false); setCreateStep(0); setCreateError(''); setSmsCooldown(0); createForm.resetFields(); } async function sendCreateSms() { const phone = String(createForm.getFieldValue('phone') ?? '').trim(); if (!/^1\d{10}$/.test(phone)) { message.error('请先填写正确的11位门店手机号'); return; } if (smsCooldown > 0) return; try { await request('/admin/stores/phone/sms/send', { method: 'POST', body: JSON.stringify({ phone }), }); message.success('验证码已发送'); setSmsCooldown(60); const timer = setInterval(() => { setSmsCooldown((s) => { if (s <= 1) { clearInterval(timer); return 0; } return s - 1; }); }, 1000); } catch (e) { message.error(e instanceof Error ? e.message : '发送失败'); } } function openCreateModal() { void loadOptions(); createForm.setFieldsValue({ envPhotoUrls: ['', '', ''], }); setCreateStep(0); setCreateError(''); setCreateOpen(true); } async function handleCreateNext() { const values = createForm.getFieldsValue(); if (createStep === 0) { const msg = validateStoreCreateStep1(values); if (msg) { setCreateError(msg); return; } try { await createForm.validateFields(['partnerId', 'regionCodes', 'cityId', 'name', 'phone', 'smsCode', 'address']); } catch { return; } } setCreateError(''); setCreateStep((s) => s + 1); } async function handleCreateSubmit() { try { const values = await createForm.validateFields(); const step3Msg = validateStoreCreateStep3(values); if (step3Msg) { setCreateError(step3Msg); return; } 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(), smsCode: values.smsCode.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(), }), }); 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' || first === 'smsCode') { setCreateStep(0); } return; } message.error(e instanceof Error ? e.message : '创建失败'); } } const columns: ColumnsType = [ { title: '封面', dataIndex: 'coverUrl', width: 72, render: (url) => url ? : '—', }, { title: '门店名', dataIndex: 'name', width: 140 }, { title: '城市', dataIndex: 'cityName', width: 80 }, { title: '电话', dataIndex: 'phone', width: 120 }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {STORE_STATUS_LABELS[s] || s}, }, { title: '开城合伙人', dataIndex: ['partner', 'companyName'], width: 120 }, { title: '介绍', dataIndex: 'intro', width: 160, ellipsis: true, render: (v) => v || '—' }, { title: '店长', dataIndex: ['account', 'name'], width: 90, render: (v) => v || '—' }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; return (
门店 共 {data?.total ?? 0} 家门店(含合伙人录入)
{ setFilters(v); setPage(1); }}> ({ value, label }))} onChange={async (status) => { await request(`/admin/stores/${detail.id}/status`, { method: 'PUT', body: JSON.stringify({ status }) }); message.success('状态已更新'); setDetail({ ...detail, status }); void reload(); }} /> )}> {detail && ( <> {String(detail.id)} {String(detail.province)}{String(detail.cityName)}{String(detail.district)}{String(detail.address)} {String(detail.redeemCount ?? 0)} {detail.coverUrl ? ( ) : null} )} {createStep > 0 && } {createStep < 2 ? ( ) : ( )} )} > {createError && ( )}
preV1 照片上传为选填,可直接下一步(与合伙人端一致)。 环境照片 {(fields) => (
{fields.map((field, index) => ( ))}
)}
); }