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:
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { getDistrictOptionsByCityCode } from '../lib/china-region';
|
||||
|
||||
type Props = {
|
||||
cityCode?: string | null;
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/** 已选开城城市后,仅多选该市下区县(不再选省/市) */
|
||||
export default function CityDistrictMultiSelect({
|
||||
cityCode,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
}: Props) {
|
||||
const options = useMemo(() => getDistrictOptionsByCityCode(cityCode), [cityCode]);
|
||||
const ready = Boolean(cityCode) && options.length > 0;
|
||||
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled || !ready}
|
||||
placeholder={
|
||||
placeholder ??
|
||||
(cityCode ? (ready ? '请选择区县(可多选)' : '该城市暂无区县数据') : '请先选择开城城市')
|
||||
}
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
maxTagCount="responsive"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Cascader,
|
||||
Checkbox,
|
||||
Drawer,
|
||||
Form,
|
||||
@@ -27,8 +26,9 @@ 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 { fmtTime } from '../lib/constants';
|
||||
import CityDistrictMultiSelect from './CityDistrictMultiSelect';
|
||||
import PartnerSubAccountList from './PartnerSubAccountList';
|
||||
|
||||
type PartnerRow = {
|
||||
@@ -65,6 +65,7 @@ type PartnerDetail = PartnerRow & {
|
||||
|
||||
type Props = {
|
||||
cityId: string;
|
||||
cityCode?: string;
|
||||
maxPartnerCommissionRate?: number;
|
||||
onChanged?: () => void;
|
||||
};
|
||||
@@ -102,7 +103,21 @@ function commissionSumError(
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0.05, onChanged }: Props) {
|
||||
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 CityPartnersPanel({
|
||||
cityId,
|
||||
cityCode,
|
||||
maxPartnerCommissionRate = 0.05,
|
||||
onChanged,
|
||||
}: Props) {
|
||||
const [editForm] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
const [subForm] = Form.useForm();
|
||||
@@ -156,29 +171,34 @@ export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0
|
||||
|
||||
async function savePartner() {
|
||||
if (!detail) return;
|
||||
const v = await editForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 0),
|
||||
maxPartnerCommissionRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
try {
|
||||
const v = await editForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 0),
|
||||
maxPartnerCommissionRate,
|
||||
);
|
||||
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 loadPartners();
|
||||
onChanged?.();
|
||||
} 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 loadPartners();
|
||||
onChanged?.();
|
||||
}
|
||||
|
||||
async function deleteSubAccount(subId: string) {
|
||||
@@ -264,8 +284,12 @@ export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0
|
||||
<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={cityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
@@ -319,30 +343,35 @@ export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0
|
||||
</Drawer>
|
||||
|
||||
<Modal title="新建城市合伙人" open={createOpen} width={560} onCancel={() => setCreateOpen(false)} onOk={async () => {
|
||||
const v = await createForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 3),
|
||||
maxPartnerCommissionRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
try {
|
||||
const v = await createForm.validateFields();
|
||||
const err = commissionSumError(
|
||||
Number(v.orderCommissionRate ?? 0),
|
||||
Number(v.redeemCommissionRate ?? 3),
|
||||
maxPartnerCommissionRate,
|
||||
);
|
||||
if (err) {
|
||||
message.error(err);
|
||||
return;
|
||||
}
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
cityId,
|
||||
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 loadPartners();
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
const msg = formatApiError(e);
|
||||
if (msg) message.error(msg);
|
||||
}
|
||||
await request('/admin/partners', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...v,
|
||||
cityId,
|
||||
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 loadPartners();
|
||||
onChanged?.();
|
||||
}}>
|
||||
<Form form={createForm} layout="vertical">
|
||||
<Form.Item name="companyName" label="公司名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
@@ -353,8 +382,13 @@ export default function CityPartnersPanel({ cityId, maxPartnerCommissionRate = 0
|
||||
<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={cityCode} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
|
||||
@@ -79,6 +79,33 @@ export function districtCodeToCityCode(districtCode: string): string {
|
||||
return `${districtCode.slice(0, 4)}00`;
|
||||
}
|
||||
|
||||
export type DistrictOption = { value: string; label: string };
|
||||
|
||||
/** 按开城城市 code(4 位或 6 位)取该市下区县列表 */
|
||||
export function getDistrictOptionsByCityCode(cityCode?: string | null): DistrictOption[] {
|
||||
if (!cityCode?.trim()) return [];
|
||||
const normalized = normalizeCityAdcode(cityCode.trim());
|
||||
const prefix4 = normalized.slice(0, 4);
|
||||
|
||||
for (const province of regionData) {
|
||||
for (const city of province.children ?? []) {
|
||||
const cityValue = String(city.value);
|
||||
const cityNorm = normalizeCityAdcode(cityValue);
|
||||
if (cityNorm === normalized || cityValue === prefix4 || cityValue === cityCode.trim()) {
|
||||
return (city.children ?? []).map((d) => ({
|
||||
value: String(d.value),
|
||||
label: String(d.label),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function districtCodeLabel(code: string): string {
|
||||
return codeToText[code] || code;
|
||||
}
|
||||
|
||||
export function parseRegionCodes(codes?: string[]): ParsedChinaRegion | null {
|
||||
if (!codes || codes.length < 3) return null;
|
||||
const [provinceCode, cityCode, districtCode] = codes;
|
||||
|
||||
@@ -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