import { useCallback, useEffect, useState } from 'react'; import { Button, Checkbox, Drawer, Form, Input, InputNumber, Modal, Select, Space, Table, Tabs, 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 { fmtTime } from '../lib/constants'; import CityDistrictMultiSelect from './CityDistrictMultiSelect'; import PartnerSubAccountList from './PartnerSubAccountList'; type PartnerRow = { id: string; companyName: string; phone: string; name: string; scopeType?: string; districtCodes?: string[] | null; orderCommissionRate?: number; redeemCommissionRate?: number; accountCount: number; createdAt: string; }; type PartnerDetail = PartnerRow & { address?: string; districtCodes?: string[] | null; bindingStatus?: string; bankAccountName?: string | null; bankAccountNo?: string | null; bankBranch?: string | null; managedWarehouseId?: string | null; managedWarehouseName?: string | null; contactPhone?: string | null; children?: Array<{ id: string; phone: string; name: string; staffRole?: string; permissions?: string[]; status: string; }>; }; type Props = { cityId: string; cityCode?: string; maxPartnerCommissionRate?: number; onChanged?: () => void; }; 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 CityPartnersPanel({ cityId, cityCode, maxPartnerCommissionRate = 0.05, onChanged, }: Props) { const [editForm] = Form.useForm(); const [createForm] = Form.useForm(); const [subForm] = Form.useForm(); const [subEditForm] = Form.useForm(); const [partners, setPartners] = useState([]); const [loading, setLoading] = useState(false); 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 [createScopeType, setCreateScopeType] = useState(CityPartnerScopeType.CITY_WIDE); const [editScopeType, setEditScopeType] = useState(CityPartnerScopeType.CITY_WIDE); const loadPartners = useCallback(async () => { setLoading(true); try { const res = await request>(`/admin/partners?cityId=${cityId}&pageSize=100`); setPartners(res.items); } finally { setLoading(false); } }, [cityId]); useEffect(() => { void loadPartners(); }, [loadPartners]); async function openPartner(id: string) { const d = await request(`/admin/partners/${id}`); setDetail(d); setEditScopeType((d.scopeType as CityPartnerScopeType) || CityPartnerScopeType.CITY_WIDE); 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); } async function savePartner() { if (!detail) 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); } } async function deleteSubAccount(subId: string) { await request(`/admin/partner-accounts/${subId}`, { method: 'DELETE' }); message.success('子账号已删除'); if (detail) void openPartner(detail.id); void loadPartners(); onChanged?.(); } const columns: ColumnsType = [ { 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 }, { title: '主账号', dataIndex: 'phone', width: 120 }, { title: '管辖', dataIndex: 'scopeType', width: 100, render: (v) => (v ? CITY_PARTNER_SCOPE_LABELS[v as keyof typeof CITY_PARTNER_SCOPE_LABELS] || v : '—'), }, { title: '佣金', width: 120, render: (_, row) => `${Math.round((row.orderCommissionRate ?? 0) * 100)}% / ${Math.round((row.redeemCommissionRate ?? 0.03) * 100)}%`, }, { title: '子账号', dataIndex: 'accountCount', width: 70, render: (n) => Math.max(0, n - 1) }, { title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; return ( <> setDrawerOpen(false)} extra={} > {detail && ( setEditScopeType(v)} /> {editScopeType === CityPartnerScopeType.DISTRICT && ( )} 订单 + 核销合计不得超过 {(maxPartnerCommissionRate * 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={(row) => { setSubEditId(row.id); subEditForm.setFieldsValue({ name: row.name, phone: row.phone, staffRole: row.staffRole ?? 'INTERNAL', permissions: row.permissions ?? [], status: row.status, }); setSubEditOpen(true); }} onDelete={(subId) => void deleteSubAccount(subId)} /> ), }, ]} /> )} setCreateOpen(false)} onOk={async () => { 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); } }}>
); }