309b180087
CI / verify (pull_request) Has been cancelled
City partner storeCount jumps to /stores?partnerId=; normalize empty/null text so usage rule is not saved as literal "null. Co-authored-by: Cursor <cursoragent@cursor.com>
707 lines
25 KiB
TypeScript
707 lines
25 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Checkbox,
|
||
Descriptions,
|
||
Drawer,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Popconfirm,
|
||
Select,
|
||
Space,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
} from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import {
|
||
CITY_PARTNER_SCOPE_LABELS,
|
||
CITY_PARTNER_STATUS_LABELS,
|
||
CityPartnerScopeType,
|
||
CityPartnerStatus,
|
||
PARTNER_PERMISSION_KEYS,
|
||
PARTNER_PERMISSION_LABELS,
|
||
PARTNER_STAFF_ROLE_LABELS,
|
||
type PartnerPermissionKey,
|
||
} from '@dukang/shared-types';
|
||
import { request, type Paginated } from '../lib/api';
|
||
import { districtCodeLabel, formatDistrictLabels } 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;
|
||
|
||
type Row = {
|
||
id: string;
|
||
companyName: string;
|
||
phone: string;
|
||
name: string;
|
||
contactPhone?: string | null;
|
||
cityId?: string | null;
|
||
cityName?: string | null;
|
||
scopeType?: string;
|
||
districtCodes?: string[] | null;
|
||
orderCommissionRate?: number;
|
||
redeemCommissionRate?: number;
|
||
bindingStatus?: string;
|
||
managedWarehouseId?: string | null;
|
||
managedWarehouseName?: string | null;
|
||
maxPartnerCommissionRate?: number | null;
|
||
storeCount: number;
|
||
accountCount: number;
|
||
subAccounts?: SubRow[];
|
||
createdAt: string;
|
||
};
|
||
|
||
type PartnerDetail = Row & {
|
||
address?: string;
|
||
districtCodes?: string[] | null;
|
||
bankAccountName?: string | null;
|
||
bankAccountNo?: string | null;
|
||
bankBranch?: string | null;
|
||
/** 详情接口仍返回 children */
|
||
children?: SubRow[];
|
||
};
|
||
|
||
function toTableRows(items: Row[]): Row[] {
|
||
return items.map((row) => {
|
||
const { children, subAccounts, ...rest } = row as Row & { children?: SubRow[] };
|
||
return { ...rest, subAccounts: subAccounts ?? children ?? [] };
|
||
});
|
||
}
|
||
|
||
type CityOption = { id: string; name: string; code: string };
|
||
|
||
const SCOPE_OPTIONS = Object.entries(CITY_PARTNER_SCOPE_LABELS).map(([value, label]) => ({ value, label }));
|
||
const BINDING_OPTIONS = Object.entries(CITY_PARTNER_STATUS_LABELS).map(([value, label]) => ({ value, label }));
|
||
const PERM_OPTIONS = PARTNER_PERMISSION_KEYS.map((k: PartnerPermissionKey) => ({
|
||
value: k,
|
||
label: PARTNER_PERMISSION_LABELS[k],
|
||
}));
|
||
const STAFF_ROLE_OPTIONS = Object.entries(PARTNER_STAFF_ROLE_LABELS)
|
||
.filter(([value]) => value !== 'PARTNER')
|
||
.map(([value, label]) => ({ value, label }));
|
||
|
||
function flattenDistrictCodes(values: string[] | string[][] | undefined): string[] {
|
||
if (!values?.length) return [];
|
||
if (Array.isArray(values[0])) {
|
||
return (values as string[][]).map((path) => path[path.length - 1]).filter(Boolean);
|
||
}
|
||
return values as string[];
|
||
}
|
||
|
||
function commissionSumError(orderPercent: number, redeemPercent: number, maxRate: number): string | null {
|
||
// API 可能把 Prisma Decimal 序列化为字符串;`"0.05" + 1e-9` 会变成字符串拼接导致误判超限
|
||
const max = Number(maxRate);
|
||
const sum = Number(orderPercent) / 100 + Number(redeemPercent) / 100;
|
||
if (!Number.isFinite(max) || !Number.isFinite(sum)) return '佣金比例无效';
|
||
if (sum > max + 1e-9) {
|
||
return `订单佣金与核销佣金合计不得超过 ${(max * 100).toFixed(2)}%(当前 ${(sum * 100).toFixed(2)}%)`;
|
||
}
|
||
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();
|
||
const [createForm] = Form.useForm();
|
||
const [subForm] = Form.useForm();
|
||
const [subEditForm] = Form.useForm();
|
||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||
'/admin/partners',
|
||
() => {
|
||
const qs = new URLSearchParams();
|
||
if (filters.companyName) qs.set('companyName', filters.companyName);
|
||
if (filters.phone) qs.set('phone', filters.phone);
|
||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||
return qs;
|
||
},
|
||
[filters],
|
||
);
|
||
const [detail, setDetail] = useState<PartnerDetail | null>(null);
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
const [subOpen, setSubOpen] = useState(false);
|
||
const [subEditOpen, setSubEditOpen] = useState(false);
|
||
const [subEditId, setSubEditId] = useState<string | null>(null);
|
||
const [subEditParentId, setSubEditParentId] = useState<string | null>(null);
|
||
const [cities, setCities] = useState<CityOption[]>([]);
|
||
const [createScopeType, setCreateScopeType] = useState<CityPartnerScopeType>(CityPartnerScopeType.CITY_WIDE);
|
||
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}`);
|
||
setCities(res.items);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
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);
|
||
setEditScopeType((d.scopeType as CityPartnerScopeType) || CityPartnerScopeType.CITY_WIDE);
|
||
setMaxCommissionRate(Number(d.maxPartnerCommissionRate ?? 0.05));
|
||
editForm.setFieldsValue({
|
||
name: d.name,
|
||
phone: d.phone,
|
||
companyName: d.companyName,
|
||
contactPhone: d.contactPhone ?? d.phone,
|
||
address: d.address ?? '',
|
||
scopeType: d.scopeType,
|
||
districtCodes: d.districtCodes ?? [],
|
||
orderCommissionRate: (d.orderCommissionRate ?? 0) * 100,
|
||
redeemCommissionRate: (d.redeemCommissionRate ?? 0.03) * 100,
|
||
bindingStatus: d.bindingStatus ?? CityPartnerStatus.ACTIVE,
|
||
bankAccountName: d.bankAccountName ?? '',
|
||
bankAccountNo: d.bankAccountNo ?? '',
|
||
bankBranch: d.bankBranch ?? '',
|
||
});
|
||
setDrawerOpen(true);
|
||
return d;
|
||
}
|
||
|
||
async function savePartner() {
|
||
if (!detail) 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);
|
||
}
|
||
}
|
||
|
||
async function refreshPartnerContext(parentId: string) {
|
||
if (detail?.id === parentId) {
|
||
await openPartner(parentId);
|
||
}
|
||
void reload();
|
||
}
|
||
|
||
async function deleteSubAccount(subId: string, parentId: string) {
|
||
await request(`/admin/partner-accounts/${subId}`, { method: 'DELETE' });
|
||
message.success('子账号已删除');
|
||
await refreshPartnerContext(parentId);
|
||
}
|
||
|
||
function openSubEdit(row: SubRow, parentId: string) {
|
||
setSubEditId(row.id);
|
||
setSubEditParentId(parentId);
|
||
subEditForm.setFieldsValue({
|
||
name: row.name,
|
||
phone: row.phone,
|
||
staffRole: row.staffRole ?? 'INTERNAL',
|
||
permissions: row.permissions ?? [],
|
||
status: row.status,
|
||
});
|
||
setSubEditOpen(true);
|
||
}
|
||
|
||
async function openAddSubAccount(parentId: string) {
|
||
await openPartner(parentId);
|
||
subForm.resetFields();
|
||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||
setSubOpen(true);
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
const columns: ColumnsType<Row> = [
|
||
{ title: '城市', dataIndex: 'cityName', width: 90 },
|
||
{
|
||
title: '区县',
|
||
dataIndex: 'districtCodes',
|
||
width: 160,
|
||
ellipsis: true,
|
||
render: (codes: string[] | null | undefined, row) =>
|
||
row.scopeType === CityPartnerScopeType.CITY_WIDE ? '全城' : formatDistrictLabels(codes),
|
||
},
|
||
{ title: '公司名', dataIndex: 'companyName', ellipsis: true, width: 140 },
|
||
{ title: '主账号姓名', dataIndex: 'name', width: 100, ellipsis: true },
|
||
{ title: '登录手机', dataIndex: 'phone', width: 120 },
|
||
{
|
||
title: '管辖',
|
||
dataIndex: 'scopeType',
|
||
width: 90,
|
||
render: (v) => (v ? CITY_PARTNER_SCOPE_LABELS[v as keyof typeof CITY_PARTNER_SCOPE_LABELS] || v : '—'),
|
||
},
|
||
{
|
||
title: '佣金',
|
||
width: 110,
|
||
render: (_, row) =>
|
||
`${Math.round((row.orderCommissionRate ?? 0) * 100)}% / ${Math.round((row.redeemCommissionRate ?? 0.03) * 100)}%`,
|
||
},
|
||
{
|
||
title: '绑定状态',
|
||
dataIndex: 'bindingStatus',
|
||
width: 80,
|
||
render: (v) => (
|
||
<Tag color={v === CityPartnerStatus.ACTIVE ? 'green' : 'default'}>
|
||
{CITY_PARTNER_STATUS_LABELS[v as CityPartnerStatus] || v || '—'}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '管仓仓库',
|
||
dataIndex: 'managedWarehouseName',
|
||
width: 110,
|
||
ellipsis: true,
|
||
render: (v) => v || '—',
|
||
},
|
||
{
|
||
title: '门店',
|
||
dataIndex: 'storeCount',
|
||
width: 60,
|
||
render: (n: number, row) => (
|
||
<Link to={`/stores?partnerId=${row.id}`} title={`查看「${row.companyName || row.phone}」门店`}>
|
||
{n ?? 0}
|
||
</Link>
|
||
),
|
||
},
|
||
{ title: '子账号', dataIndex: 'accountCount', width: 70, render: (n) => Math.max(0, n - 1) },
|
||
{ title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime },
|
||
{
|
||
title: '操作',
|
||
width: 80,
|
||
fixed: 'right',
|
||
render: (_, row) => (
|
||
<Button type="link" size="small" onClick={() => void openPartner(row.id)}>
|
||
管理
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||
城市合伙人
|
||
</Typography.Title>
|
||
<Button
|
||
type="primary"
|
||
onClick={() => {
|
||
createForm.resetFields();
|
||
createForm.setFieldsValue({
|
||
orderCommissionRate: 0,
|
||
redeemCommissionRate: 3,
|
||
scopeType: CityPartnerScopeType.CITY_WIDE,
|
||
});
|
||
setCreateScopeType(CityPartnerScopeType.CITY_WIDE);
|
||
setCreateMaxRate(0.05);
|
||
setCreateCityCode(undefined);
|
||
setCreateOpen(true);
|
||
}}
|
||
>
|
||
新建合伙人
|
||
</Button>
|
||
</Space>
|
||
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
message={
|
||
<>
|
||
账号结构仅两级:主账号 → 子账号(子账号不可再添加下级)。管仓仓库请在{' '}
|
||
<Link to="/city-warehouses">仓库管理</Link> 中分配。
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<Form
|
||
form={filterForm}
|
||
layout="inline"
|
||
style={{ marginBottom: 16 }}
|
||
onFinish={(v) => {
|
||
setFilters(v);
|
||
setPage(1);
|
||
}}
|
||
>
|
||
<Form.Item name="cityId" label="城市">
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
style={{ width: 160 }}
|
||
placeholder="全部城市"
|
||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="companyName" label="公司">
|
||
<Input allowClear placeholder="公司名" />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="手机">
|
||
<Input allowClear placeholder="登录手机" />
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Space>
|
||
<Button type="primary" htmlType="submit">
|
||
查询
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
filterForm.resetFields();
|
||
setFilters({});
|
||
setPage(1);
|
||
}}
|
||
>
|
||
重置
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
|
||
<Table
|
||
rowKey="id"
|
||
className="admin-table-nowrap"
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={toTableRows(data?.items ?? [])}
|
||
scroll={{ x: 1200 }}
|
||
childrenColumnName="__noTreeChildren__"
|
||
expandable={{
|
||
expandedRowRender: (record) => (
|
||
<PartnerSubAccountList
|
||
subs={record.subAccounts ?? []}
|
||
onAdd={() => void openAddSubAccount(record.id)}
|
||
onEdit={(sub) => openSubEdit(sub, record.id)}
|
||
onDelete={(subId) => void deleteSubAccount(subId, record.id)}
|
||
/>
|
||
),
|
||
rowExpandable: () => true,
|
||
columnWidth: 40,
|
||
}}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total: data?.total ?? 0,
|
||
showSizeChanger: true,
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
onChange: (p, ps) => {
|
||
setPage(p);
|
||
setPageSize(ps);
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Drawer
|
||
title="城市合伙人"
|
||
width={640}
|
||
open={drawerOpen}
|
||
onClose={() => setDrawerOpen(false)}
|
||
extra={
|
||
<Button type="primary" onClick={() => void savePartner()}>
|
||
保存
|
||
</Button>
|
||
}
|
||
>
|
||
{detail && (
|
||
<>
|
||
<Descriptions column={2} size="small" bordered style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="城市">{detail.cityName ?? '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="门店数">
|
||
<Link to={`/stores?partnerId=${detail.id}`} title="查看该城市合伙人门店">
|
||
{detail.storeCount ?? 0}
|
||
</Link>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="管仓仓库" span={2}>
|
||
{detail.managedWarehouseName ?? (
|
||
<Typography.Text type="secondary">
|
||
未分配(请前往 <Link to="/city-warehouses">仓库管理</Link> 设置)
|
||
</Typography.Text>
|
||
)}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
<Tabs
|
||
items={[
|
||
{
|
||
key: 'info',
|
||
label: '主账号',
|
||
children: (
|
||
<Form form={editForm} layout="vertical">
|
||
<Form.Item name="companyName" label="公司名">
|
||
<Input placeholder="选填" />
|
||
</Form.Item>
|
||
<Form.Item name="name" label="主账号姓名" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="登录手机号" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="contactPhone" label="业务联系电话">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="address" label="地址">
|
||
<Input placeholder="选填" />
|
||
</Form.Item>
|
||
<Form.Item name="bindingStatus" label="绑定状态" rules={[{ required: true }]}>
|
||
<Select options={BINDING_OPTIONS} />
|
||
</Form.Item>
|
||
<Form.Item name="scopeType" label="管辖类型" rules={[{ required: true }]}>
|
||
<Select options={SCOPE_OPTIONS} onChange={(v) => setEditScopeType(v)} />
|
||
</Form.Item>
|
||
{editScopeType === CityPartnerScopeType.DISTRICT && (
|
||
<Form.Item
|
||
name="districtCodes"
|
||
label="区县"
|
||
extra="仅作标识,可多选当前城市下的区县(不做互斥)"
|
||
>
|
||
<CityDistrictMultiSelect cityCode={editCityCode} />
|
||
</Form.Item>
|
||
)}
|
||
<Space style={{ width: '100%' }} size="large">
|
||
<Form.Item name="orderCommissionRate" label="订单佣金 %">
|
||
<InputNumber min={0} max={100} precision={2} />
|
||
</Form.Item>
|
||
<Form.Item name="redeemCommissionRate" label="核销佣金 %">
|
||
<InputNumber min={0} max={100} precision={2} />
|
||
</Form.Item>
|
||
</Space>
|
||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||
订单 + 核销合计不得超过 {(maxCommissionRate * 100).toFixed(2)}%
|
||
</Typography.Text>
|
||
<Form.Item name="bankAccountName" label="户名">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="bankAccountNo" label="账号">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="bankBranch" label="开户行">
|
||
<Input />
|
||
</Form.Item>
|
||
</Form>
|
||
),
|
||
},
|
||
{
|
||
key: 'staff',
|
||
label: `子账号 (${Math.max(0, (detail.accountCount ?? 1) - 1)})`,
|
||
children: (
|
||
<PartnerSubAccountList
|
||
subs={detail.children ?? []}
|
||
onAdd={() => {
|
||
subForm.resetFields();
|
||
subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] });
|
||
setSubOpen(true);
|
||
}}
|
||
onEdit={(sub) => openSubEdit(sub, detail.id)}
|
||
onDelete={(subId) => void deleteSubAccount(subId, detail.id)}
|
||
/>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
|
||
<Modal
|
||
title="新建城市合伙人"
|
||
open={createOpen}
|
||
width={560}
|
||
onCancel={() => setCreateOpen(false)}
|
||
onOk={async () => {
|
||
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);
|
||
}
|
||
}}
|
||
>
|
||
<Form form={createForm} layout="vertical">
|
||
<Form.Item name="cityId" label="开城城市" rules={[{ required: true }]}>
|
||
<Select
|
||
showSearch
|
||
optionFilterProp="label"
|
||
options={cities.map((c) => ({ value: c.id, label: `${c.name} (${c.code})` }))}
|
||
onChange={(id) => void onCreateCityChange(id)}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="companyName" label="公司名">
|
||
<Input placeholder="选填" />
|
||
</Form.Item>
|
||
<Form.Item name="name" label="主账号姓名" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="登录手机号" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="address" label="地址">
|
||
<Input placeholder="选填" />
|
||
</Form.Item>
|
||
<Form.Item name="scopeType" label="管辖类型" rules={[{ required: true }]}>
|
||
<Select options={SCOPE_OPTIONS} onChange={(v) => setCreateScopeType(v)} />
|
||
</Form.Item>
|
||
{createScopeType === CityPartnerScopeType.DISTRICT && (
|
||
<Form.Item
|
||
name="districtCodes"
|
||
label="区县"
|
||
extra="选填;仅作标识,可多选当前城市下的区县(不做互斥)"
|
||
>
|
||
<CityDistrictMultiSelect cityCode={createCityCode} />
|
||
</Form.Item>
|
||
)}
|
||
<Space style={{ width: '100%' }} size="large">
|
||
<Form.Item name="orderCommissionRate" label="订单佣金 %">
|
||
<InputNumber min={0} max={100} precision={2} />
|
||
</Form.Item>
|
||
<Form.Item name="redeemCommissionRate" label="核销佣金 %">
|
||
<InputNumber min={0} max={100} precision={2} />
|
||
</Form.Item>
|
||
</Space>
|
||
<Typography.Text type="secondary" style={{ display: 'block' }}>
|
||
订单 + 核销合计不得超过 {(createMaxRate * 100).toFixed(2)}%(由所选城市配置决定)
|
||
</Typography.Text>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={detail ? `添加子账号 · ${detail.companyName}` : '添加子账号'}
|
||
open={subOpen}
|
||
onCancel={() => setSubOpen(false)}
|
||
onOk={async () => {
|
||
if (!detail) return;
|
||
const v = await subForm.validateFields();
|
||
await request('/admin/partner-accounts', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ ...v, parentAccountId: detail.id }),
|
||
});
|
||
message.success('已创建');
|
||
setSubOpen(false);
|
||
await refreshPartnerContext(detail.id);
|
||
}}
|
||
>
|
||
<Form form={subForm} layout="vertical">
|
||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="手机号" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="staffRole" label="角色" initialValue="INTERNAL">
|
||
<Select options={STAFF_ROLE_OPTIONS} />
|
||
</Form.Item>
|
||
<Form.Item name="permissions" label="权限">
|
||
<Checkbox.Group options={PERM_OPTIONS} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title="编辑子账号"
|
||
open={subEditOpen}
|
||
onCancel={() => setSubEditOpen(false)}
|
||
onOk={async () => {
|
||
if (!subEditId) return;
|
||
const parentId = detail?.id ?? subEditParentId;
|
||
if (!parentId) return;
|
||
const v = await subEditForm.validateFields();
|
||
await request(`/admin/partner-accounts/${subEditId}`, { method: 'PUT', body: JSON.stringify(v) });
|
||
message.success('已更新');
|
||
setSubEditOpen(false);
|
||
await refreshPartnerContext(parentId);
|
||
}}
|
||
>
|
||
<Form form={subEditForm} layout="vertical">
|
||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="phone" label="手机号" rules={[{ required: true }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item name="staffRole" label="角色">
|
||
<Select options={STAFF_ROLE_OPTIONS} />
|
||
</Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select
|
||
options={[
|
||
{ value: 'ACTIVE', label: '启用' },
|
||
{ value: 'DISABLED', label: '停用' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="permissions" label="权限">
|
||
<Checkbox.Group options={PERM_OPTIONS} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|