WEB/src/pages/Site/Shop/Customers/index.tsx

535 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Address from '@/components/Address';
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;
return <Address address={billing} />;
},
},
{
title: '物流地址',
dataIndex: 'shipping',
hideInSearch: true,
render: (shipping) => {
return <Address address={shipping} />;
},
},
{
title: '创建时间',
dataIndex: 'date_created',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '更新时间',
dataIndex: 'date_modified',
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,
per_page: 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 ,showSizeChanger: true, showQuickJumper: true,}}
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;