forked from yoone/WEB
701 lines
18 KiB
TypeScript
701 lines
18 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 [sites, setSites] = useState<any[]>([]); // 添加站点数据状态
|
||
|
||
// 获取站点数据
|
||
const fetchSites = async () => {
|
||
try {
|
||
const { data, success } = await sitecontrollerAll();
|
||
if (success) {
|
||
setSites(data || []);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取站点数据失败:', error);
|
||
}
|
||
};
|
||
|
||
// 根据站点ID获取站点名称
|
||
const getSiteName = (siteId: number | undefined | null) => {
|
||
if (!siteId) return '-';
|
||
if (typeof siteId === 'string') {
|
||
return siteId;
|
||
}
|
||
const site = sites.find((s) => s.id === siteId);
|
||
return site ? site.name : String(siteId);
|
||
};
|
||
|
||
// 组件加载时获取站点数据
|
||
useEffect(() => {
|
||
fetchSites();
|
||
}, []);
|
||
|
||
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',
|
||
hideInSearch: 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',
|
||
width: 120,
|
||
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}
|
||
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,
|
||
};
|
||
}}
|
||
columns={columns}
|
||
search={{
|
||
labelWidth: 'auto',
|
||
span: 6,
|
||
}}
|
||
pagination={{
|
||
pageSize: 20,
|
||
showSizeChanger: true,
|
||
showTotal: (total, range) =>
|
||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||
}}
|
||
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;
|