674 lines
17 KiB
TypeScript
674 lines
17 KiB
TypeScript
import {
|
|
customercontrollerAddtag,
|
|
customercontrollerDeltag,
|
|
customercontrollerGetcustomerlist,
|
|
customercontrollerGettags,
|
|
customercontrollerSetrate,
|
|
customercontrollerSynccustomers,
|
|
} from '@/servers/api/customer';
|
|
import { sitecontrollerAll } from '@/servers/api/site';
|
|
import {
|
|
ActionType,
|
|
ModalForm,
|
|
PageContainer,
|
|
ProColumns,
|
|
ProFormDateTimeRangePicker,
|
|
ProFormSelect,
|
|
ProFormText,
|
|
ProTable,
|
|
} from '@ant-design/pro-components';
|
|
import { App, Avatar, Button, Form, Rate, Space, Tag, Tooltip } from 'antd';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import HistoryOrders from './HistoryOrders';
|
|
|
|
// 地址格式化函数
|
|
const formatAddress = (address: any) => {
|
|
if (!address) return '-';
|
|
|
|
if (typeof address === 'string') {
|
|
try {
|
|
address = JSON.parse(address);
|
|
} catch (e) {
|
|
return address;
|
|
}
|
|
}
|
|
|
|
const {
|
|
first_name,
|
|
last_name,
|
|
company,
|
|
address_1,
|
|
address_2,
|
|
city,
|
|
state,
|
|
postcode,
|
|
country,
|
|
phone: addressPhone,
|
|
email: addressEmail,
|
|
} = address;
|
|
|
|
const parts = [];
|
|
|
|
// 姓名
|
|
const fullName = [first_name, last_name].filter(Boolean).join(' ');
|
|
if (fullName) parts.push(fullName);
|
|
|
|
// 公司
|
|
if (company) parts.push(company);
|
|
|
|
// 地址行
|
|
if (address_1) parts.push(address_1);
|
|
if (address_2) parts.push(address_2);
|
|
|
|
// 城市、州、邮编
|
|
const locationParts = [city, state, postcode].filter(Boolean).join(', ');
|
|
if (locationParts) parts.push(locationParts);
|
|
|
|
// 国家
|
|
if (country) parts.push(country);
|
|
|
|
// 联系方式
|
|
if (addressPhone) parts.push(`电话: ${addressPhone}`);
|
|
if (addressEmail) parts.push(`邮箱: ${addressEmail}`);
|
|
|
|
return parts.join(', ');
|
|
};
|
|
|
|
// 地址卡片组件
|
|
const AddressCell: React.FC<{ address: any; title: string }> = ({
|
|
address,
|
|
title,
|
|
}) => {
|
|
const formattedAddress = formatAddress(address);
|
|
|
|
if (formattedAddress === '-') {
|
|
return <span>-</span>;
|
|
}
|
|
|
|
return (
|
|
<Tooltip
|
|
title={
|
|
<div style={{ maxWidth: 300, whiteSpace: 'pre-line' }}>
|
|
<strong>{title}:</strong>
|
|
<br />
|
|
{formattedAddress}
|
|
</div>
|
|
}
|
|
placement="topLeft"
|
|
>
|
|
<div
|
|
style={{
|
|
maxWidth: 200,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{formattedAddress}
|
|
</div>
|
|
</Tooltip>
|
|
);
|
|
};
|
|
|
|
const CustomerList: React.FC = () => {
|
|
const actionRef = useRef<ActionType>();
|
|
const { message } = App.useApp();
|
|
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
|
|
|
|
|
const columns: ProColumns<API.GetCustomerDTO>[] = [
|
|
{
|
|
title: 'ID',
|
|
dataIndex: 'id',
|
|
},
|
|
{
|
|
title: '原始 ID',
|
|
dataIndex: 'origin_id',
|
|
sorter: true,
|
|
},
|
|
{
|
|
title: '站点',
|
|
dataIndex: 'site_id',
|
|
valueType: 'select',
|
|
request: async () => {
|
|
try {
|
|
const { data, success } = await sitecontrollerAll();
|
|
if (success && data) {
|
|
return data.map((site: any) => ({
|
|
label: site.name,
|
|
value: site.id,
|
|
}));
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('获取站点列表失败:', error);
|
|
return [];
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: '头像',
|
|
dataIndex: 'avatar',
|
|
hideInSearch: true,
|
|
width: 60,
|
|
render: (_, record) => (
|
|
<Avatar
|
|
src={record.avatar}
|
|
size="small"
|
|
style={{ backgroundColor: '#1890ff' }}
|
|
>
|
|
{!record.avatar && record.fullname?.charAt(0)?.toUpperCase()}
|
|
</Avatar>
|
|
),
|
|
},
|
|
{
|
|
title: '姓名',
|
|
dataIndex: 'fullname',
|
|
sorter: true,
|
|
render: (_, record) => {
|
|
return (
|
|
record.fullname ||
|
|
`${record.first_name || ''} ${record.last_name || ''}`.trim() ||
|
|
record.username ||
|
|
'-'
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '用户名',
|
|
dataIndex: 'username',
|
|
copyable: true,
|
|
sorter: true,
|
|
},
|
|
{
|
|
title: '邮箱',
|
|
dataIndex: 'email',
|
|
copyable: true,
|
|
sorter: true,
|
|
},
|
|
{
|
|
title: '电话',
|
|
dataIndex: 'phone',
|
|
copyable: true,
|
|
},
|
|
{
|
|
title: '账单地址',
|
|
dataIndex: 'billing',
|
|
hideInSearch: true,
|
|
|
|
width: 200,
|
|
render: (billing) => <AddressCell address={billing} title="账单地址" />,
|
|
},
|
|
{
|
|
title: '物流地址',
|
|
dataIndex: 'shipping',
|
|
hideInSearch: true,
|
|
width: 200,
|
|
render: (shipping) => <AddressCell address={shipping} title="物流地址" />,
|
|
},
|
|
{
|
|
title: '评分',
|
|
dataIndex: 'rate',
|
|
hideInSearch: true,
|
|
sorter: true,
|
|
render: (_, record) => {
|
|
return (
|
|
<Rate
|
|
onChange={async (val) => {
|
|
try {
|
|
const { success, message: msg } =
|
|
await customercontrollerSetrate({
|
|
id: record.id,
|
|
rate: val,
|
|
});
|
|
if (success) {
|
|
message.success(msg);
|
|
actionRef.current?.reload();
|
|
}
|
|
} catch (e: any) {
|
|
message.error(e?.message || '设置评分失败');
|
|
}
|
|
}}
|
|
value={record.rate || 0}
|
|
allowHalf
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '标签',
|
|
dataIndex: 'tags',
|
|
hideInSearch: true,
|
|
render: (_, record) => {
|
|
const tags = record?.tags || [];
|
|
return (
|
|
<Space size={[0, 8]} wrap>
|
|
{tags.map((tag: string) => {
|
|
return (
|
|
<Tag
|
|
key={tag}
|
|
closable
|
|
onClose={async () => {
|
|
const { success, message: msg } =
|
|
await customercontrollerDeltag({
|
|
email: record.email,
|
|
tag,
|
|
});
|
|
if (!success) {
|
|
message.error(msg);
|
|
return false;
|
|
}
|
|
actionRef.current?.reload();
|
|
return true;
|
|
}}
|
|
style={{ marginBottom: 4 }}
|
|
>
|
|
{tag}
|
|
</Tag>
|
|
);
|
|
})}
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'site_created_at',
|
|
valueType: 'dateTime',
|
|
hideInSearch: true,
|
|
sorter: true,
|
|
width: 140,
|
|
},
|
|
{
|
|
title: '更新时间',
|
|
dataIndex: 'site_created_at',
|
|
valueType: 'dateTime',
|
|
hideInSearch: true,
|
|
sorter: true,
|
|
width: 140,
|
|
},
|
|
{
|
|
title: '操作',
|
|
dataIndex: 'option',
|
|
valueType: 'option',
|
|
fixed: 'right',
|
|
width: 120,
|
|
render: (_, record) => {
|
|
return (
|
|
<Space direction="vertical" size="small">
|
|
<AddTag
|
|
email={record.email || ''}
|
|
tags={record.raw?.tags || []}
|
|
tableRef={actionRef}
|
|
/>
|
|
{/* 订单 */}
|
|
<HistoryOrders customer={record} siteId={record.site_id} />
|
|
</Space>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer header={{ title: '客户列表' }}>
|
|
<ProTable
|
|
scroll={{ x: 'max-content' }}
|
|
headerTitle="查询表格"
|
|
actionRef={actionRef}
|
|
columns={columns}
|
|
rowKey="id"
|
|
request={async (params, sorter,filter) => {
|
|
console.log('custoemr request',params, sorter,filter)
|
|
const { current, pageSize, ...restParams } = params;
|
|
const orderBy:any = {}
|
|
Object.entries(sorter).forEach(([key, value]) => {
|
|
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
|
|
})
|
|
// 构建查询参数
|
|
const queryParams: any = {
|
|
page: current || 1,
|
|
per_page: pageSize || 20,
|
|
where: {
|
|
...filter,
|
|
...restParams
|
|
},
|
|
orderBy
|
|
};
|
|
|
|
const result = await customercontrollerGetcustomerlist({params: queryParams});
|
|
console.log(queryParams, result);
|
|
return {
|
|
total: result?.data?.total || 0,
|
|
data: result?.data?.items || [],
|
|
success: true,
|
|
};
|
|
}}
|
|
|
|
search={{
|
|
labelWidth: 'auto',
|
|
span: 6,
|
|
}}
|
|
pagination={{
|
|
pageSize: 20,
|
|
showSizeChanger: true,
|
|
showQuickJumper: true,
|
|
}}
|
|
toolBarRender={() => [
|
|
<Button
|
|
key="sync"
|
|
type="primary"
|
|
onClick={() => setSyncModalVisible(true)}
|
|
>
|
|
同步客户数据
|
|
</Button>,
|
|
// 这里可以添加导出、导入等功能按钮
|
|
]}
|
|
/>
|
|
<SyncCustomersModal
|
|
visible={syncModalVisible}
|
|
onClose={() => setSyncModalVisible(false)}
|
|
tableRef={actionRef}
|
|
/>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
const AddTag: React.FC<{
|
|
email: string;
|
|
tags?: string[];
|
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
}> = ({ email, tags, tableRef }) => {
|
|
const { message } = App.useApp();
|
|
const [tagList, setTagList] = useState<string[]>([]);
|
|
|
|
return (
|
|
<ModalForm
|
|
title={`修改标签 - ${email}`}
|
|
trigger={
|
|
<Button type="link" size="small">
|
|
修改标签
|
|
</Button>
|
|
}
|
|
width={800}
|
|
modalProps={{
|
|
destroyOnHidden: true,
|
|
}}
|
|
submitter={false}
|
|
>
|
|
<ProFormSelect
|
|
mode="tags"
|
|
allowClear
|
|
name="tag"
|
|
label="标签"
|
|
request={async () => {
|
|
const { data, success } = await customercontrollerGettags();
|
|
if (!success) return [];
|
|
setTagList(tags || []);
|
|
return data
|
|
.filter((tag: string) => {
|
|
return !(tags || []).includes(tag);
|
|
})
|
|
.map((tag: string) => ({ label: tag, value: tag }));
|
|
}}
|
|
fieldProps={{
|
|
value: tagList, // 当前值
|
|
onChange: async (newValue) => {
|
|
const added = newValue.filter((x) => !(tags || []).includes(x));
|
|
const removed = (tags || []).filter((x) => !newValue.includes(x));
|
|
|
|
if (added.length) {
|
|
const { success, message: msg } = await customercontrollerAddtag({
|
|
email,
|
|
tag: added[0],
|
|
});
|
|
if (!success) {
|
|
message.error(msg);
|
|
return;
|
|
}
|
|
}
|
|
if (removed.length) {
|
|
const { success, message: msg } = await customercontrollerDeltag({
|
|
email,
|
|
tag: removed[0],
|
|
});
|
|
if (!success) {
|
|
message.error(msg);
|
|
return;
|
|
}
|
|
}
|
|
tableRef?.current?.reload();
|
|
setTagList(newValue);
|
|
},
|
|
}}
|
|
></ProFormSelect>
|
|
</ModalForm>
|
|
);
|
|
};
|
|
|
|
const SyncCustomersModal: React.FC<{
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
|
}> = ({ visible, onClose, tableRef }) => {
|
|
const { message } = App.useApp();
|
|
const [sites, setSites] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [form] = Form.useForm(); // 添加表单实例
|
|
|
|
// 获取站点列表
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setLoading(true);
|
|
sitecontrollerAll()
|
|
.then((res: any) => {
|
|
setSites(res?.data || []);
|
|
})
|
|
.catch((error: any) => {
|
|
message.error('获取站点列表失败: ' + (error.message || '未知错误'));
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
}, [visible]);
|
|
|
|
// 定义同步参数类型
|
|
type SyncParams = {
|
|
siteId: number;
|
|
search?: string;
|
|
role?: string;
|
|
dateRange?: [string, string];
|
|
orderBy?: string;
|
|
};
|
|
|
|
const handleSync = async (values: SyncParams) => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// 构建过滤参数
|
|
const params: any = {};
|
|
|
|
// 添加搜索关键词
|
|
if (values.search) {
|
|
params.search = values.search;
|
|
}
|
|
|
|
// 添加角色过滤
|
|
if (values.role) {
|
|
params.where = {
|
|
...params.where,
|
|
role: values.role,
|
|
};
|
|
}
|
|
|
|
// 添加日期范围过滤(使用 after 和 before 参数)
|
|
if (values.dateRange && values.dateRange[0] && values.dateRange[1]) {
|
|
params.where = {
|
|
...params.where,
|
|
after: values.dateRange[0],
|
|
before: values.dateRange[1],
|
|
};
|
|
}
|
|
|
|
// 添加排序
|
|
if (values.orderBy) {
|
|
params.orderBy = values.orderBy;
|
|
}
|
|
|
|
const {
|
|
success,
|
|
message: msg,
|
|
data,
|
|
} = await customercontrollerSynccustomers({
|
|
siteId: values.siteId,
|
|
params: Object.keys(params).length > 0 ? params : undefined,
|
|
});
|
|
|
|
if (success) {
|
|
// 显示详细的同步结果
|
|
const result = data || {};
|
|
const {
|
|
total = 0,
|
|
synced = 0,
|
|
created = 0,
|
|
updated = 0,
|
|
errors = [],
|
|
} = result;
|
|
|
|
let resultMessage = `同步完成!共处理 ${total} 个客户:`;
|
|
if (created > 0) resultMessage += ` 新建 ${created} 个`;
|
|
if (updated > 0) resultMessage += ` 更新 ${updated} 个`;
|
|
if (synced > 0) resultMessage += ` 同步成功 ${synced} 个`;
|
|
if (errors.length > 0) resultMessage += ` 失败 ${errors.length} 个`;
|
|
|
|
if (errors.length > 0) {
|
|
// 如果有错误,显示警告消息
|
|
message.warning({
|
|
content: (
|
|
<div>
|
|
<div>{resultMessage}</div>
|
|
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
|
|
失败详情:
|
|
{errors
|
|
.slice(0, 3)
|
|
.map((err: any) => err.email || err.error)
|
|
.join(', ')}
|
|
{errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`}
|
|
</div>
|
|
</div>
|
|
),
|
|
duration: 8,
|
|
key: 'sync-result',
|
|
});
|
|
} else {
|
|
// 完全成功
|
|
message.success({
|
|
content: resultMessage,
|
|
duration: 4,
|
|
key: 'sync-result',
|
|
});
|
|
}
|
|
|
|
onClose();
|
|
// 刷新表格数据
|
|
tableRef.current?.reload();
|
|
return true;
|
|
} else {
|
|
message.error(msg || '同步失败');
|
|
return false;
|
|
}
|
|
} catch (error: any) {
|
|
message.error('同步失败: ' + (error.message || '未知错误'));
|
|
return false;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ModalForm
|
|
title="同步客户数据"
|
|
open={visible}
|
|
onOpenChange={(open) => !open && onClose()}
|
|
modalProps={{
|
|
destroyOnClose: true,
|
|
confirmLoading: loading,
|
|
}}
|
|
onFinish={handleSync}
|
|
form={form}
|
|
>
|
|
<ProFormSelect
|
|
name="siteId"
|
|
label="选择站点"
|
|
placeholder="请选择要同步的站点"
|
|
options={sites.map((site) => ({
|
|
label: site.name,
|
|
value: site.id,
|
|
}))}
|
|
rules={[{ required: true, message: '请选择站点' }]}
|
|
fieldProps={{
|
|
loading: loading,
|
|
}}
|
|
/>
|
|
<ProFormText
|
|
name="search"
|
|
label="搜索关键词"
|
|
placeholder="输入邮箱、姓名或用户名进行搜索"
|
|
tooltip="支持搜索邮箱、姓名、用户名等字段"
|
|
/>
|
|
<ProFormSelect
|
|
name="role"
|
|
label="客户角色"
|
|
placeholder="选择客户角色进行过滤"
|
|
options={[
|
|
{ label: '所有角色', value: '' },
|
|
{ label: '管理员', value: 'administrator' },
|
|
{ label: '编辑', value: 'editor' },
|
|
{ label: '作者', value: 'author' },
|
|
{ label: '订阅者', value: 'subscriber' },
|
|
{ label: '客户', value: 'customer' },
|
|
]}
|
|
fieldProps={{
|
|
allowClear: true,
|
|
}}
|
|
/>
|
|
<ProFormDateTimeRangePicker
|
|
name="dateRange"
|
|
label="注册日期范围"
|
|
placeholder={['开始日期', '结束日期']}
|
|
transform={(value) => {
|
|
return {
|
|
dateRange: value,
|
|
};
|
|
}}
|
|
fieldProps={{
|
|
showTime: false,
|
|
style: { width: '100%' },
|
|
}}
|
|
/>
|
|
<ProFormSelect
|
|
name="orderBy"
|
|
label="排序方式"
|
|
placeholder="选择排序方式"
|
|
options={[
|
|
{ label: '默认排序', value: '' },
|
|
{ label: '注册时间(升序)', value: 'date_created:asc' },
|
|
{ label: '注册时间(降序)', value: 'date_created:desc' },
|
|
{ label: '邮箱(升序)', value: 'email:asc' },
|
|
{ label: '邮箱(降序)', value: 'email:desc' },
|
|
{ label: '姓名(升序)', value: 'first_name:asc' },
|
|
{ label: '姓名(降序)', value: 'first_name:desc' },
|
|
]}
|
|
fieldProps={{
|
|
allowClear: true,
|
|
}}
|
|
/>
|
|
</ModalForm>
|
|
);
|
|
};
|
|
|
|
export { AddTag };
|
|
|
|
export default CustomerList;
|