118d57d710
CI / verify (pull_request) Has been cancelled
HQ can remove store sub-accounts from primary account detail; cities table store count jumps to /stores?cityId=. Co-authored-by: Cursor <cursoragent@cursor.com>
677 lines
28 KiB
TypeScript
677 lines
28 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import {
|
||
Button,
|
||
Descriptions,
|
||
Drawer,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Select,
|
||
Space,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
} from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import { WAREHOUSE_MANAGER_LABELS, WarehouseManagerType } from '@dukang/shared-types';
|
||
import { request, type HqProfile, type Paginated } from '../lib/api';
|
||
import { parseProvinceCityCodes, type ParsedProvinceCity } from '../lib/china-region';
|
||
import { ADMIN_OPTIONS_PAGE_SIZE, CITY_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||
import { useAdminList } from '../lib/useAdminList';
|
||
import CityPartnersPanel from '../components/CityPartnersPanel';
|
||
import ChinaProvinceCityCascader from '../components/ChinaProvinceCityCascader';
|
||
|
||
type WarehouseRow = {
|
||
id: string;
|
||
name: string;
|
||
address: string;
|
||
contactName: string;
|
||
contactPhone: string;
|
||
managerType: WarehouseManagerType;
|
||
partnerAccountId: string | null;
|
||
partnerCompanyName?: string | null;
|
||
status: string;
|
||
};
|
||
|
||
type Row = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
province: string;
|
||
status: string;
|
||
storeCount: number;
|
||
orderCount: number;
|
||
partnerBindingCount?: number;
|
||
createdAt: string;
|
||
};
|
||
|
||
type PartnerOption = { id: string; companyName: string; cityId?: string | null };
|
||
|
||
type CityDeletePreview = {
|
||
city: { id: string; code: string; name: string; province: string; status: string };
|
||
canDelete: boolean;
|
||
blockers: string[];
|
||
warnings: string[];
|
||
summary: {
|
||
primaryPartnerCount: number;
|
||
staffCount: number;
|
||
storeCount: number;
|
||
warehouseCount: number;
|
||
orderCount: number;
|
||
redeemCount: number;
|
||
partnerBillCount: number;
|
||
};
|
||
partners: Array<{
|
||
id: string;
|
||
phone: string;
|
||
name: string;
|
||
companyName?: string | null;
|
||
status: string;
|
||
staff: Array<{ id: string; phone: string; name: string; staffRole?: string | null }>;
|
||
}>;
|
||
orphanStaff: Array<{ id: string; phone: string; name: string }>;
|
||
stores: Array<{
|
||
id: string;
|
||
name: string;
|
||
phone: string;
|
||
status: string;
|
||
address: string;
|
||
partnerAccount?: { companyName?: string | null; phone?: string } | null;
|
||
}>;
|
||
warehouses: Array<{ id: string; name: string; status: string; address: string }>;
|
||
};
|
||
|
||
const MANAGER_OPTIONS = Object.entries(WAREHOUSE_MANAGER_LABELS).map(([value, label]) => ({ value, label }));
|
||
|
||
export default function CitiesPage() {
|
||
const [form] = Form.useForm();
|
||
const [editForm] = Form.useForm();
|
||
const [createForm] = Form.useForm();
|
||
const [warehouseForm] = Form.useForm();
|
||
const [warehouseEditForm] = Form.useForm();
|
||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||
'/admin/cities',
|
||
() => {
|
||
const qs = new URLSearchParams();
|
||
if (filters.name) qs.set('name', filters.name);
|
||
if (filters.code) qs.set('code', filters.code);
|
||
if (filters.status) qs.set('status', filters.status);
|
||
return qs;
|
||
},
|
||
[filters],
|
||
);
|
||
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
||
const [warehouses, setWarehouses] = useState<WarehouseRow[]>([]);
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
const [warehouseOpen, setWarehouseOpen] = useState(false);
|
||
const [warehouseEditOpen, setWarehouseEditOpen] = useState(false);
|
||
const [warehouseEditId, setWarehouseEditId] = useState<string | null>(null);
|
||
const [partners, setPartners] = useState<PartnerOption[]>([]);
|
||
const [createRegionPreview, setCreateRegionPreview] = useState<ParsedProvinceCity | null>(null);
|
||
const createRegionCodes = Form.useWatch('regionCodes', createForm);
|
||
const [warehouseManagerType, setWarehouseManagerType] = useState<WarehouseManagerType>(WarehouseManagerType.HQ);
|
||
const [editWarehouseManagerType, setEditWarehouseManagerType] = useState<WarehouseManagerType>(WarehouseManagerType.HQ);
|
||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||
const [deleteSubmitting, setDeleteSubmitting] = useState(false);
|
||
const [deleteTarget, setDeleteTarget] = useState<Row | null>(null);
|
||
const [deletePreview, setDeletePreview] = useState<CityDeletePreview | null>(null);
|
||
const [confirmName, setConfirmName] = useState('');
|
||
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||
const canDeleteCities = (profile?.permissionKeys ?? []).includes('cities_delete');
|
||
|
||
useEffect(() => {
|
||
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||
}, []);
|
||
|
||
const loadPartners = useCallback(async (cityId: string) => {
|
||
const res = await request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}&cityId=${cityId}`);
|
||
setPartners(res.items);
|
||
}, []);
|
||
|
||
const loadWarehouses = useCallback(async (cityId: string) => {
|
||
const wh = await request<WarehouseRow[]>(`/admin/cities/${cityId}/warehouses`);
|
||
setWarehouses(wh);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!createRegionCodes?.length) {
|
||
setCreateRegionPreview(null);
|
||
return;
|
||
}
|
||
const parsed = parseProvinceCityCodes(createRegionCodes as string[]);
|
||
createForm.setFieldsValue({
|
||
code: parsed?.cityCode,
|
||
name: parsed?.city,
|
||
province: parsed?.province,
|
||
});
|
||
setCreateRegionPreview(parsed);
|
||
}, [createRegionCodes, createForm]);
|
||
|
||
function openCreateModal() {
|
||
createForm.resetFields();
|
||
createForm.setFieldsValue({ status: 'PENDING' });
|
||
setCreateRegionPreview(null);
|
||
setCreateOpen(true);
|
||
}
|
||
|
||
const openDetail = async (row: Row) => {
|
||
const d = await request<Record<string, unknown>>(`/admin/cities/${row.id}`);
|
||
setDetail(d);
|
||
editForm.setFieldsValue({
|
||
...d,
|
||
maxPartnerCommissionPercent:
|
||
d.maxPartnerCommissionRate != null
|
||
? Number(d.maxPartnerCommissionRate) * 100
|
||
: 5,
|
||
});
|
||
await loadPartners(row.id);
|
||
await loadWarehouses(row.id);
|
||
setDrawerOpen(true);
|
||
};
|
||
|
||
const openDelete = async (row: Row) => {
|
||
setDeleteTarget(row);
|
||
setDeletePreview(null);
|
||
setConfirmName('');
|
||
setDeleteOpen(true);
|
||
setDeleteLoading(true);
|
||
try {
|
||
const preview = await request<CityDeletePreview>(`/admin/cities/${row.id}/delete-preview`);
|
||
setDeletePreview(preview);
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载删除预览失败');
|
||
setDeleteOpen(false);
|
||
} finally {
|
||
setDeleteLoading(false);
|
||
}
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!deleteTarget || !deletePreview) return;
|
||
if (!deletePreview.canDelete) {
|
||
message.error(deletePreview.blockers.join(';') || '当前城市不可删除');
|
||
return;
|
||
}
|
||
if (confirmName.trim() !== deletePreview.city.name) {
|
||
message.warning(`请输入城市名称「${deletePreview.city.name}」确认删除`);
|
||
return;
|
||
}
|
||
setDeleteSubmitting(true);
|
||
try {
|
||
await request(`/admin/cities/${deleteTarget.id}`, {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({ confirmName: confirmName.trim() }),
|
||
});
|
||
message.success(`已删除城市「${deletePreview.city.name}」`);
|
||
setDeleteOpen(false);
|
||
setDeleteTarget(null);
|
||
setDeletePreview(null);
|
||
setConfirmName('');
|
||
if (detail?.id === deleteTarget.id) setDrawerOpen(false);
|
||
void reload();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败');
|
||
} finally {
|
||
setDeleteSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const columns: ColumnsType<Row> = [
|
||
{ title: '编码', dataIndex: 'code', width: 90 },
|
||
{ title: '城市', dataIndex: 'name', width: 100 },
|
||
{ title: '省份', dataIndex: 'province', width: 90 },
|
||
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => <Tag>{CITY_STATUS_LABELS[s] || s}</Tag> },
|
||
{ title: '合伙人', dataIndex: 'partnerBindingCount', width: 90 },
|
||
{
|
||
title: '门店',
|
||
dataIndex: 'storeCount',
|
||
width: 70,
|
||
render: (n: number, row) => (
|
||
<Link to={`/stores?cityId=${row.id}`} title={`查看「${row.name}」门店`}>
|
||
{n ?? 0}
|
||
</Link>
|
||
),
|
||
},
|
||
{ title: '订单', dataIndex: 'orderCount', width: 70 },
|
||
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||
{
|
||
title: '操作',
|
||
width: 140,
|
||
render: (_, row) => (
|
||
<Space size={0}>
|
||
<Button type="link" size="small" onClick={() => void openDetail(row)}>
|
||
管理
|
||
</Button>
|
||
{canDeleteCities ? (
|
||
<Button type="link" size="small" danger onClick={() => void openDelete(row)}>
|
||
删除
|
||
</Button>
|
||
) : null}
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
const warehouseColumns: ColumnsType<WarehouseRow> = [
|
||
{ title: '仓库名', dataIndex: 'name' },
|
||
{ title: '地址', dataIndex: 'address', ellipsis: true },
|
||
{ title: '联系人', dataIndex: 'contactName', width: 90 },
|
||
{ title: '电话', dataIndex: 'contactPhone', width: 120 },
|
||
{
|
||
title: '管仓',
|
||
dataIndex: 'managerType',
|
||
width: 100,
|
||
render: (v: WarehouseManagerType) => WAREHOUSE_MANAGER_LABELS[v] || v,
|
||
},
|
||
{ title: '合伙人', dataIndex: 'partnerCompanyName', ellipsis: true, render: (v) => v || '—' },
|
||
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{s}</Tag> },
|
||
{
|
||
title: '操作',
|
||
width: 120,
|
||
render: (_, row) => (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => {
|
||
setWarehouseEditId(row.id);
|
||
setEditWarehouseManagerType(row.managerType);
|
||
warehouseEditForm.setFieldsValue(row);
|
||
setWarehouseEditOpen(true);
|
||
}}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
danger
|
||
onClick={() => {
|
||
Modal.confirm({
|
||
title: '确认删除仓库?',
|
||
onOk: async () => {
|
||
await request(`/admin/city-warehouses/${row.id}`, { method: 'DELETE' });
|
||
message.success('已删除');
|
||
if (detail?.id) await loadWarehouses(String(detail.id));
|
||
},
|
||
});
|
||
}}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>城市</Typography.Title>
|
||
<Button type="primary" onClick={openCreateModal}>新建城市</Button>
|
||
</Space>
|
||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
|
||
<Form.Item name="name" label="城市"><Input allowClear /></Form.Item>
|
||
<Form.Item name="code" label="编码"><Input allowClear /></Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select allowClear style={{ width: 110 }} options={Object.entries(CITY_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
||
</Form>
|
||
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []}
|
||
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
||
|
||
<Drawer title="城市管理" width={720} open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||
{detail && (
|
||
<Tabs
|
||
items={[
|
||
{
|
||
key: 'basic',
|
||
label: '基本信息',
|
||
children: (
|
||
<>
|
||
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="编码">{String(detail.code)}</Descriptions.Item>
|
||
<Descriptions.Item label="合伙人数">{String(detail.partnerBindingCount ?? '—')}</Descriptions.Item>
|
||
</Descriptions>
|
||
<Form form={editForm} layout="vertical">
|
||
<Form.Item name="name" label="城市名" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="province" label="省份" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select options={Object.entries(CITY_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
<Form.Item name="localMinQty" label="同城起购"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item>
|
||
<Form.Item name="crossMinQty" label="跨城起购"><InputNumber min={1} style={{ width: '100%' }} /></Form.Item>
|
||
<Form.Item
|
||
name="maxPartnerCommissionPercent"
|
||
label="合伙人佣金合计上限 %"
|
||
extra="订单佣金 + 核销佣金不得超过此比例;不填默认 5%"
|
||
>
|
||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} placeholder="5" />
|
||
</Form.Item>
|
||
<Button type="primary" onClick={async () => {
|
||
const v = await editForm.validateFields();
|
||
const { maxPartnerCommissionPercent, ...rest } = v;
|
||
await request(`/admin/cities/${detail.id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({
|
||
...rest,
|
||
maxPartnerCommissionRate:
|
||
maxPartnerCommissionPercent != null && maxPartnerCommissionPercent !== ''
|
||
? Number(maxPartnerCommissionPercent) / 100
|
||
: 0.05,
|
||
}),
|
||
});
|
||
message.success('已保存');
|
||
const refreshed = await request<Record<string, unknown>>(`/admin/cities/${detail.id}`);
|
||
setDetail(refreshed);
|
||
void reload();
|
||
}}>保存</Button>
|
||
{canDeleteCities ? (
|
||
<Button
|
||
danger
|
||
style={{ marginLeft: 8 }}
|
||
onClick={() =>
|
||
void openDelete({
|
||
id: String(detail.id),
|
||
code: String(detail.code),
|
||
name: String(detail.name),
|
||
province: String(detail.province),
|
||
status: String(detail.status),
|
||
storeCount: Number(detail.storeCount ?? 0),
|
||
orderCount: Number(detail.orderCount ?? 0),
|
||
createdAt: String(detail.createdAt ?? ''),
|
||
})
|
||
}
|
||
>
|
||
删除城市
|
||
</Button>
|
||
) : null}
|
||
</Form>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
key: 'partners',
|
||
label: '合伙人',
|
||
children: detail?.id ? (
|
||
<CityPartnersPanel
|
||
cityId={String(detail.id)}
|
||
cityCode={detail.code != null ? String(detail.code) : undefined}
|
||
maxPartnerCommissionRate={
|
||
detail.maxPartnerCommissionRate != null
|
||
? Number(detail.maxPartnerCommissionRate)
|
||
: 0.05
|
||
}
|
||
onChanged={() => {
|
||
void reload();
|
||
void loadPartners(String(detail.id));
|
||
}}
|
||
/>
|
||
) : null,
|
||
},
|
||
{
|
||
key: 'warehouses',
|
||
label: '仓库管理',
|
||
children: (
|
||
<>
|
||
<Button type="primary" style={{ marginBottom: 12 }} onClick={() => {
|
||
warehouseForm.resetFields();
|
||
warehouseForm.setFieldsValue({ managerType: WarehouseManagerType.HQ, status: 'ACTIVE' });
|
||
setWarehouseManagerType(WarehouseManagerType.HQ);
|
||
setWarehouseOpen(true);
|
||
}}>新增仓库</Button>
|
||
<Table rowKey="id" size="small" columns={warehouseColumns} dataSource={warehouses} pagination={false} />
|
||
</>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
)}
|
||
</Drawer>
|
||
|
||
<Modal title="新建开城城市" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={async () => {
|
||
const v = await createForm.validateFields();
|
||
if (!v.code || !v.name || !v.province) {
|
||
message.error('请选择省 / 市');
|
||
return;
|
||
}
|
||
await request('/admin/cities', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
code: v.code,
|
||
name: v.name,
|
||
province: v.province,
|
||
status: v.status,
|
||
}),
|
||
});
|
||
message.success('已创建');
|
||
setCreateOpen(false);
|
||
createForm.resetFields();
|
||
setCreateRegionPreview(null);
|
||
void reload();
|
||
}}>
|
||
<Form form={createForm} layout="vertical">
|
||
<Form.Item
|
||
name="regionCodes"
|
||
label="所在地区"
|
||
rules={[{ required: true, message: '请选择省 / 市' }]}
|
||
>
|
||
<ChinaProvinceCityCascader />
|
||
</Form.Item>
|
||
{createRegionPreview ? (
|
||
<Descriptions column={1} size="small" bordered style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="城市编码">{createRegionPreview.cityCode}</Descriptions.Item>
|
||
<Descriptions.Item label="省份">{createRegionPreview.province}</Descriptions.Item>
|
||
<Descriptions.Item label="城市名">{createRegionPreview.city}</Descriptions.Item>
|
||
</Descriptions>
|
||
) : (
|
||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
|
||
选择省 / 市后,城市编码将自动填入(不可编辑)
|
||
</Typography.Text>
|
||
)}
|
||
<Form.Item name="code" hidden rules={[{ required: true, message: '请选择省 / 市' }]}><Input /></Form.Item>
|
||
<Form.Item name="name" hidden rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="province" hidden rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="status" label="状态" initialValue="PENDING">
|
||
<Select options={Object.entries(CITY_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal title="新增仓库" open={warehouseOpen} onCancel={() => setWarehouseOpen(false)} onOk={async () => {
|
||
const v = await warehouseForm.validateFields();
|
||
await request(`/admin/cities/${detail?.id}/warehouses`, { method: 'POST', body: JSON.stringify(v) });
|
||
message.success('已创建');
|
||
setWarehouseOpen(false);
|
||
if (detail?.id) await loadWarehouses(String(detail.id));
|
||
}}>
|
||
<Form form={warehouseForm} layout="vertical">
|
||
<Form.Item name="name" label="仓库名" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="address" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="contactName" label="联系人" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="contactPhone" label="联系电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="managerType" label="管仓类型" rules={[{ required: true }]}>
|
||
<Select options={MANAGER_OPTIONS} onChange={(v) => setWarehouseManagerType(v)} />
|
||
</Form.Item>
|
||
{warehouseManagerType === WarehouseManagerType.PARTNER && (
|
||
<Form.Item name="partnerAccountId" label="管仓合伙人" rules={[{ required: true }]}>
|
||
<Select options={partners.map((p) => ({ value: p.id, label: p.companyName }))} />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item name="status" label="状态" initialValue="ACTIVE">
|
||
<Select options={[{ value: 'ACTIVE', label: '启用' }, { value: 'PAUSED', label: '暂停' }]} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal title="编辑仓库" open={warehouseEditOpen} onCancel={() => setWarehouseEditOpen(false)} onOk={async () => {
|
||
const v = await warehouseEditForm.validateFields();
|
||
await request(`/admin/city-warehouses/${warehouseEditId}`, { method: 'PUT', body: JSON.stringify(v) });
|
||
message.success('已更新');
|
||
setWarehouseEditOpen(false);
|
||
if (detail?.id) await loadWarehouses(String(detail.id));
|
||
}}>
|
||
<Form form={warehouseEditForm} layout="vertical">
|
||
<Form.Item name="name" label="仓库名" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="address" label="地址" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="contactName" label="联系人" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="contactPhone" label="联系电话" rules={[{ required: true }]}><Input /></Form.Item>
|
||
<Form.Item name="managerType" label="管仓类型" rules={[{ required: true }]}>
|
||
<Select options={MANAGER_OPTIONS} onChange={(v) => setEditWarehouseManagerType(v)} />
|
||
</Form.Item>
|
||
{editWarehouseManagerType === WarehouseManagerType.PARTNER && (
|
||
<Form.Item name="partnerAccountId" label="管仓合伙人" rules={[{ required: true }]}>
|
||
<Select options={partners.map((p) => ({ value: p.id, label: p.companyName }))} />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item name="status" label="状态">
|
||
<Select options={[{ value: 'ACTIVE', label: '启用' }, { value: 'PAUSED', label: '暂停' }]} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={deleteTarget ? `删除城市「${deleteTarget.name}」` : '删除城市'}
|
||
open={deleteOpen}
|
||
onCancel={() => {
|
||
if (deleteSubmitting) return;
|
||
setDeleteOpen(false);
|
||
}}
|
||
okText="确认删除"
|
||
okButtonProps={{
|
||
danger: true,
|
||
disabled:
|
||
!deletePreview?.canDelete ||
|
||
!deletePreview ||
|
||
confirmName.trim() !== (deletePreview?.city.name ?? ''),
|
||
loading: deleteSubmitting,
|
||
}}
|
||
confirmLoading={deleteSubmitting}
|
||
onOk={() => void confirmDelete()}
|
||
width={720}
|
||
destroyOnClose
|
||
>
|
||
{deleteLoading || !deletePreview ? (
|
||
<Typography.Text type="secondary">正在加载关联数据…</Typography.Text>
|
||
) : (
|
||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||
<Descriptions size="small" bordered column={2}>
|
||
<Descriptions.Item label="编码">{deletePreview.city.code}</Descriptions.Item>
|
||
<Descriptions.Item label="省份">{deletePreview.city.province}</Descriptions.Item>
|
||
<Descriptions.Item label="合伙人主账号">{deletePreview.summary.primaryPartnerCount}</Descriptions.Item>
|
||
<Descriptions.Item label="子账号">{deletePreview.summary.staffCount}</Descriptions.Item>
|
||
<Descriptions.Item label="门店">{deletePreview.summary.storeCount}</Descriptions.Item>
|
||
<Descriptions.Item label="仓库">{deletePreview.summary.warehouseCount}</Descriptions.Item>
|
||
<Descriptions.Item label="订单">{deletePreview.summary.orderCount}</Descriptions.Item>
|
||
<Descriptions.Item label="核销">{deletePreview.summary.redeemCount}</Descriptions.Item>
|
||
</Descriptions>
|
||
|
||
{deletePreview.blockers.length > 0 && (
|
||
<Typography.Paragraph type="danger" style={{ marginBottom: 0 }}>
|
||
{deletePreview.blockers.map((b) => (
|
||
<div key={b}>• {b}</div>
|
||
))}
|
||
</Typography.Paragraph>
|
||
)}
|
||
{deletePreview.warnings.length > 0 && (
|
||
<Typography.Paragraph type="warning" style={{ marginBottom: 0 }}>
|
||
{deletePreview.warnings.map((w) => (
|
||
<div key={w}>• {w}</div>
|
||
))}
|
||
</Typography.Paragraph>
|
||
)}
|
||
|
||
<div>
|
||
<Typography.Text strong>合伙人及子账号</Typography.Text>
|
||
<Table
|
||
size="small"
|
||
style={{ marginTop: 8 }}
|
||
pagination={false}
|
||
rowKey="id"
|
||
locale={{ emptyText: '无合伙人' }}
|
||
dataSource={deletePreview.partners.flatMap((p) => [
|
||
{
|
||
id: p.id,
|
||
kind: '主账号',
|
||
name: p.companyName || p.name,
|
||
phone: p.phone,
|
||
parent: '—',
|
||
},
|
||
...p.staff.map((s) => ({
|
||
id: s.id,
|
||
kind: '子账号',
|
||
name: s.name,
|
||
phone: s.phone,
|
||
parent: p.companyName || p.name,
|
||
})),
|
||
]).concat(
|
||
deletePreview.orphanStaff.map((s) => ({
|
||
id: s.id,
|
||
kind: '子账号',
|
||
name: s.name,
|
||
phone: s.phone,
|
||
parent: '(无主账号)',
|
||
})),
|
||
)}
|
||
columns={[
|
||
{ title: '类型', dataIndex: 'kind', width: 80 },
|
||
{ title: '名称', dataIndex: 'name', ellipsis: true },
|
||
{ title: '手机号', dataIndex: 'phone', width: 120 },
|
||
{ title: '归属', dataIndex: 'parent', ellipsis: true },
|
||
]}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Typography.Text strong>门店</Typography.Text>
|
||
<Table
|
||
size="small"
|
||
style={{ marginTop: 8 }}
|
||
pagination={false}
|
||
rowKey="id"
|
||
locale={{ emptyText: '无门店' }}
|
||
dataSource={deletePreview.stores}
|
||
columns={[
|
||
{ title: '门店名', dataIndex: 'name', ellipsis: true },
|
||
{ title: '电话', dataIndex: 'phone', width: 120 },
|
||
{ title: '状态', dataIndex: 'status', width: 90 },
|
||
{
|
||
title: '合伙人',
|
||
width: 140,
|
||
ellipsis: true,
|
||
render: (_, r) => r.partnerAccount?.companyName || r.partnerAccount?.phone || '—',
|
||
},
|
||
]}
|
||
/>
|
||
</div>
|
||
|
||
{deletePreview.canDelete ? (
|
||
<Form.Item
|
||
label={`请输入城市名称「${deletePreview.city.name}」确认删除`}
|
||
style={{ marginBottom: 0 }}
|
||
>
|
||
<Input
|
||
value={confirmName}
|
||
placeholder={deletePreview.city.name}
|
||
onChange={(e) => setConfirmName(e.target.value)}
|
||
disabled={deleteSubmitting}
|
||
/>
|
||
</Form.Item>
|
||
) : (
|
||
<Typography.Text type="secondary">存在阻断项,无法删除。请先处理订单等关联数据。</Typography.Text>
|
||
)}
|
||
</Space>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|