b626db5d84
Unify product/store visibility on HQ whitelist, mark isTest snapshots, and fix SUPER_ADMIN access for the new module.
688 lines
23 KiB
TypeScript
688 lines
23 KiB
TypeScript
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>
|
||
);
|
||
}
|