import { useEffect, useState } from 'react'; import { Button, Descriptions, Drawer, Form, Input, Select, Space, Table, Tag, Typography, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request } from '../lib/api'; import { AdminCellLine } from '../components/AdminCellLine'; import { fmtTime, maskPhone } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; type ActorType = 'USER' | 'STORE' | 'PARTNER' | 'HQ'; type Identity = { actorType: ActorType; actorId: string; phone: string | null; name: string | null; wxOpenId: string; wxUnionId: string | null; phoneVerified?: boolean; refLabel: string | null; refId: string | null; lastLoginAt: string | null; status: string | number; }; type GroupRow = { groupKey: string; unionId: string | null; identityCount: number; actorTypes: ActorType[]; multiRole: boolean; primaryPhone: string | null; latestLoginAt: string | null; identities: Identity[]; }; const ACTOR_TYPE_LABELS: Record = { USER: 'C 端用户', STORE: '门店账号', PARTNER: '合伙人账号', HQ: 'HQ 账号', }; const ACTOR_TYPE_COLORS: Record = { USER: 'blue', STORE: 'green', PARTNER: 'orange', HQ: 'purple', }; function renderActorTags(types: ActorType[]) { return types.map((t) => ( {ACTOR_TYPE_LABELS[t]} )); } export default function WechatBindingsPage() { const [form] = Form.useForm(); const [filters, setFilters] = useState>({ actorType: '', phone: '', unionId: '', openId: '', }); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/wechat-bindings', () => { const qs = new URLSearchParams(); if (filters.actorType) qs.set('actorType', filters.actorType); if (filters.phone) qs.set('phone', filters.phone); if (filters.unionId) qs.set('unionId', filters.unionId); if (filters.openId) qs.set('openId', filters.openId); return qs; }, [filters], ); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); useEffect(() => { form.setFieldsValue(filters); }, [form, filters]); async function openDetail(row: GroupRow) { const res = await request(`/admin/wechat-bindings/${encodeURIComponent(row.groupKey)}`); setDetail(res); setDrawerOpen(true); } const columns: ColumnsType = [ { title: 'unionId', dataIndex: 'unionId', width: 180, ellipsis: true, render: (v) => v || 无 unionId, }, { title: '身份数', dataIndex: 'identityCount', width: 90, render: (v, r) => ( {v} {r.multiRole ? 一人多角色 : null} ), }, { title: '端类型', dataIndex: 'actorTypes', width: 220, render: (types: ActorType[]) => renderActorTags(types), }, { title: '手机号', dataIndex: 'primaryPhone', width: 140, render: (v) => maskPhone(v), }, { title: '身份摘要', ellipsis: true, render: (_, r) => ( ACTOR_TYPE_LABELS[i.actorType]).join(' / ')} secondary={r.identities .map((i) => i.refLabel || i.name || (i.phone ? maskPhone(i.phone) : '')) .filter(Boolean) .join(' · ')} /> ), }, { title: '最近登录', dataIndex: 'latestLoginAt', width: 160, render: fmtTime, }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; const identityColumns: ColumnsType = [ { title: '端类型', dataIndex: 'actorType', width: 120, render: (t: ActorType) => {ACTOR_TYPE_LABELS[t]}, }, { title: '账号', ellipsis: true, render: (_, r) => ( ), }, { title: '归属', dataIndex: 'refLabel', width: 160, ellipsis: true, render: (v, r) => (v ? `${v}${r.refId ? ` #${r.refId}` : ''}` : '—'), }, { title: 'wxOpenId', dataIndex: 'wxOpenId', width: 160, ellipsis: true, }, { title: '手机验证', width: 90, render: (_, r) => r.actorType === 'USER' ? ( r.phoneVerified ? 已验证 : 未验证 ) : ( '—' ), }, { title: '最近登录', dataIndex: 'lastLoginAt', width: 160, render: fmtTime, }, { title: '状态', dataIndex: 'status', width: 90, render: (v) => String(v), }, ]; return (
微信绑定总览 按 unionId 聚合展示已绑定微信的 C 端用户、门店账号、合伙人账号与 HQ 账号;无 unionId 时按单账号分组。
{ setPage(1); setFilters({ actorType: values.actorType ?? '', phone: values.phone?.trim() ?? '', unionId: values.unionId?.trim() ?? '', openId: values.openId?.trim() ?? '', }); }} >
rowKey="groupKey" loading={loading} columns={columns} dataSource={data?.items ?? []} pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)} > {detail ? ( <> {detail.groupKey} {detail.unionId || '—'} {detail.identityCount} {renderActorTags(detail.actorTypes)} {detail.multiRole ? : } {fmtTime(detail.latestLoginAt)} rowKey={(r) => `${r.actorType}-${r.actorId}`} size="small" columns={identityColumns} dataSource={detail.identities} pagination={false} /> ) : null}
); }