forked from yoone/WEB
532 lines
16 KiB
TypeScript
532 lines
16 KiB
TypeScript
import {
|
||
DeleteFilled,
|
||
EditOutlined,
|
||
PlusOutlined,
|
||
UserOutlined,
|
||
} from '@ant-design/icons';
|
||
import {
|
||
ActionType,
|
||
DrawerForm,
|
||
ModalForm,
|
||
PageContainer,
|
||
ProColumns,
|
||
ProFormText,
|
||
ProFormTextArea,
|
||
ProTable,
|
||
} from '@ant-design/pro-components';
|
||
import { request, useParams } from '@umijs/max';
|
||
import { App, Avatar, Button, Modal, Popconfirm, Space, Tag } from 'antd';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
|
||
const BatchEditCustomers: React.FC<{
|
||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||
selectedRowKeys: React.Key[];
|
||
setSelectedRowKeys: (keys: React.Key[]) => void;
|
||
siteId?: string;
|
||
}> = ({ tableRef, selectedRowKeys, setSelectedRowKeys, siteId }) => {
|
||
const { message } = App.useApp();
|
||
return (
|
||
<ModalForm
|
||
title="批量编辑客户"
|
||
trigger={
|
||
<Button
|
||
disabled={!selectedRowKeys.length}
|
||
type="primary"
|
||
icon={<EditOutlined />}
|
||
>
|
||
批量编辑
|
||
</Button>
|
||
}
|
||
width={400}
|
||
modalProps={{ destroyOnHidden: true }}
|
||
onFinish={async (values) => {
|
||
if (!siteId) return false;
|
||
let ok = 0,
|
||
fail = 0;
|
||
for (const id of selectedRowKeys) {
|
||
try {
|
||
// Remove undefined values
|
||
const data = Object.fromEntries(
|
||
Object.entries(values).filter(
|
||
([_, v]) => v !== undefined && v !== '',
|
||
),
|
||
);
|
||
if (Object.keys(data).length === 0) continue;
|
||
|
||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
||
method: 'PUT',
|
||
data: data,
|
||
});
|
||
if (res.success) ok++;
|
||
else fail++;
|
||
} catch (e) {
|
||
fail++;
|
||
}
|
||
}
|
||
message.success(`成功 ${ok}, 失败 ${fail}`);
|
||
tableRef.current?.reload();
|
||
setSelectedRowKeys([]);
|
||
return true;
|
||
}}
|
||
>
|
||
<ProFormText
|
||
name="role"
|
||
label="角色"
|
||
placeholder="请输入角色,不修改请留空"
|
||
/>
|
||
<ProFormText
|
||
name="phone"
|
||
label="电话"
|
||
placeholder="请输入电话,不修改请留空"
|
||
/>
|
||
</ModalForm>
|
||
);
|
||
};
|
||
|
||
const CustomerPage: React.FC = () => {
|
||
const { message } = App.useApp();
|
||
const { siteId } = useParams<{ siteId: string }>();
|
||
const [editing, setEditing] = useState<any>(null);
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||
const actionRef = useRef<ActionType>();
|
||
const [ordersVisible, setOrdersVisible] = useState<boolean>(false);
|
||
const [ordersCustomer, setOrdersCustomer] = useState<any>(null);
|
||
|
||
useEffect(() => {
|
||
// 当siteId变化时, 重新加载表格数据
|
||
if (siteId) {
|
||
actionRef.current?.reload();
|
||
}
|
||
}, [siteId]);
|
||
|
||
const handleDelete = async (id: number) => {
|
||
if (!siteId) return;
|
||
try {
|
||
const res = await request(`/site-api/${siteId}/customers/${id}`, {
|
||
method: 'DELETE',
|
||
});
|
||
if (res.success) {
|
||
message.success('删除成功');
|
||
actionRef.current?.reload();
|
||
} else {
|
||
message.error(res.message || '删除失败');
|
||
}
|
||
} catch (e) {
|
||
message.error('删除失败');
|
||
}
|
||
};
|
||
|
||
const columns: ProColumns<any>[] = [
|
||
{
|
||
title: '头像',
|
||
dataIndex: 'avatar_url',
|
||
hideInSearch: true,
|
||
width: 80,
|
||
render: (_, record) => {
|
||
// 从raw数据中获取头像URL,因为DTO中没有这个字段
|
||
const avatarUrl = record.raw?.avatar_url || record.avatar_url;
|
||
return <Avatar src={avatarUrl} icon={<UserOutlined />} size="large" />;
|
||
},
|
||
},
|
||
{
|
||
title: '姓名',
|
||
dataIndex: 'name',
|
||
hideInTable: true,
|
||
},
|
||
{
|
||
title: 'ID',
|
||
dataIndex: 'id',
|
||
hideInSearch: true,
|
||
width: 120,
|
||
copyable: true,
|
||
render: (_, record) => {
|
||
return record?.id ?? '-';
|
||
},
|
||
},
|
||
{
|
||
title: '姓名',
|
||
dataIndex: 'username',
|
||
hideInSearch: true,
|
||
render: (_, record) => {
|
||
// DTO中有first_name和last_name字段,username可能从raw数据中获取
|
||
const username = record.username || record.raw?.username || 'N/A';
|
||
return (
|
||
<div>
|
||
<div>{username}</div>
|
||
<div style={{ fontSize: 12, color: '#888' }}>
|
||
{record.first_name} {record.last_name}
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '邮箱',
|
||
dataIndex: 'email',
|
||
copyable: true,
|
||
},
|
||
{
|
||
title: '电话',
|
||
dataIndex: 'phone',
|
||
render: (_, record) =>
|
||
record.phone || record.billing?.phone || record.shipping?.phone || '-',
|
||
copyable: true,
|
||
},
|
||
{
|
||
title: '角色',
|
||
dataIndex: 'role',
|
||
render: (_, record) => {
|
||
// 角色信息可能从raw数据中获取,因为DTO中没有这个字段
|
||
const role = record.role || record.raw?.role || 'N/A';
|
||
return <Tag color="blue">{role}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '账单地址',
|
||
dataIndex: 'billing',
|
||
hideInSearch: true,
|
||
render: (_, record) => {
|
||
const { billing } = record;
|
||
if (!billing) return '-';
|
||
return (
|
||
<div style={{ fontSize: 12 }}>
|
||
<div>
|
||
{billing.address_1} {billing.address_2}
|
||
</div>
|
||
<div>
|
||
{billing.city}, {billing.state}, {billing.postcode}
|
||
</div>
|
||
<div>{billing.country}</div>
|
||
<div>{billing.phone}</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '注册时间',
|
||
dataIndex: 'date_created',
|
||
valueType: 'dateTime',
|
||
hideInSearch: true,
|
||
},
|
||
{
|
||
title: '操作',
|
||
valueType: 'option',
|
||
width: 120,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
title="编辑"
|
||
icon={<EditOutlined />}
|
||
onClick={() => setEditing(record)}
|
||
/>
|
||
<Popconfirm
|
||
title="确定删除?"
|
||
onConfirm={() => handleDelete(record.id)}
|
||
>
|
||
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
||
</Popconfirm>
|
||
<Button
|
||
type="link"
|
||
title="查询订单"
|
||
onClick={() => {
|
||
setOrdersCustomer(record);
|
||
setOrdersVisible(true);
|
||
}}
|
||
>
|
||
查询订单
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<PageContainer
|
||
ghost
|
||
header={{
|
||
title: null,
|
||
breadcrumb: undefined,
|
||
}}
|
||
>
|
||
<ProTable
|
||
rowKey="id"
|
||
columns={columns}
|
||
search={{ labelWidth: 'auto' }}
|
||
options={{ reload: true }}
|
||
actionRef={actionRef}
|
||
scroll={{ x: 'max-content' }}
|
||
rowSelection={{
|
||
selectedRowKeys,
|
||
onChange: setSelectedRowKeys,
|
||
}}
|
||
request={async (params, sort, filter) => {
|
||
if (!siteId) return { data: [], total: 0, success: true };
|
||
const { current, pageSize, name, email, ...rest } = params || {};
|
||
const where = { ...rest, ...(filter || {}) };
|
||
if (email) {
|
||
(where as any).email = email;
|
||
}
|
||
let orderObj: Record<string, 'asc' | 'desc'> | undefined = undefined;
|
||
if (sort && typeof sort === 'object') {
|
||
const [field, dir] = Object.entries(sort)[0] || [];
|
||
if (field && dir) {
|
||
orderObj = { [field]: dir === 'descend' ? 'desc' : 'asc' };
|
||
}
|
||
}
|
||
const response = await request(`/site-api/${siteId}/customers`, {
|
||
params: {
|
||
page: current,
|
||
page_size: pageSize,
|
||
where,
|
||
...(orderObj ? { order: orderObj } : {}),
|
||
...(name || email ? { search: name || email } : {}),
|
||
},
|
||
});
|
||
|
||
if (!response.success) {
|
||
message.error(response.message || '获取客户列表失败');
|
||
return {
|
||
data: [],
|
||
total: 0,
|
||
success: false,
|
||
};
|
||
}
|
||
|
||
const data = response.data;
|
||
return {
|
||
total: data?.total || 0,
|
||
data: data?.items || [],
|
||
success: true,
|
||
};
|
||
}}
|
||
toolBarRender={() => [
|
||
<DrawerForm
|
||
title="新增客户"
|
||
trigger={
|
||
<Button type="primary" title="新增" icon={<PlusOutlined />} />
|
||
}
|
||
onFinish={async (values) => {
|
||
if (!siteId) return false;
|
||
const res = await request(`/site-api/${siteId}/customers`, {
|
||
method: 'POST',
|
||
data: values,
|
||
});
|
||
if (res.success) {
|
||
message.success('新增成功');
|
||
actionRef.current?.reload();
|
||
return true;
|
||
}
|
||
message.error(res.message || '新增失败');
|
||
return false;
|
||
}}
|
||
>
|
||
<ProFormText
|
||
name="email"
|
||
label="邮箱"
|
||
rules={[{ required: true }]}
|
||
/>
|
||
<ProFormText name="first_name" label="名" />
|
||
<ProFormText name="last_name" label="姓" />
|
||
<ProFormText name="username" label="用户名" />
|
||
<ProFormText name="phone" label="电话" />
|
||
</DrawerForm>,
|
||
<BatchEditCustomers
|
||
tableRef={actionRef}
|
||
selectedRowKeys={selectedRowKeys}
|
||
setSelectedRowKeys={setSelectedRowKeys}
|
||
siteId={siteId}
|
||
/>,
|
||
<Button
|
||
title="批量导出"
|
||
onClick={async () => {
|
||
if (!siteId) return;
|
||
const idsParam = selectedRowKeys.length
|
||
? (selectedRowKeys as any[]).join(',')
|
||
: undefined;
|
||
const res = await request(
|
||
`/site-api/${siteId}/customers/export`,
|
||
{ params: { ids: idsParam } },
|
||
);
|
||
if (res?.success && res?.data?.csv) {
|
||
const blob = new Blob([res.data.csv], {
|
||
type: 'text/csv;charset=utf-8;',
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'customers.csv';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
} else {
|
||
message.error(res.message || '导出失败');
|
||
}
|
||
}}
|
||
>
|
||
批量导出
|
||
</Button>,
|
||
<ModalForm
|
||
title="批量导入客户"
|
||
trigger={
|
||
<Button type="primary" ghost>
|
||
批量导入
|
||
</Button>
|
||
}
|
||
width={600}
|
||
modalProps={{ destroyOnHidden: true }}
|
||
onFinish={async (values) => {
|
||
if (!siteId) return false;
|
||
const csv = values.csv || '';
|
||
const items = values.items || [];
|
||
const res = await request(
|
||
`/site-api/${siteId}/customers/import`,
|
||
{ method: 'POST', data: { csv, items } },
|
||
);
|
||
if (res.success) {
|
||
message.success('导入完成');
|
||
actionRef.current?.reload();
|
||
return true;
|
||
}
|
||
message.error(res.message || '导入失败');
|
||
return false;
|
||
}}
|
||
>
|
||
<ProFormTextArea
|
||
name="csv"
|
||
label="CSV文本"
|
||
placeholder="粘贴CSV,首行为表头"
|
||
/>
|
||
</ModalForm>,
|
||
|
||
<Button
|
||
title="批量删除"
|
||
danger
|
||
icon={<DeleteFilled />}
|
||
onClick={async () => {
|
||
if (!siteId) return;
|
||
const res = await request(`/site-api/${siteId}/customers/batch`, {
|
||
method: 'POST',
|
||
data: { delete: selectedRowKeys },
|
||
});
|
||
actionRef.current?.reload();
|
||
setSelectedRowKeys([]);
|
||
if (res.success) {
|
||
message.success('批量删除成功');
|
||
} else {
|
||
message.warning(res.message || '部分删除失败');
|
||
}
|
||
}}
|
||
/>,
|
||
]}
|
||
/>
|
||
|
||
<DrawerForm
|
||
title="编辑客户"
|
||
open={!!editing}
|
||
onOpenChange={(visible) => !visible && setEditing(null)}
|
||
initialValues={editing || {}}
|
||
onFinish={async (values) => {
|
||
if (!siteId || !editing) return false;
|
||
const res = await request(
|
||
`/site-api/${siteId}/customers/${editing.id}`,
|
||
{ method: 'PUT', data: values },
|
||
);
|
||
if (res.success) {
|
||
message.success('更新成功');
|
||
actionRef.current?.reload();
|
||
setEditing(null);
|
||
return true;
|
||
}
|
||
message.error(res.message || '更新失败');
|
||
return false;
|
||
}}
|
||
>
|
||
<ProFormText name="email" label="邮箱" rules={[{ required: true }]} />
|
||
<ProFormText name="first_name" label="名" />
|
||
<ProFormText name="last_name" label="姓" />
|
||
<ProFormText name="username" label="用户名" />
|
||
<ProFormText name="phone" label="电话" />
|
||
</DrawerForm>
|
||
<Modal
|
||
open={ordersVisible}
|
||
onCancel={() => {
|
||
setOrdersVisible(false);
|
||
setOrdersCustomer(null);
|
||
}}
|
||
footer={null}
|
||
width={1000}
|
||
title="客户订单"
|
||
destroyOnClose
|
||
>
|
||
<ProTable
|
||
rowKey="id"
|
||
search={false}
|
||
pagination={{ pageSize: 20 }}
|
||
columns={[
|
||
{ title: '订单号', dataIndex: 'number', copyable: true },
|
||
{
|
||
title: '客户邮箱',
|
||
dataIndex: 'email',
|
||
copyable: true,
|
||
render: () => {
|
||
return ordersCustomer?.email;
|
||
},
|
||
},
|
||
{
|
||
title: '支付时间',
|
||
dataIndex: 'date_paid',
|
||
valueType: 'dateTime',
|
||
hideInSearch: true,
|
||
},
|
||
{ title: '订单金额', dataIndex: 'total', hideInSearch: true },
|
||
{ title: '状态', dataIndex: 'status', hideInSearch: true },
|
||
{ title: '来源', dataIndex: 'created_via', hideInSearch: true },
|
||
{
|
||
title: '订单内容',
|
||
dataIndex: 'line_items',
|
||
hideInSearch: true,
|
||
render: (_, record) => {
|
||
return (
|
||
<div>
|
||
{record.line_items?.map((item: any) => (
|
||
<div key={item.id}>
|
||
{item.name} x {item.quantity}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
]}
|
||
request={async (params) => {
|
||
if (!siteId || !ordersCustomer?.id)
|
||
return { data: [], total: 0, success: true };
|
||
const res = await request(
|
||
`/site-api/${siteId}/customers/${ordersCustomer.id}/orders`,
|
||
{
|
||
params: {
|
||
page: params.current,
|
||
per_page: params.pageSize,
|
||
},
|
||
},
|
||
);
|
||
if (!res?.success) {
|
||
message.error(res?.message || '获取订单失败');
|
||
return { data: [], total: 0, success: false };
|
||
}
|
||
const data = res.data || {};
|
||
return {
|
||
data: data.items || [],
|
||
total: data.total || 0,
|
||
success: true,
|
||
};
|
||
}}
|
||
/>
|
||
</Modal>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default CustomerPage;
|