From ac1e0a9f137f82e9e75a4d7c1057f7201eecea0f Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Sun, 26 Jul 2026 07:13:56 +0800 Subject: [PATCH] feat(admin): require store category on HQ store create Co-authored-by: Cursor --- apps/admin-web/src/lib/storeCreate.ts | 15 +++- apps/admin-web/src/pages/StoresPage.tsx | 86 ++++++++++++++++++- .../src/modules/ops/admin-stores.service.ts | 11 ++- .../src/modules/ops/dto/admin-mutate.dto.ts | 4 +- 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/apps/admin-web/src/lib/storeCreate.ts b/apps/admin-web/src/lib/storeCreate.ts index e21fd57..a1ed5b9 100644 --- a/apps/admin-web/src/lib/storeCreate.ts +++ b/apps/admin-web/src/lib/storeCreate.ts @@ -6,6 +6,8 @@ export type StoreCreateForm = { city?: string; district: string; districtCode?: string; + categoryParentId?: string; + categoryId: string; name: string; phone: string; address: string; @@ -23,11 +25,22 @@ const PHONE_RE = /^1\d{10}$/; const BANK_RE = /^\d{16,19}$/; export function validateStoreCreateStep1( - form: Pick, + form: Pick< + StoreCreateForm, + | 'partnerAccountId' + | 'cityId' + | 'regionCodes' + | 'categoryId' + | 'name' + | 'phone' + | 'address' + | 'intro' + >, ): string | null { if (!form.partnerAccountId) return '请选择开城合伙人'; if (!form.regionCodes || form.regionCodes.length < 3) return '请选择省 / 市 / 区县'; if (!form.cityId) return '所选地区未匹配到开城城市,请先在「开城 → 城市」配置对应区划'; + if (!form.categoryId?.trim()) return '请选择门店分类(细类)'; if (!form.name?.trim()) return '请填写门店名称'; if (!form.phone?.trim()) return '请填写门店手机号'; if (!PHONE_RE.test(form.phone.trim())) return '门店手机号须为11位手机号'; diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index 411203b..b82c781 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -263,6 +263,7 @@ type StoreRow = { cityRef?: { name: string; code: string }; partner?: { companyName: string }; account?: { phone: string; name: string; status: string }; + category?: { id: string; name: string; parentId?: string | null } | null; }; type PartnerOption = { id: string; companyName: string }; @@ -272,6 +273,12 @@ type CityOption = { code: string; partnerBindings?: Array<{ partnerAccountId: string; partnerCompanyName?: string }>; }; +type CategoryNode = { + id: string; + name: string; + status?: string; + children?: CategoryNode[]; +}; export default function StoresPage() { const navigate = useNavigate(); @@ -301,11 +308,27 @@ export default function StoresPage() { const [createError, setCreateError] = useState(''); const [partners, setPartners] = useState([]); const [cities, setCities] = useState([]); + const [categoryTree, setCategoryTree] = useState([]); const [optionsLoading, setOptionsLoading] = useState(false); const selectedPartnerId = Form.useWatch('partnerAccountId', createForm); const selectedRegionCodes = Form.useWatch('regionCodes', createForm); const selectedCityId = Form.useWatch('cityId', createForm); + const selectedCategoryParentId = Form.useWatch('categoryParentId', createForm); + + const categoryParentOptions = useMemo( + () => + categoryTree + .filter((n) => n.status !== 'INACTIVE') + .map((n) => ({ value: n.id, label: n.name })), + [categoryTree], + ); + const categoryChildOptions = useMemo(() => { + const parent = categoryTree.find((n) => n.id === selectedCategoryParentId); + return (parent?.children ?? []) + .filter((n) => n.status !== 'INACTIVE') + .map((n) => ({ value: n.id, label: n.name })); + }, [categoryTree, selectedCategoryParentId]); function bindRegionSelection(codes: string[], partnerAccountId?: string) { const binding = resolveRegionBinding(codes, cities, partnerAccountId ?? selectedPartnerId); @@ -337,16 +360,21 @@ export default function StoresPage() { setOptionsLoading(true); try { const qs = `pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`; - const [p, c] = await Promise.all([ + const [p, c, cats] = await Promise.all([ request>(`/admin/partners?${qs}`), request>(`/admin/cities?${qs}`), + request('/admin/store-categories'), ]); setPartners(p.items); setCities(c.items); + setCategoryTree(Array.isArray(cats) ? cats : []); if (!p.items.length) message.warning('暂无开城合伙人,请先在「开城 → 城市」详情中创建合伙人'); if (!c.items.length) message.warning('暂无开城城市,请先在「开城 → 城市」中创建'); + if (!Array.isArray(cats) || !cats.length) { + message.warning('暂无门店分类,请先在「门店 → 门店分类」中配置'); + } } catch (e) { - message.error(e instanceof Error ? e.message : '加载合伙人/城市失败'); + message.error(e instanceof Error ? e.message : '加载合伙人/城市/分类失败'); } finally { setOptionsLoading(false); } @@ -379,7 +407,16 @@ export default function StoresPage() { return; } try { - await createForm.validateFields(['partnerAccountId', 'regionCodes', 'cityId', 'name', 'phone', 'address']); + await createForm.validateFields([ + 'partnerAccountId', + 'regionCodes', + 'cityId', + 'categoryParentId', + 'categoryId', + 'name', + 'phone', + 'address', + ]); } catch { return; } @@ -403,6 +440,7 @@ export default function StoresPage() { body: JSON.stringify({ partnerAccountId: values.partnerAccountId, cityId: values.cityId, + categoryId: values.categoryId, province: values.province, city: values.city, name: values.name.trim(), @@ -441,6 +479,11 @@ export default function StoresPage() { render: (url) => url ? : '—', }, { title: '门店名', dataIndex: 'name', width: 140 }, + { + title: '分类', + width: 100, + render: (_, row) => row.category?.name || '—', + }, { title: '城市', dataIndex: 'cityName', width: 80 }, { title: '电话', dataIndex: 'phone', width: 120 }, { @@ -580,6 +623,11 @@ export default function StoresPage() { {String(detail.id)} + + {detail.category && typeof detail.category === 'object' && 'name' in detail.category + ? String((detail.category as { name?: string }).name || '—') + : '—'} +