import { useCallback, useEffect, useState } from 'react'; import { Button, Checkbox, Drawer, Form, Input, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PARTNER_PERMISSION_KEYS, PARTNER_PERMISSION_LABELS, PARTNER_STAFF_ROLE_LABELS, type PartnerPermissionKey } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { AdminCellLine } from '../components/AdminCellLine'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; type PartnerOption = { id: string; companyName: string }; type ParentAccount = { id: string; name: string; phone: string }; type AccountTreeRow = { id: string; phone: string; name: string; status: string; isPrimary: number; staffRole?: string | null; permissions?: string[] | null; parentAccountId?: string | null; parent?: ParentAccount | null; createdAt: string; lastLoginAt?: string | null; companyName?: string | null; children?: AccountTreeRow[]; }; type BillRow = { id: string; billNo: string; totalAmount: number; orderCommission: number; redeemCommission: number; status: string; periodStart: string; periodEnd: string; createdAt: string; }; type OrderRow = { id: string; orderNo: string; status: string; payAmount: number; createdAt: string; user?: { userNo: string; phone: string | null }; }; type Detail = AccountTreeRow & { bills?: BillRow[]; orders?: OrderRow[] }; const STAFF_ROLE_OPTIONS = Object.entries(PARTNER_STAFF_ROLE_LABELS).map(([value, label]) => ({ value, label, })); const PERMISSION_OPTIONS = PARTNER_PERMISSION_KEYS.map((key: PartnerPermissionKey) => ({ value: key, label: PARTNER_PERMISSION_LABELS[key], })); function filterTree(rows: AccountTreeRow[], phone?: string, status?: string): AccountTreeRow[] { const phoneQ = phone?.trim(); const match = (row: AccountTreeRow) => { const phoneOk = !phoneQ || row.phone.includes(phoneQ); const statusOk = !status || row.status === status; return phoneOk && statusOk; }; const walk = (list: AccountTreeRow[]): AccountTreeRow[] => { const result: AccountTreeRow[] = []; for (const row of list) { const children = row.children?.length ? walk(row.children) : undefined; if (match(row) || (children && children.length > 0)) { result.push({ ...row, children: children?.length ? children : undefined }); } } return result; }; return walk(rows); } function staffRoleLabel(role?: string | null) { if (!role) return '-'; return PARTNER_STAFF_ROLE_LABELS[role as keyof typeof PARTNER_STAFF_ROLE_LABELS] ?? role; } export default function PartnerAccountsPage() { const [form] = Form.useForm(); const [editForm] = Form.useForm(); const [subForm] = Form.useForm(); const [filters, setFilters] = useState<{ phone?: string; status?: string; partnerId?: string }>({}); const [treeData, setTreeData] = useState([]); const [loading, setLoading] = useState(false); const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [subOpen, setSubOpen] = useState(false); const [subParent, setSubParent] = useState(null); const [partners, setPartners] = useState([]); const loadTree = useCallback(async () => { setLoading(true); try { const qs = new URLSearchParams(); if (filters.partnerId) qs.set('partnerId', filters.partnerId); const path = qs.toString() ? `/admin/partner-accounts/tree?${qs}` : '/admin/partner-accounts/tree'; const res = await request(path); setTreeData(filterTree(res, filters.phone, filters.status)); } finally { setLoading(false); } }, [filters]); useEffect(() => { void loadPartners(); }, []); useEffect(() => { void loadTree(); }, [loadTree]); useEffect(() => { if (!drawerOpen || !detail) return; editForm.setFieldsValue({ name: detail.name, phone: detail.phone, status: detail.status, permissions: detail.permissions ?? [], }); }, [drawerOpen, detail, editForm]); async function loadPartners() { const res = await request>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`); setPartners(res.items); } async function openAccount(id: string) { try { const d = await request(`/admin/partner-accounts/${id}`); setDetail(d); setDrawerOpen(true); } catch (e) { message.error(e instanceof Error ? e.message : '加载账户详情失败'); } } async function saveAccount() { if (!detail) return; const v = await editForm.validateFields(); await request(`/admin/partner-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) }); message.success('已保存'); setDrawerOpen(false); void loadTree(); } async function deleteSubAccount(row: AccountTreeRow) { await request(`/admin/partner-accounts/${row.id}`, { method: 'DELETE' }); message.success('子账号已删除'); void loadTree(); } function openAddSub(parent: AccountTreeRow) { setSubParent(parent); subForm.resetFields(); subForm.setFieldsValue({ staffRole: 'INTERNAL', permissions: ['store:create', 'store:manage'] }); setSubOpen(true); } function collectExpandableKeys(rows: AccountTreeRow[]): string[] { const keys: string[] = []; for (const row of rows) { if (row.children?.length) { keys.push(row.id); keys.push(...collectExpandableKeys(row.children)); } } return keys; } const columns: ColumnsType = [ { title: '姓名 / 类型', width: 200, ellipsis: true, render: (_, row) => ( ), }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '开城合伙人', width: 140, ellipsis: true, render: (_, row) => row.companyName || row.parent?.name || '—', }, { title: '账号类型', width: 90, render: (_, row) => ( {row.parentAccountId ? '子账号' : '主账号'} ), }, { title: '角色', dataIndex: 'staffRole', width: 100, render: (role) => staffRoleLabel(role), }, { title: '所属主账号', width: 140, render: (_, row) => ( row.parentAccountId && row.parent ? `${row.parent.name} / ${row.parent.phone}` : '-' ), }, { title: '状态', dataIndex: 'status', width: 80, render: (s) => {ACCOUNT_STATUS_LABELS[s] || s} }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '操作', width: 180, render: (_, row) => ( e.stopPropagation()}> {!row.parentAccountId ? ( ) : null} {row.parentAccountId ? ( void deleteSubAccount(row)}> ) : null} ), }, ]; const billColumns: ColumnsType = [ { title: '账单号', dataIndex: 'billNo', width: 140 }, { title: '总额', dataIndex: 'totalAmount', width: 90, render: (v) => `¥${v}` }, { title: '订单佣金', dataIndex: 'orderCommission', width: 90, render: (v) => `¥${v}` }, { title: '核销佣金', dataIndex: 'redeemCommission', width: 90, render: (v) => `¥${v}` }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {PARTNER_BILL_STATUS_LABELS[s] || s} }, { title: '周期', width: 200, render: (_, r) => `${fmtTime(r.periodStart)} ~ ${fmtTime(r.periodEnd)}` }, ]; const orderColumns: ColumnsType = [ { title: '订单号', dataIndex: 'orderNo', width: 160 }, { title: '用户', dataIndex: ['user', 'userNo'], width: 110 }, { title: '金额', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` }, { title: '状态', dataIndex: 'status', width: 90, render: (s) => {ORDER_STATUS_LABELS[s] || s} }, { title: '下单', dataIndex: 'createdAt', width: 160, render: fmtTime }, ]; return (
合伙人子账号 主账号在「开城合伙人」创建;此处仅管理子账号树与权限
{ setFilters({ phone: v.phone?.trim() || undefined, status: v.status || undefined, partnerId: v.partnerId || undefined, }); }} > {detail.parentAccountId && detail.parent ? ( ) : null}