feat(客户): 新增客户数据分析列表页面
refactor(产品): 优化属性字典项获取逻辑,增加错误处理 fix(订单): 修复取消发货按钮在已完成订单中显示的问题 style: 统一代码格式,修复缩进和导入顺序问题 perf(字典): 优化字典配置加载逻辑,增加重试机制 docs(API): 更新API类型定义,添加客户统计相关接口 chore: 更新package.json文件格式
This commit is contained in:
parent
39a64d3714
commit
8524cc1ec0
|
|
@ -131,6 +131,11 @@ export default defineConfig({
|
|||
path: '/customer/list',
|
||||
component: './Customer/List',
|
||||
},
|
||||
{
|
||||
name: '数据分析列表',
|
||||
path: '/customer/statistic',
|
||||
component: './Customer/Statistic',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -114,7 +114,9 @@ const CategoryPage: React.FC = () => {
|
|||
// Fetch all dicts and filter those that are allowed attributes
|
||||
try {
|
||||
const res = await request('/dict/list');
|
||||
const filtered = (res || []).filter((d: any) => !notAttributes.has(d.name));
|
||||
const filtered = (res || []).filter(
|
||||
(d: any) => !notAttributes.has(d.name),
|
||||
);
|
||||
// Filter out already added attributes
|
||||
const existingDictIds = new Set(
|
||||
categoryAttributes.map((ca: any) => ca.dict.id),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
||||
import {
|
||||
customercontrollerAddtag,
|
||||
customercontrollerDeltag,
|
||||
customercontrollerGetcustomerlist,
|
||||
customercontrollerGettags,
|
||||
customercontrollerSetrate,
|
||||
customercontrollerSynccustomers,
|
||||
} from '@/servers/api/customer';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import {
|
||||
ActionType,
|
||||
ModalForm,
|
||||
|
|
@ -14,97 +15,219 @@ import {
|
|||
ProFormSelect,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Rate, Space, Tag } from 'antd';
|
||||
import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
// 地址格式化函数
|
||||
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 columns: ProColumns[] = [
|
||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||
const [syncLoading, setSyncLoading] = 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 '-';
|
||||
const site = sites.find(s => s.id === siteId);
|
||||
console.log(`site`,site)
|
||||
return site ? site.name : String(siteId);
|
||||
};
|
||||
|
||||
// 组件加载时获取站点数据
|
||||
useEffect(() => {
|
||||
fetchSites();
|
||||
}, []);
|
||||
|
||||
const columns: ProColumns<API.UnifiedCustomerDTO>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
hideInSearch: 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 [];
|
||||
}
|
||||
},
|
||||
render: (siteId, record) => {
|
||||
return siteId
|
||||
return getSiteName(record.site_id) || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
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.firstName || ''} ${record.lastName || ''}`.trim() ||
|
||||
record.username ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
if (record.billing.first_name || record.billing.last_name)
|
||||
return record.billing.first_name + ' ' + record.billing.last_name;
|
||||
return record.shipping.first_name + ' ' + record.shipping.last_name;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
title: '客户编号',
|
||||
dataIndex: 'customerId',
|
||||
render: (_, record) => {
|
||||
if (!record.customerId) return '-';
|
||||
return String(record.customerId).padStart(6, 0);
|
||||
},
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '首单时间',
|
||||
dataIndex: 'first_purchase_date',
|
||||
valueType: 'dateMonth',
|
||||
sorter: true,
|
||||
render: (_, record) =>
|
||||
record.first_purchase_date
|
||||
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-',
|
||||
// search: {
|
||||
// transform: (value: string) => {
|
||||
// return { month: value };
|
||||
// },
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '尾单时间',
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
hideInSearch: true,
|
||||
dataIndex: 'last_purchase_date',
|
||||
valueType: 'dateTime',
|
||||
sorter: true,
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
title: '订单数',
|
||||
dataIndex: 'orders',
|
||||
title: '账单地址',
|
||||
dataIndex: 'billing',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'total',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: 'YOONE订单数',
|
||||
dataIndex: 'yoone_orders',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: 'YOONE金额',
|
||||
dataIndex: 'yoone_total',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
if (!record.yoone_orders || !record.yoone_total) return '-';
|
||||
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
|
||||
return 'B';
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '评星',
|
||||
dataIndex: 'rate',
|
||||
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,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Rate
|
||||
|
|
@ -112,46 +235,31 @@ const ListPage: React.FC = () => {
|
|||
try {
|
||||
const { success, message: msg } =
|
||||
await customercontrollerSetrate({
|
||||
id: record.customerId,
|
||||
id: record.id,
|
||||
rate: val,
|
||||
});
|
||||
if (success) {
|
||||
message.success(msg);
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '设置评分失败');
|
||||
}
|
||||
}}
|
||||
value={record.rate}
|
||||
allowHalf
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'phone',
|
||||
dataIndex: 'phone',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.phone || record?.shipping.phone,
|
||||
},
|
||||
{
|
||||
title: 'state',
|
||||
dataIndex: 'state',
|
||||
render: (_, record) => record?.billing.state || record?.shipping.state,
|
||||
},
|
||||
{
|
||||
title: 'city',
|
||||
dataIndex: 'city',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.city || record?.shipping.city,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Space>
|
||||
{(record.tags || []).map((tag) => {
|
||||
<Space size={[0, 8]} wrap>
|
||||
{(record.tags || []).map((tag: string) => {
|
||||
return (
|
||||
<Tag
|
||||
key={tag}
|
||||
|
|
@ -162,8 +270,14 @@ const ListPage: React.FC = () => {
|
|||
email: record.email,
|
||||
tag,
|
||||
});
|
||||
if (!success) {
|
||||
message.error(msg);
|
||||
return false;
|
||||
}
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}}
|
||||
style={{ marginBottom: 4 }}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
|
|
@ -173,31 +287,55 @@ const ListPage: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
<Space direction="vertical" size="small">
|
||||
<AddTag
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
<HistoryOrder
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
// 这里可以添加查看客户详情的逻辑
|
||||
message.info('客户详情功能开发中...');
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer ghost>
|
||||
<PageContainer header={{ title: '客户列表' }}>
|
||||
<ProTable
|
||||
scroll={{ x: 'max-content' }}
|
||||
headerTitle="查询表格"
|
||||
|
|
@ -207,7 +345,9 @@ const ListPage: React.FC = () => {
|
|||
const key = Object.keys(sorter)[0];
|
||||
const { data, success } = await customercontrollerGetcustomerlist({
|
||||
...params,
|
||||
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
|
||||
current: params.current?.toString(),
|
||||
pageSize: params.pageSize?.toString(),
|
||||
...(key ? { sorterKey: key, sorterValue: sorter[key] as string } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -217,12 +357,37 @@ const ListPage: React.FC = () => {
|
|||
};
|
||||
}}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddTag: React.FC<{
|
||||
const AddTag: React.FC<{
|
||||
email: string;
|
||||
tags?: string[];
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
|
|
@ -233,7 +398,11 @@ export const AddTag: React.FC<{
|
|||
return (
|
||||
<ModalForm
|
||||
title={`修改标签 - ${email}`}
|
||||
trigger={<Button>修改标签</Button>}
|
||||
trigger={
|
||||
<Button type="link" size="small">
|
||||
修改标签
|
||||
</Button>
|
||||
}
|
||||
width={800}
|
||||
modalProps={{
|
||||
destroyOnHidden: true,
|
||||
|
|
@ -250,16 +419,16 @@ export const AddTag: React.FC<{
|
|||
if (!success) return [];
|
||||
setTagList(tags || []);
|
||||
return data
|
||||
.filter((tag) => {
|
||||
.filter((tag: string) => {
|
||||
return !(tags || []).includes(tag);
|
||||
})
|
||||
.map((tag) => ({ label: tag, value: tag }));
|
||||
.map((tag: string) => ({ label: tag, value: tag }));
|
||||
}}
|
||||
fieldProps={{
|
||||
value: tagList, // 当前值
|
||||
onChange: async (newValue) => {
|
||||
const added = newValue.filter((x) => !tagList.includes(x));
|
||||
const removed = tagList.filter((x) => !newValue.includes(x));
|
||||
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({
|
||||
|
|
@ -282,7 +451,6 @@ export const AddTag: React.FC<{
|
|||
}
|
||||
}
|
||||
tableRef?.current?.reload();
|
||||
|
||||
setTagList(newValue);
|
||||
},
|
||||
}}
|
||||
|
|
@ -291,4 +459,124 @@ export const AddTag: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
export default ListPage;
|
||||
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);
|
||||
|
||||
// 获取站点列表
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setLoading(true);
|
||||
sitecontrollerAll()
|
||||
.then((res: any) => {
|
||||
setSites(res?.data || []);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
message.error('获取站点列表失败: ' + (error.message || '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleSync = async (values: { siteId: number }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { success, message: msg, data } = await customercontrollerSynccustomers({
|
||||
siteId: values.siteId,
|
||||
});
|
||||
|
||||
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}
|
||||
>
|
||||
<ProFormSelect
|
||||
name="siteId"
|
||||
label="选择站点"
|
||||
placeholder="请选择要同步的站点"
|
||||
options={sites.map((site) => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}))}
|
||||
rules={[{ required: true, message: '请选择站点' }]}
|
||||
fieldProps={{
|
||||
loading: loading,
|
||||
}}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export { AddTag };
|
||||
|
||||
export default CustomerList;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
||||
import {
|
||||
customercontrollerAddtag,
|
||||
customercontrollerDeltag,
|
||||
customercontrollerGetcustomerlist,
|
||||
customercontrollerGettags,
|
||||
customercontrollerSetrate,
|
||||
} from '@/servers/api/customer';
|
||||
import {
|
||||
ActionType,
|
||||
ModalForm,
|
||||
PageContainer,
|
||||
ProColumns,
|
||||
ProFormSelect,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Rate, Space, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
if (record.billing.first_name || record.billing.last_name)
|
||||
return record.billing.first_name + ' ' + record.billing.last_name;
|
||||
return record.shipping.first_name + ' ' + record.shipping.last_name;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
},
|
||||
{
|
||||
title: '客户编号',
|
||||
dataIndex: 'customerId',
|
||||
render: (_, record) => {
|
||||
if (!record.customerId) return '-';
|
||||
return String(record.customerId).padStart(6, 0);
|
||||
},
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '首单时间',
|
||||
dataIndex: 'first_purchase_date',
|
||||
valueType: 'dateMonth',
|
||||
sorter: true,
|
||||
render: (_, record) =>
|
||||
record.first_purchase_date
|
||||
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-',
|
||||
// search: {
|
||||
// transform: (value: string) => {
|
||||
// return { month: value };
|
||||
// },
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '尾单时间',
|
||||
hideInSearch: true,
|
||||
dataIndex: 'last_purchase_date',
|
||||
valueType: 'dateTime',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '订单数',
|
||||
dataIndex: 'orders',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'total',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: 'YOONE订单数',
|
||||
dataIndex: 'yoone_orders',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: 'YOONE金额',
|
||||
dataIndex: 'yoone_total',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
if (!record.yoone_orders || !record.yoone_total) return '-';
|
||||
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
|
||||
return 'B';
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '评星',
|
||||
dataIndex: 'rate',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Rate
|
||||
onChange={async (val) => {
|
||||
try {
|
||||
const { success, message: msg } =
|
||||
await customercontrollerSetrate({
|
||||
id: record.customerId,
|
||||
rate: val,
|
||||
});
|
||||
if (success) {
|
||||
message.success(msg);
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(e.message);
|
||||
}
|
||||
}}
|
||||
value={record.rate}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'phone',
|
||||
dataIndex: 'phone',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.phone || record?.shipping.phone,
|
||||
},
|
||||
{
|
||||
title: 'state',
|
||||
dataIndex: 'state',
|
||||
render: (_, record) => record?.billing.state || record?.shipping.state,
|
||||
},
|
||||
{
|
||||
title: 'city',
|
||||
dataIndex: 'city',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.city || record?.shipping.city,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Space>
|
||||
{(record.tags || []).map((tag) => {
|
||||
return (
|
||||
<Tag
|
||||
key={tag}
|
||||
closable
|
||||
onClose={async () => {
|
||||
const { success, message: msg } =
|
||||
await customercontrollerDeltag({
|
||||
email: record.email,
|
||||
tag,
|
||||
});
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
valueType: 'option',
|
||||
fixed: 'right',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Space>
|
||||
<AddTag
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
<HistoryOrder
|
||||
email={record.email}
|
||||
tags={record.tags}
|
||||
tableRef={actionRef}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<PageContainer ghost>
|
||||
<ProTable
|
||||
scroll={{ x: 'max-content' }}
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
request={async (params, sorter) => {
|
||||
const key = Object.keys(sorter)[0];
|
||||
const { data, success } = await customercontrollerGetcustomerlist({
|
||||
...params,
|
||||
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
success,
|
||||
};
|
||||
}}
|
||||
columns={columns}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export 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>修改标签</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) => {
|
||||
return !(tags || []).includes(tag);
|
||||
})
|
||||
.map((tag) => ({ label: tag, value: tag }));
|
||||
}}
|
||||
fieldProps={{
|
||||
value: tagList, // 当前值
|
||||
onChange: async (newValue) => {
|
||||
const added = newValue.filter((x) => !tagList.includes(x));
|
||||
const removed = tagList.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>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListPage;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import * as dictApi from '@/servers/api/dict';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ActionType,
|
||||
|
|
@ -16,7 +17,6 @@ import {
|
|||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import * as dictApi from '@/servers/api/dict';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
|
|
@ -37,8 +37,10 @@ const DictPage: React.FC = () => {
|
|||
const [editDictData, setEditDictData] = useState<any>(null);
|
||||
|
||||
// 右侧字典项列表的状态
|
||||
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] = useState(false);
|
||||
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] = useState(false);
|
||||
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] =
|
||||
useState(false);
|
||||
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] =
|
||||
useState(false);
|
||||
const [editDictItemData, setEditDictItemData] = useState<any>(null);
|
||||
const [dictItemForm] = Form.useForm();
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
|
@ -96,7 +98,7 @@ const DictPage: React.FC = () => {
|
|||
try {
|
||||
const result = await dictApi.dictcontrollerDeletedict({ id });
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '删除失败')
|
||||
throw new Error(result.message || '删除失败');
|
||||
}
|
||||
message.success('删除成功');
|
||||
fetchDicts();
|
||||
|
|
@ -158,7 +160,7 @@ const DictPage: React.FC = () => {
|
|||
try {
|
||||
const result = await dictApi.dictcontrollerDeletedictitem({ id });
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '删除失败')
|
||||
throw new Error(result.message || '删除失败');
|
||||
}
|
||||
message.success('删除成功');
|
||||
|
||||
|
|
@ -176,7 +178,7 @@ const DictPage: React.FC = () => {
|
|||
try {
|
||||
const result = await dictApi.dictcontrollerCreatedictitem({
|
||||
...values,
|
||||
dictId: selectedDict.id
|
||||
dictId: selectedDict.id,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
|
|
@ -199,7 +201,10 @@ const DictPage: React.FC = () => {
|
|||
const handleEditDictItemFormSubmit = async (values: any) => {
|
||||
if (!editDictItemData) return;
|
||||
try {
|
||||
const result = await dictApi.dictcontrollerUpdatedictitem({ id: editDictItemData.id }, values);
|
||||
const result = await dictApi.dictcontrollerUpdatedictitem(
|
||||
{ id: editDictItemData.id },
|
||||
values,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || '更新失败');
|
||||
|
|
@ -227,7 +232,9 @@ const DictPage: React.FC = () => {
|
|||
|
||||
try {
|
||||
// 获取当前字典的所有数据
|
||||
const response = await dictApi.dictcontrollerGetdictitems({ dictId: selectedDict.id });
|
||||
const response = await dictApi.dictcontrollerGetdictitems({
|
||||
dictId: selectedDict.id,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
message.warning('当前字典没有数据可导出');
|
||||
|
|
@ -503,9 +510,10 @@ const DictPage: React.FC = () => {
|
|||
customRequest={async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
try {
|
||||
const result = await dictApi.dictcontrollerImportdictitems(
|
||||
const result =
|
||||
await dictApi.dictcontrollerImportdictitems(
|
||||
{ dictId: selectedDict?.id },
|
||||
[file as File]
|
||||
[file as File],
|
||||
);
|
||||
onSuccess?.(result);
|
||||
} catch (error) {
|
||||
|
|
@ -515,7 +523,7 @@ const DictPage: React.FC = () => {
|
|||
showUploadList={false}
|
||||
disabled={!selectedDict}
|
||||
onChange={(info) => {
|
||||
console.log(`info`,info)
|
||||
console.log(`info`, info);
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功`);
|
||||
// 重新加载字典项列表
|
||||
|
|
@ -677,14 +685,18 @@ const DictPage: React.FC = () => {
|
|||
<Input
|
||||
placeholder="字典名称 (e.g., brand)"
|
||||
value={editDictData?.name || ''}
|
||||
onChange={(e) => setEditDictData({ ...editDictData, name: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setEditDictData({ ...editDictData, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="字典标题">
|
||||
<Input
|
||||
placeholder="字典标题 (e.g., 品牌)"
|
||||
value={editDictData?.title || ''}
|
||||
onChange={(e) => setEditDictData({ ...editDictData, title: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setEditDictData({ ...editDictData, title: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -456,7 +456,6 @@ const ListPage: React.FC = () => {
|
|||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
}}
|
||||
|
||||
rowClassName={(record) => {
|
||||
return record.id === activeLine
|
||||
? styles['selected-line-order-protable']
|
||||
|
|
@ -496,7 +495,6 @@ const ListPage: React.FC = () => {
|
|||
title="批量导出"
|
||||
description="确认导出选中的订单吗?"
|
||||
onConfirm={async () => {
|
||||
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerExportorder({
|
||||
|
|
@ -511,15 +509,16 @@ const ListPage: React.FC = () => {
|
|||
} catch (error: any) {
|
||||
message.error(error?.message || '导出失败');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
<Button type="primary" disabled={selectedRowKeys.length === 0} ghost>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
ghost
|
||||
>
|
||||
批量导出
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
request={async ({ date, ...param }: any) => {
|
||||
if (param.status === 'all') {
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
export const notAttributes = new Set([
|
||||
'zh-cn',
|
||||
'en-us',
|
||||
'category'
|
||||
]);
|
||||
export const notAttributes = new Set(['zh-cn', 'en-us', 'category']);
|
||||
|
|
|
|||
|
|
@ -41,11 +41,14 @@ const AttributePage: React.FC = () => {
|
|||
setLoadingDicts(true);
|
||||
try {
|
||||
const res = await request('/dict/list', { params: { title } });
|
||||
// 条件判断,过滤只保留 allowedDictNames 中的字典
|
||||
const filtered = (res || []).filter((d: any) => !notAttributes.has(d?.name));
|
||||
// 条件判断,确保res是数组再进行过滤
|
||||
const dataList = Array.isArray(res) ? res : res?.data || [];
|
||||
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
|
||||
setDicts(filtered);
|
||||
} catch (error) {
|
||||
console.error('获取字典列表失败:', error);
|
||||
message.error('获取字典列表失败');
|
||||
setDicts([]);
|
||||
}
|
||||
setLoadingDicts(false);
|
||||
};
|
||||
|
|
@ -114,19 +117,26 @@ const AttributePage: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
if (selectedDict?.id) {
|
||||
try {
|
||||
const list = await request('/dict/items', {
|
||||
params: {
|
||||
dictId: selectedDict.id,
|
||||
},
|
||||
});
|
||||
const exists =
|
||||
Array.isArray(list) && list.some((it: any) => it.id === itemId);
|
||||
// 确保list是数组再进行some操作
|
||||
const dataList = Array.isArray(list) ? list : list?.data || [];
|
||||
const exists = dataList.some((it: any) => it.id === itemId);
|
||||
if (exists) {
|
||||
message.error('删除失败');
|
||||
} else {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证删除结果失败:', error);
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
} else {
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
|
|
@ -245,6 +255,7 @@ const AttributePage: React.FC = () => {
|
|||
};
|
||||
}
|
||||
const { name, title } = params;
|
||||
try {
|
||||
const res = await request('/dict/items', {
|
||||
params: {
|
||||
dictId: selectedDict.id,
|
||||
|
|
@ -252,17 +263,36 @@ const AttributePage: React.FC = () => {
|
|||
title,
|
||||
},
|
||||
});
|
||||
// 确保返回的是数组
|
||||
const data = Array.isArray(res) ? res : res?.data || [];
|
||||
return {
|
||||
data: res,
|
||||
data: data,
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取字典项失败:', error);
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}}
|
||||
rowKey="id"
|
||||
search={{
|
||||
layout: 'vertical',
|
||||
}}
|
||||
pagination={false}
|
||||
options={false}
|
||||
options={{
|
||||
reload: true,
|
||||
density: false,
|
||||
setting: {
|
||||
draggable: true,
|
||||
checkable: true,
|
||||
checkedReset: false,
|
||||
},
|
||||
search: false,
|
||||
fullScreen: false,
|
||||
}}
|
||||
size="small"
|
||||
key={selectedDict?.id}
|
||||
headerTitle={
|
||||
|
|
|
|||
|
|
@ -244,7 +244,10 @@ const CategoryPage: React.FC = () => {
|
|||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta title={`${item.title}(${item.titleCN??'-'})`} description={item.name} />
|
||||
<List.Item.Meta
|
||||
title={`${item.title}(${item.titleCN ?? '-'})`}
|
||||
description={item.name}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -112,6 +112,11 @@ const PermutationPage: React.FC = () => {
|
|||
// 2. Fetch Attribute Values (Dict Items)
|
||||
const valuesMap: Record<string, any[]> = {};
|
||||
for (const attr of attrs) {
|
||||
// 使用属性中直接包含的items,而不是额外请求
|
||||
if (attr.items && Array.isArray(attr.items)) {
|
||||
valuesMap[attr.name] = attr.items;
|
||||
} else {
|
||||
// 如果没有items,尝试通过dictId获取
|
||||
const dictId = attr.dict?.id || attr.dictId;
|
||||
if (dictId) {
|
||||
const itemsRes = await request('/dict/items', {
|
||||
|
|
@ -120,6 +125,7 @@ const PermutationPage: React.FC = () => {
|
|||
valuesMap[attr.name] = itemsRes || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
setAttributeValues(valuesMap);
|
||||
|
||||
// 3. Fetch Existing Products
|
||||
|
|
|
|||
|
|
@ -9,7 +9,16 @@ import {
|
|||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { Button, Card, Spin, Tag, message, Select, Progress, Modal } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Select,
|
||||
Spin,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import EditForm from '../List/EditForm';
|
||||
|
||||
|
|
@ -89,7 +98,13 @@ const ProductSyncPage: React.FC = () => {
|
|||
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
|
||||
const [syncProgress, setSyncProgress] = useState(0);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [syncResults, setSyncResults] = useState<{ success: number; failed: number; errors: string[] }>({ success: 0, failed: 0, errors: [] });
|
||||
const [syncResults, setSyncResults] = useState<{
|
||||
success: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
}>({ success: 0, failed: 0, errors: [] });
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<ProductWithWP[]>([]);
|
||||
|
||||
// 初始化数据:获取站点和所有 WP 产品
|
||||
useEffect(() => {
|
||||
|
|
@ -197,24 +212,24 @@ const ProductSyncPage: React.FC = () => {
|
|||
};
|
||||
|
||||
// 批量同步产品到指定站点
|
||||
const batchSyncProducts = async () => {
|
||||
const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => {
|
||||
if (!selectedSiteId) {
|
||||
message.error('请选择要同步到的站点');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSite = sites.find(site => site.id === selectedSiteId);
|
||||
const targetSite = sites.find((site) => site.id === selectedSiteId);
|
||||
if (!targetSite) {
|
||||
message.error('选择的站点不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncing(true);
|
||||
setSyncProgress(0);
|
||||
setSyncResults({ success: 0, failed: 0, errors: [] });
|
||||
// 如果没有传入产品列表,则使用选中的产品
|
||||
let products = productsToSync || selectedRows;
|
||||
|
||||
// 如果既没有传入产品也没有选中产品,则同步所有产品
|
||||
if (!products || products.length === 0) {
|
||||
try {
|
||||
// 获取所有产品
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
current: 1,
|
||||
pageSize: 10000, // 获取所有产品
|
||||
|
|
@ -224,14 +239,24 @@ const ProductSyncPage: React.FC = () => {
|
|||
message.error('获取产品列表失败');
|
||||
return;
|
||||
}
|
||||
products = data.items as ProductWithWP[];
|
||||
} catch (error) {
|
||||
message.error('获取产品列表失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSyncing(true);
|
||||
setSyncProgress(0);
|
||||
setSyncResults({ success: 0, failed: 0, errors: [] });
|
||||
|
||||
const products = data.items as ProductWithWP[];
|
||||
const totalProducts = products.length;
|
||||
let processed = 0;
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
// 逐个同步产品
|
||||
for (const product of products) {
|
||||
try {
|
||||
|
|
@ -239,7 +264,10 @@ const ProductSyncPage: React.FC = () => {
|
|||
let siteProductSku = '';
|
||||
if (product.siteSkus && product.siteSkus.length > 0) {
|
||||
const siteSkuInfo = product.siteSkus.find((sku: any) => {
|
||||
return sku.siteSku && sku.siteSku.includes(targetSite.skuPrefix || targetSite.name);
|
||||
return (
|
||||
sku.siteSku &&
|
||||
sku.siteSku.includes(targetSite.skuPrefix || targetSite.name)
|
||||
);
|
||||
});
|
||||
if (siteSkuInfo) {
|
||||
siteProductSku = siteSkuInfo.siteSku;
|
||||
|
|
@ -247,11 +275,11 @@ const ProductSyncPage: React.FC = () => {
|
|||
}
|
||||
|
||||
// 如果没有找到实际的siteSku,则根据模板生成
|
||||
const expectedSku = siteProductSku || (
|
||||
skuTemplate
|
||||
const expectedSku =
|
||||
siteProductSku ||
|
||||
(skuTemplate
|
||||
? renderSiteSku(skuTemplate, { site: targetSite, product })
|
||||
: `${targetSite.skuPrefix || ''}-${product.sku}`
|
||||
);
|
||||
: `${targetSite.skuPrefix || ''}-${product.sku}`);
|
||||
|
||||
// 检查是否已存在
|
||||
const existingProduct = wpProductMap.get(expectedSku);
|
||||
|
|
@ -272,10 +300,13 @@ const ProductSyncPage: React.FC = () => {
|
|||
let res;
|
||||
if (existingProduct?.externalProductId) {
|
||||
// 更新现有产品
|
||||
res = await request(`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, {
|
||||
res = await request(
|
||||
`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data: syncData,
|
||||
});
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 创建新产品
|
||||
res = await request(`/site-api/${targetSite.id}/products`, {
|
||||
|
|
@ -300,7 +331,6 @@ const ProductSyncPage: React.FC = () => {
|
|||
failedCount++;
|
||||
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
failedCount++;
|
||||
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
|
||||
|
|
@ -315,12 +345,13 @@ const ProductSyncPage: React.FC = () => {
|
|||
if (failedCount === 0) {
|
||||
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
|
||||
} else {
|
||||
message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`);
|
||||
message.warning(
|
||||
`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`,
|
||||
);
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
actionRef.current?.reload();
|
||||
|
||||
} catch (error: any) {
|
||||
message.error('批量同步失败: ' + (error.message || error.toString()));
|
||||
} finally {
|
||||
|
|
@ -453,7 +484,9 @@ const ProductSyncPage: React.FC = () => {
|
|||
const siteSkuInfo = record.siteSkus.find((sku: any) => {
|
||||
// 这里假设可以根据站点名称或其他标识来匹配
|
||||
// 如果需要更精确的匹配逻辑,可以根据实际需求调整
|
||||
return sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name);
|
||||
return (
|
||||
sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name)
|
||||
);
|
||||
});
|
||||
if (siteSkuInfo) {
|
||||
siteProductSku = siteSkuInfo.siteSku;
|
||||
|
|
@ -461,18 +494,21 @@ const ProductSyncPage: React.FC = () => {
|
|||
}
|
||||
|
||||
// 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU
|
||||
const expectedSku = siteProductSku || (
|
||||
skuTemplate
|
||||
const expectedSku =
|
||||
siteProductSku ||
|
||||
(skuTemplate
|
||||
? renderSiteSku(skuTemplate, { site, product: record })
|
||||
: `${site.skuPrefix || ''}-${record.sku}`
|
||||
);
|
||||
: `${site.skuPrefix || ''}-${record.sku}`);
|
||||
|
||||
// 尝试用确定的SKU获取WP产品
|
||||
let wpProduct = wpProductMap.get(expectedSku);
|
||||
|
||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
||||
if (!wpProduct && siteProductSku && skuTemplate) {
|
||||
const templateSku = renderSiteSku(skuTemplate, { site, product: record });
|
||||
const templateSku = renderSiteSku(skuTemplate, {
|
||||
site,
|
||||
product: record,
|
||||
});
|
||||
wpProduct = wpProductMap.get(templateSku);
|
||||
}
|
||||
|
||||
|
|
@ -490,11 +526,11 @@ const ProductSyncPage: React.FC = () => {
|
|||
return await syncProductToSite(values, record, site);
|
||||
}}
|
||||
initialValues={{
|
||||
sku: siteProductSku || (
|
||||
skuTemplate
|
||||
sku:
|
||||
siteProductSku ||
|
||||
(skuTemplate
|
||||
? renderSiteSku(skuTemplate, { site, product: record })
|
||||
: `${site.skuPrefix || ''}-${record.sku}`
|
||||
),
|
||||
: `${site.skuPrefix || ''}-${record.sku}`),
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
|
|
@ -587,36 +623,46 @@ const ProductSyncPage: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="商品同步状态"
|
||||
className="product-sync-card"
|
||||
extra={
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Select
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择目标站点"
|
||||
value={selectedSiteId}
|
||||
onChange={setSelectedSiteId}
|
||||
options={sites.map(site => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}))}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => setBatchSyncModalVisible(true)}
|
||||
disabled={!selectedSiteId || sites.length === 0}
|
||||
>
|
||||
批量同步
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Card title="商品同步状态" className="product-sync-card">
|
||||
<ProTable<ProductWithWP>
|
||||
columns={generateColumns()}
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys, rows) => {
|
||||
setSelectedRowKeys(keys);
|
||||
setSelectedRows(rows);
|
||||
},
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Select
|
||||
key="site-select"
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择目标站点"
|
||||
value={selectedSiteId}
|
||||
onChange={setSelectedSiteId}
|
||||
options={sites.map((site) => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}))}
|
||||
/>,
|
||||
<Button
|
||||
key="batch-sync"
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => {
|
||||
if (!selectedSiteId) {
|
||||
message.warning('请先选择目标站点');
|
||||
return;
|
||||
}
|
||||
setBatchSyncModalVisible(true);
|
||||
}}
|
||||
disabled={!selectedSiteId || sites.length === 0}
|
||||
>
|
||||
批量同步
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params, sort, filter) => {
|
||||
// 调用本地获取产品列表 API
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
|
|
@ -661,8 +707,17 @@ const ProductSyncPage: React.FC = () => {
|
|||
maskClosable={!syncing}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p>目标站点:<strong>{sites.find(s => s.id === selectedSiteId)?.name}</strong></p>
|
||||
<p>
|
||||
目标站点:
|
||||
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
|
||||
</p>
|
||||
{selectedRows.length > 0 ? (
|
||||
<p>
|
||||
已选择 <strong>{selectedRows.length}</strong> 个产品进行同步
|
||||
</p>
|
||||
) : (
|
||||
<p>此操作将同步所有库存产品到指定站点,请确认是否继续?</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{syncing && (
|
||||
|
|
@ -679,12 +734,17 @@ const ProductSyncPage: React.FC = () => {
|
|||
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
|
||||
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
||||
{syncResults.errors.slice(0, 10).map((error, index) => (
|
||||
<div key={index} style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
<div
|
||||
key={index}
|
||||
style={{ fontSize: 12, color: '#666', marginBottom: 4 }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
{syncResults.errors.length > 10 && (
|
||||
<div style={{ fontSize: 12, color: '#999' }}>...还有 {syncResults.errors.length - 10} 个错误</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
...还有 {syncResults.errors.length - 10} 个错误
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -699,7 +759,7 @@ const ProductSyncPage: React.FC = () => {
|
|||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={batchSyncProducts}
|
||||
onClick={() => batchSyncProducts()}
|
||||
loading={syncing}
|
||||
disabled={syncing}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { sitecontrollerAll } from '@/servers/api/site';
|
|||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
|
||||
import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd';
|
||||
import { Button, Col, Menu, Row, Select, Spin, message } from 'antd';
|
||||
import Sider from 'antd/es/layout/Sider';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { SiteItem } from '../List/index';
|
||||
import EditSiteForm from './EditSiteForm';
|
||||
import Sider from 'antd/es/layout/Sider';
|
||||
|
||||
const ShopLayout: React.FC = () => {
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
|
|
@ -91,10 +91,7 @@ const ShopLayout: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
header={{ title: null, breadcrumb: undefined }}
|
||||
>
|
||||
|
||||
<PageContainer header={{ title: null, breadcrumb: undefined }}>
|
||||
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
|
||||
<Col span={4} style={{ height: '100%' }}>
|
||||
<Sider
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Button, Card, List } from 'antd';
|
||||
import { request, useParams } from '@umijs/max';
|
||||
import { App } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { request, useParams } from '@umijs/max';
|
||||
import { App, Button, Card, List } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// 定义链接项的类型
|
||||
interface LinkItem {
|
||||
|
|
@ -48,10 +47,7 @@ const LinksPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="站点链接"
|
||||
breadcrumb={{ items: [] }}
|
||||
/>
|
||||
<PageHeader title="站点链接" breadcrumb={{ items: [] }} />
|
||||
<Card
|
||||
title="常用链接"
|
||||
bordered={false}
|
||||
|
|
@ -76,7 +72,7 @@ const LinksPage: React.FC = () => {
|
|||
target="_blank"
|
||||
>
|
||||
访问
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ const OrdersPage: React.FC = () => {
|
|||
>
|
||||
发货
|
||||
</Button>
|
||||
{record.status === 'completed' && (
|
||||
<Popconfirm
|
||||
title="确定取消发货?"
|
||||
description="取消发货后订单状态将恢复为处理中"
|
||||
|
|
@ -232,6 +233,7 @@ const OrdersPage: React.FC = () => {
|
|||
取消发货
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除订单?"
|
||||
onConfirm={async () => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
message,
|
||||
} from 'antd';
|
||||
import {
|
||||
siteapicontrollerCreatereview,
|
||||
siteapicontrollerUpdatereview,
|
||||
} from '@/servers/api/siteApi';
|
||||
import { Form, Input, InputNumber, Modal, Select, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
|
@ -64,7 +57,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
|||
review: values.content,
|
||||
rating: values.rating,
|
||||
status: values.status,
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 创建新评论
|
||||
|
|
@ -78,7 +71,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
|||
rating: values.rating,
|
||||
author: values.author,
|
||||
author_email: values.email,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +129,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
|||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
|
|
|
|||
|
|
@ -101,14 +101,22 @@ const ReviewsPage: React.FC = () => {
|
|||
};
|
||||
}
|
||||
// 确保 response.data.items 是数组
|
||||
const items = Array.isArray(response.data.items) ? response.data.items : [];
|
||||
const items = Array.isArray(response.data.items)
|
||||
? response.data.items
|
||||
: [];
|
||||
// 确保每个 item 有有效的 id
|
||||
const processedItems = items.map((item, index) => ({
|
||||
...item,
|
||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
||||
id: typeof item.id === 'object' ? JSON.stringify(item.id) : (item.id || index),
|
||||
id:
|
||||
typeof item.id === 'object'
|
||||
? JSON.stringify(item.id)
|
||||
: item.id || index,
|
||||
// 如果 product_id 是对象,转换为字符串
|
||||
product_id: typeof item.product_id === 'object' ? JSON.stringify(item.product_id) : item.product_id,
|
||||
product_id:
|
||||
typeof item.product_id === 'object'
|
||||
? JSON.stringify(item.product_id)
|
||||
: item.product_id,
|
||||
}));
|
||||
return {
|
||||
data: processedItems,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { siteapicontrollerGetwebhooks, siteapicontrollerDeletewebhook, siteapicontrollerCreatewebhook, siteapicontrollerUpdatewebhook, siteapicontrollerGetwebhook } from '@/servers/api/siteApi';
|
||||
import {
|
||||
siteapicontrollerCreatewebhook,
|
||||
siteapicontrollerDeletewebhook,
|
||||
siteapicontrollerGetwebhooks,
|
||||
siteapicontrollerUpdatewebhook,
|
||||
} from '@/servers/api/siteApi';
|
||||
import {
|
||||
ActionType,
|
||||
ProCard,
|
||||
|
|
@ -6,7 +11,16 @@ import {
|
|||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { useParams } from '@umijs/max';
|
||||
import { Button, message, Popconfirm, Space, Modal, Form, Input, Select } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
const WebhooksPage: React.FC = () => {
|
||||
|
|
@ -17,7 +31,8 @@ const WebhooksPage: React.FC = () => {
|
|||
// 模态框状态
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [currentWebhook, setCurrentWebhook] = useState<API.UnifiedWebhookDTO | null>(null);
|
||||
const [currentWebhook, setCurrentWebhook] =
|
||||
useState<API.UnifiedWebhookDTO | null>(null);
|
||||
|
||||
// 表单实例
|
||||
const [form] = Form.useForm();
|
||||
|
|
@ -114,10 +129,25 @@ const WebhooksPage: React.FC = () => {
|
|||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '主题', dataIndex: 'topic', key: 'topic' },
|
||||
{ title: '回调URL', dataIndex: 'delivery_url', key: 'delivery_url', ellipsis: true },
|
||||
{
|
||||
title: '回调URL',
|
||||
dataIndex: 'delivery_url',
|
||||
key: 'delivery_url',
|
||||
ellipsis: true,
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
{ title: '创建时间', dataIndex: 'date_created', key: 'date_created', valueType: 'dateTime' },
|
||||
{ title: '更新时间', dataIndex: 'date_modified', key: 'date_modified', valueType: 'dateTime' },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'date_created',
|
||||
key: 'date_created',
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'date_modified',
|
||||
key: 'date_modified',
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
|
|
@ -184,12 +214,17 @@ const WebhooksPage: React.FC = () => {
|
|||
};
|
||||
}
|
||||
// 确保 response.data.items 是数组
|
||||
const items = Array.isArray(response.data.items) ? response.data.items : [];
|
||||
const items = Array.isArray(response.data.items)
|
||||
? response.data.items
|
||||
: [];
|
||||
// 确保每个 item 有有效的 id
|
||||
const processedItems = items.map((item, index) => ({
|
||||
...item,
|
||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
||||
id: typeof item.id === 'object' ? JSON.stringify(item.id) : (item.id || index),
|
||||
id:
|
||||
typeof item.id === 'object'
|
||||
? JSON.stringify(item.id)
|
||||
: item.id || index,
|
||||
}));
|
||||
return {
|
||||
data: processedItems,
|
||||
|
|
@ -211,10 +246,7 @@ const WebhooksPage: React.FC = () => {
|
|||
}}
|
||||
headerTitle="Webhooks列表"
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={showCreateModal}
|
||||
>
|
||||
<Button type="primary" onClick={showCreateModal}>
|
||||
新建Webhook
|
||||
</Button>,
|
||||
]}
|
||||
|
|
@ -245,7 +277,10 @@ const WebhooksPage: React.FC = () => {
|
|||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入webhook名称' }, { max: 100, message: '名称不能超过100个字符' }]}
|
||||
rules={[
|
||||
{ required: true, message: '请输入webhook名称' },
|
||||
{ max: 100, message: '名称不能超过100个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入webhook名称" />
|
||||
</Form.Item>
|
||||
|
|
@ -265,7 +300,10 @@ const WebhooksPage: React.FC = () => {
|
|||
<Form.Item
|
||||
name="delivery_url"
|
||||
label="回调URL"
|
||||
rules={[{ required: true, message: '请输入回调URL' }, { type: 'url', message: '请输入有效的URL' }]}
|
||||
rules={[
|
||||
{ required: true, message: '请输入回调URL' },
|
||||
{ type: 'url', message: '请输入有效的URL' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入回调URL,如:https://example.com/webhook" />
|
||||
</Form.Item>
|
||||
|
|
@ -283,10 +321,7 @@ const WebhooksPage: React.FC = () => {
|
|||
label="状态"
|
||||
rules={[{ required: true, message: '请选择webhook状态' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择webhook状态"
|
||||
options={webhookStatuses}
|
||||
/>
|
||||
<Select placeholder="请选择webhook状态" options={webhookStatuses} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
templatecontrollerCreatetemplate,
|
||||
templatecontrollerDeletetemplate,
|
||||
templatecontrollerGettemplatelist,
|
||||
templatecontrollerRendertemplate,
|
||||
templatecontrollerRendertemplatedirect,
|
||||
templatecontrollerUpdatetemplate,
|
||||
} from '@/servers/api/template';
|
||||
|
|
@ -75,12 +74,10 @@ const useTemplatePreview = () => {
|
|||
renderedResult,
|
||||
handlePreview,
|
||||
refreshPreview,
|
||||
setPreviewData
|
||||
setPreviewData,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
const List: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
|
|
@ -176,7 +173,8 @@ const CreateForm: React.FC<{
|
|||
}> = ({ tableRef }) => {
|
||||
const { message } = App.useApp();
|
||||
const [form] = ProForm.useForm();
|
||||
const { renderedResult, handlePreview, refreshPreview } = useTemplatePreview();
|
||||
const { renderedResult, handlePreview, refreshPreview } =
|
||||
useTemplatePreview();
|
||||
|
||||
return (
|
||||
<DrawerForm<API.CreateTemplateDTO>
|
||||
|
|
@ -263,8 +261,17 @@ const CreateForm: React.FC<{
|
|||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>实时预览</Typography.Title>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
实时预览
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
|
|
@ -283,7 +290,7 @@ const CreateForm: React.FC<{
|
|||
height: '600px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
||||
|
|
@ -302,7 +309,8 @@ const UpdateForm: React.FC<{
|
|||
}> = ({ tableRef, values: initialValues }) => {
|
||||
const { message } = App.useApp();
|
||||
const [form] = ProForm.useForm();
|
||||
const { renderedResult, handlePreview, refreshPreview, setPreviewData } = useTemplatePreview();
|
||||
const { renderedResult, handlePreview, refreshPreview, setPreviewData } =
|
||||
useTemplatePreview();
|
||||
|
||||
// 组件挂载时初始化预览数据
|
||||
useEffect(() => {
|
||||
|
|
@ -310,7 +318,7 @@ const UpdateForm: React.FC<{
|
|||
setPreviewData({
|
||||
name: initialValues.name,
|
||||
value: initialValues.value,
|
||||
testData: initialValues.testData
|
||||
testData: initialValues.testData,
|
||||
});
|
||||
}
|
||||
}, [initialValues, setPreviewData]);
|
||||
|
|
@ -405,8 +413,17 @@ const UpdateForm: React.FC<{
|
|||
</div>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>实时预览</Typography.Title>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
实时预览
|
||||
</Typography.Title>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
|
|
@ -425,7 +442,7 @@ const UpdateForm: React.FC<{
|
|||
height: '600px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@ const WpToolPage: React.FC = () => {
|
|||
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
|
||||
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
|
||||
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
|
||||
const [isConfigLoading, setIsConfigLoading] = useState(false); // 是否正在加载配置
|
||||
const [configLoadAttempts, setConfigLoadAttempts] = useState(0); // 配置加载重试次数
|
||||
const [config, setConfig] = useState<TagConfig>({
|
||||
// 动态配置
|
||||
brands: [],
|
||||
|
|
@ -237,22 +239,34 @@ const WpToolPage: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const fetchAllConfigs = async () => {
|
||||
try {
|
||||
message.loading({ content: '正在加载字典配置...', key: 'loading-config' });
|
||||
|
||||
// 1. 获取所有字典列表以找到对应的 ID
|
||||
const dictList = await request('/dict/list');
|
||||
const dictListResponse = await request('/dict/list');
|
||||
// 处理后端统一响应格式
|
||||
const dictList = dictListResponse?.data || dictListResponse || [];
|
||||
|
||||
// 2. 根据字典名称获取字典项
|
||||
const getItems = async (dictName: string) => {
|
||||
try {
|
||||
const dict = dictList.find((d: any) => d.name === dictName);
|
||||
if (!dict) {
|
||||
console.warn(`Dictionary ${dictName} not found`);
|
||||
return [];
|
||||
}
|
||||
const res = await request('/dict/items', {
|
||||
const response = await request('/dict/items', {
|
||||
params: { dictId: dict.id },
|
||||
});
|
||||
return res.map((item: any) => item.name);
|
||||
// 处理后端统一响应格式,获取数据数组
|
||||
const items = response?.data || response || [];
|
||||
return items.map((item: any) => item.name);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch items for ${dictName}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 并行获取所有字典项
|
||||
const [
|
||||
brands,
|
||||
fruitKeys,
|
||||
|
|
@ -264,9 +278,9 @@ const WpToolPage: React.FC = () => {
|
|||
categoryKeys,
|
||||
] = await Promise.all([
|
||||
getItems('brand'),
|
||||
getItems('fruit'), // 假设字典名为 fruit
|
||||
getItems('mint'), // 假设字典名为 mint
|
||||
getItems('flavor'), // 假设字典名为 flavor
|
||||
getItems('fruit'),
|
||||
getItems('mint'),
|
||||
getItems('flavor'),
|
||||
getItems('strength'),
|
||||
getItems('size'),
|
||||
getItems('humidity'),
|
||||
|
|
@ -283,11 +297,19 @@ const WpToolPage: React.FC = () => {
|
|||
humidityKeys,
|
||||
categoryKeys,
|
||||
};
|
||||
|
||||
setConfig(newConfig);
|
||||
form.setFieldsValue(newConfig);
|
||||
message.success({ content: '字典配置加载成功', key: 'loading-config' });
|
||||
|
||||
// 显示加载结果统计
|
||||
const totalItems = brands.length + fruitKeys.length + mintKeys.length + flavorKeys.length +
|
||||
strengthKeys.length + sizeKeys.length + humidityKeys.length + categoryKeys.length;
|
||||
console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch configs:', error);
|
||||
message.error('获取字典配置失败');
|
||||
message.error({ content: '获取字典配置失败,请刷新页面重试', key: 'loading-config' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,21 @@ export async function customercontrollerGetcustomerlist(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/getcustomerstatisticlist */
|
||||
export async function customercontrollerGetcustomerstatisticlist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerGetcustomerstatisticlistParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/getcustomerstatisticlist', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/gettags */
|
||||
export async function customercontrollerGettags(options?: {
|
||||
[key: string]: any;
|
||||
|
|
@ -71,3 +86,18 @@ export async function customercontrollerSetrate(
|
|||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/sync */
|
||||
export async function customercontrollerSynccustomers(
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { request } from 'umi';
|
|||
|
||||
/** 此处后端没有提供注释 GET /site/all */
|
||||
export async function sitecontrollerAll(options?: { [key: string]: any }) {
|
||||
return request<API.WpSitesResponse>('/site/all', {
|
||||
return request<API.SitesResponse>('/site/all', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -285,6 +285,18 @@ declare namespace API {
|
|||
customerId?: number;
|
||||
};
|
||||
|
||||
type customercontrollerGetcustomerstatisticlistParams = {
|
||||
current?: string;
|
||||
pageSize?: string;
|
||||
email?: string;
|
||||
tags?: string;
|
||||
sorterKey?: string;
|
||||
sorterValue?: string;
|
||||
state?: string;
|
||||
first_purchase_date?: string;
|
||||
customerId?: number;
|
||||
};
|
||||
|
||||
type CustomerTagDTO = {
|
||||
email?: string;
|
||||
tag?: string;
|
||||
|
|
@ -1893,6 +1905,17 @@ declare namespace API {
|
|||
id: string;
|
||||
};
|
||||
|
||||
type SitesResponse = {
|
||||
/** 状态码 */
|
||||
code?: number;
|
||||
/** 是否成功 */
|
||||
success?: boolean;
|
||||
/** 消息内容 */
|
||||
message?: string;
|
||||
/** 响应数据 */
|
||||
data?: SiteConfig[];
|
||||
};
|
||||
|
||||
type statisticscontrollerGetinativeusersbymonthParams = {
|
||||
month?: string;
|
||||
};
|
||||
|
|
@ -3014,15 +3037,4 @@ declare namespace API {
|
|||
/** 数据列表 */
|
||||
items?: WpProductDTO[];
|
||||
};
|
||||
|
||||
type WpSitesResponse = {
|
||||
/** 状态码 */
|
||||
code?: number;
|
||||
/** 是否成功 */
|
||||
success?: boolean;
|
||||
/** 消息内容 */
|
||||
message?: string;
|
||||
/** 响应数据 */
|
||||
data?: SiteConfig[];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue