fix(admin): city-scoped district pick and clearer overlap errors
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Region partners only select districts under the chosen city; overlap errors name the occupying partner. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -274,6 +274,7 @@ export default function CitiesPage() {
|
||||
children: detail?.id ? (
|
||||
<CityPartnersPanel
|
||||
cityId={String(detail.id)}
|
||||
cityCode={detail.code != null ? String(detail.code) : undefined}
|
||||
maxPartnerCommissionRate={
|
||||
detail.maxPartnerCommissionRate != null
|
||||
? Number(detail.maxPartnerCommissionRate)
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
@@ -32,9 +31,10 @@ import {
|
||||
type PartnerPermissionKey,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { CHINA_REGION_OPTIONS } from '../lib/china-region';
|
||||
import { districtCodeLabel } from '../lib/china-region';
|
||||
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect';
|
||||
import PartnerSubAccountList, { type PartnerSubAccountRow } from '../components/PartnerSubAccountList';
|
||||
|
||||
type SubRow = PartnerSubAccountRow;
|
||||
@@ -108,6 +108,15 @@ function commissionSumError(orderPercent: number, redeemPercent: number, maxRate
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatApiError(err: unknown): string | null {
|
||||
if (err && typeof err === 'object' && 'errorFields' in err) return null;
|
||||
if (!(err instanceof Error)) return '操作失败';
|
||||
return err.message.replace(/\b(\d{6})\b/g, (code) => {
|
||||
const label = districtCodeLabel(code);
|
||||
return label !== code ? `${label}(${code})` : code;
|
||||
});
|
||||
}
|
||||
|
||||
export default function CityPartnersPage() {
|
||||
const [filterForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
@@ -138,6 +147,7 @@ export default function CityPartnersPage() {
|
||||
const [editScopeType, setEditScopeType] = useState<CityPartnerScopeType>(CityPartnerScopeType.CITY_WIDE);
|
||||
const [maxCommissionRate, setMaxCommissionRate] = useState(0.05);
|
||||
const [createMaxRate, setCreateMaxRate] = useState(0.05);
|
||||
const [createCityCode, setCreateCityCode] = useState<string | undefined>();
|
||||
|
||||
const loadCities = useCallback(async () => {
|
||||
const res = await request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
|
||||
@@ -148,6 +158,10 @@ export default function CityPartnersPage() {
|
||||
void loadCities();
|
||||
}, [loadCities]);
|
||||
|
||||
const editCityCode = detail?.cityId
|
||||
? cities.find((c) => c.id === detail.cityId)?.code
|
||||
: undefined;
|
||||
|
||||
async function openPartner(id: string) {
|
||||
const d = await request<PartnerDetail>(`/admin/partners/${id}`);
|
||||
setDetail(d);
|
||||
@@ -174,28 +188,33 @@ export default function CityPartnersPage() {
|
||||
|
||||
async function savePartner() {
|
||||
if (!detail) return;
|
||||
const v = await editForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 0),
|
||||
maxCommissionRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
try {
|
||||
const v = await editForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 0),
|
||||
maxCommissionRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
}
|
||||
await request(`/admin/partners/${detail.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: editScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已保存');
|
||||
setDrawerOpen(false);
|
||||
void reload();
|
||||
} catch (e) {
|
||||
const msg = formatApiError(e);
|
||||
if (msg) message.error(msg);
|
||||
}
|
||||
await request(`/admin/partners/${detail.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: editScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已保存');
|
||||
setDrawerOpen(false);
|
||||
void reload();
|
||||
}
|
||||
|
||||
async function refreshPartnerContext(parentId: string) {
|
||||
@@ -232,6 +251,9 @@ export default function CityPartnersPage() {
|
||||
}
|
||||
|
||||
async function onCreateCityChange(cityId: string) {
|
||||
createForm.setFieldsValue({ districtCodes: undefined });
|
||||
const matched = cities.find((c) => c.id === cityId);
|
||||
setCreateCityCode(matched?.code);
|
||||
const cityRes = await request<{ maxPartnerCommissionRate?: number }>(`/admin/cities/${cityId}`);
|
||||
setCreateMaxRate(Number(cityRes.maxPartnerCommissionRate ?? 0.05));
|
||||
}
|
||||
@@ -302,6 +324,7 @@ export default function CityPartnersPage() {
|
||||
});
|
||||
setCreateScopeType(CityPartnerScopeType.CITY_WIDE);
|
||||
setCreateMaxRate(0.05);
|
||||
setCreateCityCode(undefined);
|
||||
setCreateOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -450,8 +473,12 @@ export default function CityPartnersPage() {
|
||||
<Select options={SCOPE_OPTIONS} onChange={(v) => setEditScopeType(v)} />
|
||||
</Form.Item>
|
||||
{editScopeType === CityPartnerScopeType.DISTRICT && (
|
||||
<Form.Item name="districtCodes" label="区县">
|
||||
<Cascader options={CHINA_REGION_OPTIONS} multiple changeOnSelect />
|
||||
<Form.Item
|
||||
name="districtCodes"
|
||||
label="区县"
|
||||
extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合"
|
||||
>
|
||||
<CityDistrictMultiSelect cityCode={editCityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
@@ -505,29 +532,34 @@ export default function CityPartnersPage() {
|
||||
width={560}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={async () => {
|
||||
const v = await createForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 3),
|
||||
createMaxRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
try {
|
||||
const v = await createForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 3),
|
||||
createMaxRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
}
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes:
|
||||
createScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
void reload();
|
||||
} catch (e) {
|
||||
const msg = formatApiError(e);
|
||||
if (msg) message.error(msg);
|
||||
}
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes:
|
||||
createScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
void reload();
|
||||
}}
|
||||
>
|
||||
<Form form={createForm} layout="vertical">
|
||||
@@ -555,8 +587,13 @@ export default function CityPartnersPage() {
|
||||
<Select options={SCOPE_OPTIONS} onChange={(v) => setCreateScopeType(v)} />
|
||||
</Form.Item>
|
||||
{createScopeType === CityPartnerScopeType.DISTRICT && (
|
||||
<Form.Item name="districtCodes" label="区县" rules={[{ required: true }]}>
|
||||
<Cascader options={CHINA_REGION_OPTIONS} multiple changeOnSelect />
|
||||
<Form.Item
|
||||
name="districtCodes"
|
||||
label="区县"
|
||||
rules={[{ required: true, message: '请选择至少一个区县' }]}
|
||||
extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合"
|
||||
>
|
||||
<CityDistrictMultiSelect cityCode={createCityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Cascader,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
@@ -24,9 +23,10 @@ import {
|
||||
type PartnerPermissionKey,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { CHINA_REGION_OPTIONS } from '../lib/china-region';
|
||||
import { districtCodeLabel } from '../lib/china-region';
|
||||
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect';
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
@@ -102,6 +102,18 @@ export default function PartnersPage() {
|
||||
const [editScopeType, setEditScopeType] = useState<CityPartnerScopeType>(CityPartnerScopeType.CITY_WIDE);
|
||||
const [createCityId, setCreateCityId] = useState<string | undefined>();
|
||||
|
||||
const createCityCode = createCityId ? cities.find((c) => c.id === createCityId)?.code : undefined;
|
||||
const editCityCode = detail?.cityId ? cities.find((c) => c.id === detail.cityId)?.code : undefined;
|
||||
|
||||
function formatApiError(err: unknown): string | null {
|
||||
if (err && typeof err === 'object' && 'errorFields' in err) return null;
|
||||
if (!(err instanceof Error)) return '操作失败';
|
||||
return err.message.replace(/\b(\d{6})\b/g, (code) => {
|
||||
const label = districtCodeLabel(code);
|
||||
return label !== code ? `${label}(${code})` : code;
|
||||
});
|
||||
}
|
||||
|
||||
const loadCities = useCallback(async () => {
|
||||
const res = await request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
|
||||
setCities(res.items);
|
||||
@@ -142,17 +154,22 @@ export default function PartnersPage() {
|
||||
|
||||
async function savePartner() {
|
||||
if (!detail) return;
|
||||
const v = await editForm.validateFields();
|
||||
const body = {
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: editScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
};
|
||||
await request(`/admin/partners/${detail.id}`, { method: 'PUT', body: JSON.stringify(body) });
|
||||
message.success('已保存');
|
||||
setDrawerOpen(false);
|
||||
void reload();
|
||||
try {
|
||||
const v = await editForm.validateFields();
|
||||
const body = {
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: editScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
};
|
||||
await request(`/admin/partners/${detail.id}`, { method: 'PUT', body: JSON.stringify(body) });
|
||||
message.success('已保存');
|
||||
setDrawerOpen(false);
|
||||
void reload();
|
||||
} catch (e) {
|
||||
const msg = formatApiError(e);
|
||||
if (msg) message.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Row> = [
|
||||
@@ -231,8 +248,12 @@ export default function PartnersPage() {
|
||||
<Select options={SCOPE_OPTIONS} onChange={(v) => setEditScopeType(v)} />
|
||||
</Form.Item>
|
||||
{editScopeType === CityPartnerScopeType.DISTRICT && (
|
||||
<Form.Item name="districtCodes" label="区县">
|
||||
<Cascader options={CHINA_REGION_OPTIONS} multiple changeOnSelect />
|
||||
<Form.Item
|
||||
name="districtCodes"
|
||||
label="区县"
|
||||
extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合"
|
||||
>
|
||||
<CityDistrictMultiSelect cityCode={editCityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
@@ -286,27 +307,36 @@ export default function PartnersPage() {
|
||||
width={560}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={async () => {
|
||||
const v = await createForm.validateFields();
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: createScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
createForm.resetFields();
|
||||
void reload();
|
||||
try {
|
||||
const v = await createForm.validateFields();
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
orderCommissionRate: Number(v.orderCommissionRate ?? 0) / 100,
|
||||
redeemCommissionRate: Number(v.redeemCommissionRate ?? 3) / 100,
|
||||
districtCodes: createScopeType === CityPartnerScopeType.DISTRICT ? flattenDistrictCodes(v.districtCodes) : undefined,
|
||||
}),
|
||||
});
|
||||
message.success('已创建');
|
||||
setCreateOpen(false);
|
||||
createForm.resetFields();
|
||||
void reload();
|
||||
} catch (e) {
|
||||
const msg = formatApiError(e);
|
||||
if (msg) message.error(msg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form form={createForm} layout="vertical" initialValues={{ orderCommissionRate: 0, redeemCommissionRate: 3, scopeType: CityPartnerScopeType.CITY_WIDE }}>
|
||||
<Form.Item name="cityId" label="开城城市" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={cities.map((c) => ({ value: c.id, label: `${c.name} (${c.code})` }))}
|
||||
onChange={(id) => { setCreateCityId(id); void loadWarehouses(id); }}
|
||||
onChange={(id) => {
|
||||
setCreateCityId(id);
|
||||
createForm.setFieldsValue({ districtCodes: undefined });
|
||||
void loadWarehouses(id);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="companyName" label="公司名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
@@ -317,8 +347,13 @@ export default function PartnersPage() {
|
||||
<Select options={SCOPE_OPTIONS} onChange={(v) => setCreateScopeType(v)} />
|
||||
</Form.Item>
|
||||
{createScopeType === CityPartnerScopeType.DISTRICT && (
|
||||
<Form.Item name="districtCodes" label="区县" rules={[{ required: true }]}>
|
||||
<Cascader options={CHINA_REGION_OPTIONS} multiple changeOnSelect />
|
||||
<Form.Item
|
||||
name="districtCodes"
|
||||
label="区县"
|
||||
rules={[{ required: true, message: '请选择至少一个区县' }]}
|
||||
extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合"
|
||||
>
|
||||
<CityDistrictMultiSelect cityCode={createCityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
|
||||
Reference in New Issue
Block a user