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; isTest?: boolean; }; 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>({}); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/partners', () => { const qs = new URLSearchParams(); if (filters.companyName) qs.set('companyName', String(filters.companyName)); if (filters.phone) qs.set('phone', String(filters.phone)); if (filters.cityId) qs.set('cityId', String(filters.cityId)); if (filters.excludeTest) qs.set('excludeTest', 'true'); return qs; }, [filters], ); const [detail, setDetail] = useState(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(null); const [subEditParentId, setSubEditParentId] = useState(null); const [cities, setCities] = useState([]); const [createScopeType, setCreateScopeType] = useState(CityPartnerScopeType.CITY_WIDE); const [editScopeType, setEditScopeType] = useState(CityPartnerScopeType.CITY_WIDE); const [maxCommissionRate, setMaxCommissionRate] = useState(0.05); const [createMaxRate, setCreateMaxRate] = useState(0.05); const [createCityCode, setCreateCityCode] = useState(); const loadCities = useCallback(async () => { const res = await request>(`/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(`/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 = [ { 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: 120, ellipsis: true, render: (v, row) => ( {v} {row.isTest ? 测试 : null} ), }, { 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) => ( {CITY_PARTNER_STATUS_LABELS[v as CityPartnerStatus] || v || '—'} ), }, { title: '管仓仓库', dataIndex: 'managedWarehouseName', width: 110, ellipsis: true, render: (v) => v || '—', }, { title: '门店', dataIndex: 'storeCount', width: 60, render: (n: number, row) => ( {n ?? 0} ), }, { 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) => ( ), }, ]; return (
城市合伙人 账号结构仅两级:主账号 → 子账号(子账号不可再添加下级)。管仓仓库请在{' '} 仓库管理 中分配。 } />
{ setFilters(v); setPage(1); }} > 过滤测试账号
( 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); }, }} /> setDrawerOpen(false)} extra={ } > {detail && ( <> {detail.cityName ?? '—'} {detail.storeCount ?? 0} {detail.managedWarehouseName ?? ( 未分配(请前往 仓库管理 设置) )} setEditScopeType(v)} /> {editScopeType === CityPartnerScopeType.DISTRICT && ( )} 订单 + 核销合计不得超过 {(maxCommissionRate * 100).toFixed(2)}% ), }, { key: 'staff', label: `子账号 (${Math.max(0, (detail.accountCount ?? 1) - 1)})`, children: ( { 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)} /> ), }, ]} /> )} 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); } }} >
); }