feat(ops): add global test whitelist and exclude test accounts from settlement
Unify product/store visibility on HQ whitelist, mark isTest snapshots, and fix SUPER_ADMIN access for the new module.
This commit is contained in:
@@ -0,0 +1,687 @@
|
||||
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<Paginated<PhoneRow> | 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<PhoneRow | null>(null);
|
||||
const [editForm] = Form.useForm();
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
|
||||
const [accountType, setAccountType] = useState<AccountType>('user');
|
||||
const [accountPhone, setAccountPhone] = useState('');
|
||||
const [accounts, setAccounts] = useState<Paginated<Record<string, unknown>> | 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<LinkedPayload | null>(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<Paginated<PhoneRow>>(`/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<Paginated<Record<string, unknown>>>(
|
||||
`/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<LinkedPayload>(`/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<PhoneRow> = [
|
||||
{ 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) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" onClick={() => void openLinked(row)}>
|
||||
关联账号
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditRow(row);
|
||||
editForm.setFieldsValue({ note: row.note ?? '' });
|
||||
setEditOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑备注
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认移出白名单?"
|
||||
description="将同步清除该手机号关联账号的测试标记"
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void onDeletePhone(row.id)}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function accountColumns(): ColumnsType<Record<string, unknown>> {
|
||||
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 ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
{ 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 ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
{ 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 ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
{ 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 ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
{ 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 ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
{ title: '下单', dataIndex: 'createdAt', width: 170, render: (v) => fmtTime(v as string) },
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
白名单管理
|
||||
</Typography.Title>
|
||||
|
||||
<Card size="small" loading={mockLoading} style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||
Mock 开关(与系统设置同源,勾选 = 不做真实验证)
|
||||
</Typography.Text>
|
||||
<Space wrap>
|
||||
<Checkbox
|
||||
checked={mockSms}
|
||||
disabled={mockSaving}
|
||||
onChange={(e) => void saveMockFlags({ MOCK_SMS: e.target.checked })}
|
||||
>
|
||||
短信不做真实验证
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={mockWechat}
|
||||
disabled={mockSaving}
|
||||
onChange={(e) => void saveMockFlags({ MOCK_WECHAT: e.target.checked })}
|
||||
>
|
||||
微信不做真实验证
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={mockPay}
|
||||
disabled={mockSaving}
|
||||
onChange={(e) => void saveMockFlags({ MOCK_PAY: e.target.checked })}
|
||||
>
|
||||
支付不做真实验证
|
||||
</Checkbox>
|
||||
</Space>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 8, fontSize: 12 }}>
|
||||
配置键:{MOCK_KEYS.join(' / ')}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'phones',
|
||||
label: '手机号名单',
|
||||
children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }} wrap>
|
||||
<Form
|
||||
form={phoneForm}
|
||||
layout="inline"
|
||||
onFinish={(v) => {
|
||||
setPhoneFilters({ phone: v.phone || undefined });
|
||||
setPhonePage(1);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="phone" label="手机号">
|
||||
<Input allowClear placeholder="模糊搜索" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
Modal.confirm({
|
||||
title: '从可见性白名单导入',
|
||||
content: '将商品/门店旧可见性手机号合并入全局名单(幂等),并同步测试标记。',
|
||||
okText: '开始导入',
|
||||
cancelText: '取消',
|
||||
onOk: () => onMigrate(),
|
||||
});
|
||||
}}
|
||||
loading={migrating}
|
||||
>
|
||||
从可见性白名单导入
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => setAddOpen(true)}>
|
||||
添加手机号
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={phonesLoading}
|
||||
columns={phoneColumns}
|
||||
dataSource={phones?.items ?? []}
|
||||
pagination={{
|
||||
current: phonePage,
|
||||
pageSize: phonePageSize,
|
||||
total: phones?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPhonePage(p);
|
||||
setPhonePageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'accounts',
|
||||
label: '测试账号记录',
|
||||
children: (
|
||||
<>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
value={accountType}
|
||||
options={ACCOUNT_TYPE_OPTIONS}
|
||||
onChange={(v: AccountType) => {
|
||||
setAccountType(v);
|
||||
setAccountPage(1);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="按手机号筛选"
|
||||
style={{ width: 160 }}
|
||||
value={accountPhone}
|
||||
onChange={(e) => setAccountPhone(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
setAccountPage(1);
|
||||
void loadAccounts();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (accountPage !== 1) setAccountPage(1);
|
||||
else void loadAccounts();
|
||||
}}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={accountsLoading}
|
||||
columns={accountColumns()}
|
||||
dataSource={accounts?.items ?? []}
|
||||
scroll={{ x: 900 }}
|
||||
pagination={{
|
||||
current: accountPage,
|
||||
pageSize: accountPageSize,
|
||||
total: accounts?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setAccountPage(p);
|
||||
setAccountPageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="添加白名单手机号"
|
||||
open={addOpen}
|
||||
onCancel={() => setAddOpen(false)}
|
||||
onOk={() => void onAddPhone()}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={addForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="1xxxxxxxxxx" maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item name="note" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={256} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`编辑备注 · ${editRow?.phone ?? ''}`}
|
||||
open={editOpen}
|
||||
onCancel={() => {
|
||||
setEditOpen(false);
|
||||
setEditRow(null);
|
||||
}}
|
||||
onOk={() => void onEditPhone()}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item name="note" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={256} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
title={linked ? `关联账号 · ${linked.phone.phone}` : '关联账号'}
|
||||
open={linkedOpen}
|
||||
onClose={() => setLinkedOpen(false)}
|
||||
width={560}
|
||||
destroyOnClose
|
||||
>
|
||||
{linkedLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : linked ? (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Descriptions size="small" column={1} bordered>
|
||||
<Descriptions.Item label="手机号">{linked.phone.phone}</Descriptions.Item>
|
||||
<Descriptions.Item label="备注">{linked.phone.note || '—'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div>
|
||||
<Typography.Title level={5}>C 端用户({linked.users.length})</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={linked.users}
|
||||
columns={[
|
||||
{ title: '编号', dataIndex: 'userNo' },
|
||||
{ title: '昵称', dataIndex: 'nickname', render: (v) => v || '—' },
|
||||
{
|
||||
title: '测试',
|
||||
dataIndex: 'isTest',
|
||||
width: 70,
|
||||
render: (v) => (v ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={5}>门店账号({linked.storeAccounts.length})</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={linked.storeAccounts}
|
||||
columns={[
|
||||
{ title: '姓名', dataIndex: 'name' },
|
||||
{ title: '状态', dataIndex: 'status', width: 90 },
|
||||
{
|
||||
title: '测试',
|
||||
dataIndex: 'isTest',
|
||||
width: 70,
|
||||
render: (v) => (v ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={5}>合伙人({linked.partners.length})</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={linked.partners}
|
||||
columns={[
|
||||
{ title: '姓名', dataIndex: 'name' },
|
||||
{ title: '公司', dataIndex: 'companyName', render: (v) => v || '—' },
|
||||
{
|
||||
title: '测试',
|
||||
dataIndex: 'isTest',
|
||||
width: 70,
|
||||
render: (v) => (v ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={5}>门店({linked.stores.length})</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={linked.stores}
|
||||
columns={[
|
||||
{ title: '名称', dataIndex: 'name' },
|
||||
{ title: '状态', dataIndex: 'status', width: 90 },
|
||||
{
|
||||
title: '测试',
|
||||
dataIndex: 'isTest',
|
||||
width: 70,
|
||||
render: (v) => (v ? <Tag color="orange">测试</Tag> : '—'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
) : null}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user