feat(admin): require store category on HQ store create
CI / verify (pull_request) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-26 07:13:56 +08:00
parent 2a208d5dea
commit ac1e0a9f13
4 changed files with 109 additions and 7 deletions
+14 -1
View File
@@ -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<StoreCreateForm, 'partnerAccountId' | 'cityId' | 'regionCodes' | 'name' | 'phone' | 'address' | 'intro'>,
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位手机号';
+83 -3
View File
@@ -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<PartnerOption[]>([]);
const [cities, setCities] = useState<CityOption[]>([]);
const [categoryTree, setCategoryTree] = useState<CategoryNode[]>([]);
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<Paginated<PartnerOption>>(`/admin/partners?${qs}`),
request<Paginated<CityOption>>(`/admin/cities?${qs}`),
request<CategoryNode[]>('/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 ? <Image src={url} width={48} height={48} style={{ objectFit: 'cover', borderRadius: 4 }} /> : '—',
},
{ 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() {
<StoreAuditMediaSection detail={detail} />
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="ID">{String(detail.id)}</Descriptions.Item>
<Descriptions.Item label="门店分类">
{detail.category && typeof detail.category === 'object' && 'name' in detail.category
? String((detail.category as { name?: string }).name || '—')
: '—'}
</Descriptions.Item>
<Descriptions.Item label="审核状态">
<Tag color={
String(detail.auditStatus) === 'PENDING' ? 'orange'
@@ -685,6 +733,38 @@ export default function StoresPage() {
<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="categoryParentId"
label="门店分类(大类)"
rules={[{ required: true, message: '请选择门店大类' }]}
>
<Select
showSearch
loading={optionsLoading}
optionFilterProp="label"
placeholder={optionsLoading ? '加载中…' : '选择大类'}
options={categoryParentOptions}
onChange={() => createForm.setFieldValue('categoryId', undefined)}
/>
</Form.Item>
<Form.Item
name="categoryId"
label="门店分类(细类)"
rules={[{ required: true, message: '请选择门店细类' }]}
extra={
<Typography.Link onClick={() => navigate('/store-categories')}>
</Typography.Link>
}
>
<Select
showSearch
optionFilterProp="label"
placeholder={selectedCategoryParentId ? '选择细类' : '请先选大类'}
disabled={!selectedCategoryParentId}
options={categoryChildOptions}
/>
</Form.Item>
<Form.Item name="name" label="门店名称" rules={[{ required: true, message: '请填写门店名称' }]}>
<Input placeholder="请输入门店名称" />
</Form.Item>