import { useCallback, useEffect, useState } from 'react'; import { Button, Card, Checkbox, Descriptions, Drawer, Form, Input, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request, type Paginated } from '../lib/api'; import { fmtTime } from '../lib/constants'; const MOCK_KEYS = ['MOCK_SMS', 'MOCK_WECHAT', 'MOCK_PAY'] as const; type PhoneRow = { id: string; phone: string; note: string | null; createdByHqId: string | null; createdAt: string; updatedAt: string; }; type AccountType = 'user' | 'store_account' | 'partner' | 'store' | 'order'; type LinkedPayload = { phone: PhoneRow; users: Array<{ id: string; userNo: string; phone: string | null; nickname: string | null; isTest: boolean; status: number }>; storeAccounts: Array<{ id: string; phone: string; name: string; isTest: boolean; status: string }>; partners: Array<{ id: string; phone: string; name: string; companyName: string | null; isTest: boolean; status: string }>; stores: Array<{ id: string; name: string; phone: string; isTest: boolean; status: string }>; }; const ACCOUNT_TYPE_OPTIONS: { value: AccountType; label: string }[] = [ { value: 'user', label: 'C 端用户' }, { value: 'store_account', label: '门店账号' }, { value: 'partner', label: '合伙人' }, { value: 'store', label: '门店' }, { value: 'order', label: '订单' }, ]; export default function TestWhitelistPage() { const [mockSms, setMockSms] = useState(false); const [mockWechat, setMockWechat] = useState(false); const [mockPay, setMockPay] = useState(false); const [mockLoading, setMockLoading] = useState(true); const [mockSaving, setMockSaving] = useState(false); const [phoneForm] = Form.useForm(); const [phones, setPhones] = useState | null>(null); const [phonesLoading, setPhonesLoading] = useState(false); const [phonePage, setPhonePage] = useState(1); const [phonePageSize, setPhonePageSize] = useState(20); const [phoneFilters, setPhoneFilters] = useState<{ phone?: string }>({}); const [addOpen, setAddOpen] = useState(false); const [addForm] = Form.useForm(); const [editOpen, setEditOpen] = useState(false); const [editRow, setEditRow] = useState(null); const [editForm] = Form.useForm(); const [migrating, setMigrating] = useState(false); const [accountType, setAccountType] = useState('user'); const [accountPhone, setAccountPhone] = useState(''); const [accounts, setAccounts] = useState> | null>(null); const [accountsLoading, setAccountsLoading] = useState(false); const [accountPage, setAccountPage] = useState(1); const [accountPageSize, setAccountPageSize] = useState(20); const [linkedOpen, setLinkedOpen] = useState(false); const [linkedLoading, setLinkedLoading] = useState(false); const [linked, setLinked] = useState(null); async function loadMockFlags() { setMockLoading(true); try { const cfg = await request<{ mockSms: boolean; mockWechat: boolean; mockPay: boolean }>( '/admin/test-whitelist/mock-flags', ); setMockSms(!!cfg.mockSms); setMockWechat(!!cfg.mockWechat); setMockPay(!!cfg.mockPay); } catch (e) { message.error(e instanceof Error ? e.message : '加载 Mock 配置失败'); } finally { setMockLoading(false); } } async function saveMockFlags(next: { MOCK_SMS?: boolean; MOCK_WECHAT?: boolean; MOCK_PAY?: boolean }) { const body: { mockSms?: boolean; mockWechat?: boolean; mockPay?: boolean } = {}; if (next.MOCK_SMS !== undefined) body.mockSms = next.MOCK_SMS; if (next.MOCK_WECHAT !== undefined) body.mockWechat = next.MOCK_WECHAT; if (next.MOCK_PAY !== undefined) body.mockPay = next.MOCK_PAY; setMockSaving(true); try { const cfg = await request<{ mockSms: boolean; mockWechat: boolean; mockPay: boolean }>( '/admin/test-whitelist/mock-flags', { method: 'PUT', body: JSON.stringify(body), }, ); setMockSms(!!cfg.mockSms); setMockWechat(!!cfg.mockWechat); setMockPay(!!cfg.mockPay); message.success('已保存'); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); await loadMockFlags(); } finally { setMockSaving(false); } } const loadPhones = useCallback(async () => { setPhonesLoading(true); try { const qs = new URLSearchParams({ page: String(phonePage), pageSize: String(phonePageSize), }); if (phoneFilters.phone) qs.set('phone', phoneFilters.phone); const res = await request>(`/admin/test-whitelist/phones?${qs}`); setPhones(res); } catch (e) { message.error(e instanceof Error ? e.message : '加载手机号名单失败'); } finally { setPhonesLoading(false); } }, [phonePage, phonePageSize, phoneFilters]); const loadAccounts = useCallback(async () => { setAccountsLoading(true); try { const qs = new URLSearchParams({ type: accountType, page: String(accountPage), pageSize: String(accountPageSize), }); if (accountPhone.trim()) qs.set('phone', accountPhone.trim()); const res = await request>>( `/admin/test-whitelist/accounts?${qs}`, ); setAccounts(res); } catch (e) { message.error(e instanceof Error ? e.message : '加载测试账号失败'); } finally { setAccountsLoading(false); } }, [accountType, accountPage, accountPageSize, accountPhone]); useEffect(() => { void loadMockFlags(); }, []); useEffect(() => { void loadPhones(); }, [loadPhones]); useEffect(() => { void loadAccounts(); }, [loadAccounts]); async function onAddPhone() { const v = await addForm.validateFields(); try { await request('/admin/test-whitelist/phones', { method: 'POST', body: JSON.stringify({ phone: v.phone, note: v.note || undefined }), }); message.success('已添加'); setAddOpen(false); addForm.resetFields(); setPhonePage(1); void loadPhones(); void loadAccounts(); } catch (e) { message.error(e instanceof Error ? e.message : '添加失败'); } } async function onEditPhone() { if (!editRow) return; const v = await editForm.validateFields(); try { await request(`/admin/test-whitelist/phones/${editRow.id}`, { method: 'PATCH', body: JSON.stringify({ note: v.note ?? null }), }); message.success('已更新'); setEditOpen(false); setEditRow(null); void loadPhones(); } catch (e) { message.error(e instanceof Error ? e.message : '更新失败'); } } async function onDeletePhone(id: string) { try { await request(`/admin/test-whitelist/phones/${id}`, { method: 'DELETE' }); message.success('已删除'); void loadPhones(); void loadAccounts(); } catch (e) { message.error(e instanceof Error ? e.message : '删除失败'); } } async function onMigrate() { setMigrating(true); try { const res = await request<{ importedCandidates: number; added: number }>( '/admin/test-whitelist/migrate-visibility', { method: 'POST' }, ); message.success( `导入完成:候选 ${res.importedCandidates} 个,新增 ${res.added} 个`, ); void loadPhones(); void loadAccounts(); } catch (e) { message.error(e instanceof Error ? e.message : '导入失败'); } finally { setMigrating(false); } } async function openLinked(row: PhoneRow) { setLinkedOpen(true); setLinkedLoading(true); setLinked(null); try { const res = await request(`/admin/test-whitelist/phones/${row.id}/linked`); setLinked(res); } catch (e) { message.error(e instanceof Error ? e.message : '加载关联失败'); setLinkedOpen(false); } finally { setLinkedLoading(false); } } const phoneColumns: ColumnsType = [ { title: '手机号', dataIndex: 'phone', width: 140 }, { title: '备注', dataIndex: 'note', ellipsis: true, render: (v) => v || '—', }, { title: '创建时间', dataIndex: 'createdAt', width: 170, render: fmtTime, }, { title: '操作', width: 220, render: (_, row) => ( void onDeletePhone(row.id)} > ), }, ]; function accountColumns(): ColumnsType> { if (accountType === 'user') { return [ { title: '用户编号', dataIndex: 'userNo', width: 120 }, { title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => (v as string) || '—' }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '标记', dataIndex: 'isTest', width: 80, render: (v) => (v ? 测试 : '—'), }, { title: '注册', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) }, ]; } if (accountType === 'store_account') { return [ { title: '姓名', dataIndex: 'name', width: 100 }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '状态', dataIndex: 'status', width: 90 }, { title: '标记', dataIndex: 'isTest', width: 80, render: (v) => (v ? 测试 : '—'), }, { title: '创建', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) }, ]; } if (accountType === 'partner') { return [ { title: '姓名', dataIndex: 'name', width: 100 }, { title: '公司', dataIndex: 'companyName', ellipsis: true, render: (v) => (v as string) || '—' }, { title: '手机', dataIndex: 'phone', width: 120 }, { title: '标记', dataIndex: 'isTest', width: 80, render: (v) => (v ? 测试 : '—'), }, { title: '创建', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) }, ]; } if (accountType === 'store') { return [ { title: '门店名', dataIndex: 'name', width: 160, ellipsis: true }, { title: '城市', dataIndex: 'cityName', width: 90 }, { title: '电话', dataIndex: 'phone', width: 120 }, { title: '状态', dataIndex: 'status', width: 90 }, { title: '标记', dataIndex: 'isTest', width: 80, render: (v) => (v ? 测试 : '—'), }, { title: '创建', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) }, ]; } return [ { title: '订单号', dataIndex: 'orderNo', width: 180 }, { title: '状态', dataIndex: 'status', width: 110 }, { title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}`, }, { title: '收货手机', dataIndex: 'receiverPhone', width: 120 }, { title: '用户手机', width: 120, render: (_, row) => (row.user as { phone?: string | null } | undefined)?.phone || '—', }, { title: '标记', dataIndex: 'isTest', width: 80, render: (v) => (v ? 测试 : '—'), }, { title: '下单', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) }, ]; } return (
白名单管理 Mock 开关(与系统设置同源,勾选 = 不做真实验证) void saveMockFlags({ MOCK_SMS: e.target.checked })} > 短信不做真实验证 void saveMockFlags({ MOCK_WECHAT: e.target.checked })} > 微信不做真实验证 void saveMockFlags({ MOCK_PAY: e.target.checked })} > 支付不做真实验证 配置键:{MOCK_KEYS.join(' / ')}
{ setPhoneFilters({ phone: v.phone || undefined }); setPhonePage(1); }} >
{ setPhonePage(p); setPhonePageSize(ps); }, }} /> ), }, { key: 'accounts', label: '测试账号记录', children: ( <> setAccountPhone(e.target.value)} onPressEnter={() => { setAccountPage(1); void loadAccounts(); }} />
{ setAccountPage(p); setAccountPageSize(ps); }, }} /> ), }, ]} /> setAddOpen(false)} onOk={() => void onAddPhone()} destroyOnClose >
{ setEditOpen(false); setEditRow(null); }} onOk={() => void onEditPhone()} destroyOnClose >
setLinkedOpen(false)} width={560} destroyOnClose > {linkedLoading ? ( 加载中… ) : linked ? ( {linked.phone.phone} {linked.phone.note || '—'}
C 端用户({linked.users.length})
v || '—' }, { title: '测试', dataIndex: 'isTest', width: 70, render: (v) => (v ? 测试 : '—'), }, ]} />
门店账号({linked.storeAccounts.length})
(v ? 测试 : '—'), }, ]} />
合伙人({linked.partners.length})
v || '—' }, { title: '测试', dataIndex: 'isTest', width: 70, render: (v) => (v ? 测试 : '—'), }, ]} />
门店({linked.stores.length})
(v ? 测试 : '—'), }, ]} /> ) : null} ); }