feat(客户): 新增客户数据分析列表页面
refactor(产品): 优化属性字典项获取逻辑,增加错误处理 fix(订单): 修复取消发货按钮在已完成订单中显示的问题 style: 统一代码格式,修复缩进和导入顺序问题 perf(字典): 优化字典配置加载逻辑,增加重试机制 docs(API): 更新API类型定义,添加客户统计相关接口 chore: 更新package.json文件格式
This commit is contained in:
parent
ffa77560fa
commit
1a68e469dd
|
|
@ -131,6 +131,11 @@ export default defineConfig({
|
||||||
path: '/customer/list',
|
path: '/customer/list',
|
||||||
component: './Customer/List',
|
component: './Customer/List',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '数据分析列表',
|
||||||
|
path: '/customer/statistic',
|
||||||
|
component: './Customer/Statistic',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,4 @@
|
||||||
"prettier-plugin-packagejson": "^2.4.3",
|
"prettier-plugin-packagejson": "^2.4.3",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,9 @@ const CategoryPage: React.FC = () => {
|
||||||
// Fetch all dicts and filter those that are allowed attributes
|
// Fetch all dicts and filter those that are allowed attributes
|
||||||
try {
|
try {
|
||||||
const res = await request('/dict/list');
|
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
|
// Filter out already added attributes
|
||||||
const existingDictIds = new Set(
|
const existingDictIds = new Set(
|
||||||
categoryAttributes.map((ca: any) => ca.dict.id),
|
categoryAttributes.map((ca: any) => ca.dict.id),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { HistoryOrder } from '@/pages/Statistics/Order';
|
|
||||||
import {
|
import {
|
||||||
customercontrollerAddtag,
|
customercontrollerAddtag,
|
||||||
customercontrollerDeltag,
|
customercontrollerDeltag,
|
||||||
customercontrollerGetcustomerlist,
|
customercontrollerGetcustomerlist,
|
||||||
customercontrollerGettags,
|
customercontrollerGettags,
|
||||||
customercontrollerSetrate,
|
customercontrollerSetrate,
|
||||||
|
customercontrollerSynccustomers,
|
||||||
} from '@/servers/api/customer';
|
} from '@/servers/api/customer';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
ModalForm,
|
ModalForm,
|
||||||
|
|
@ -14,97 +15,219 @@ import {
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} 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 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 actionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
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: '用户名',
|
title: '用户名',
|
||||||
dataIndex: 'username',
|
dataIndex: 'username',
|
||||||
hideInSearch: true,
|
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: '邮箱',
|
title: '邮箱',
|
||||||
dataIndex: 'email',
|
dataIndex: 'email',
|
||||||
|
copyable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '客户编号',
|
title: '电话',
|
||||||
dataIndex: 'customerId',
|
dataIndex: 'phone',
|
||||||
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,
|
hideInSearch: true,
|
||||||
dataIndex: 'last_purchase_date',
|
copyable: true,
|
||||||
valueType: 'dateTime',
|
|
||||||
sorter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '订单数',
|
title: '账单地址',
|
||||||
dataIndex: 'orders',
|
dataIndex: 'billing',
|
||||||
hideInSearch: true,
|
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,
|
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) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<Rate
|
<Rate
|
||||||
|
|
@ -112,46 +235,31 @@ const ListPage: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: msg } =
|
const { success, message: msg } =
|
||||||
await customercontrollerSetrate({
|
await customercontrollerSetrate({
|
||||||
id: record.customerId,
|
id: record.id,
|
||||||
rate: val,
|
rate: val,
|
||||||
});
|
});
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success(msg);
|
message.success(msg);
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
message.error(e.message);
|
message.error(e?.message || '设置评分失败');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={record.rate}
|
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: '标签',
|
title: '标签',
|
||||||
dataIndex: 'tags',
|
dataIndex: 'tags',
|
||||||
|
hideInSearch: true,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space size={[0, 8]} wrap>
|
||||||
{(record.tags || []).map((tag) => {
|
{(record.tags || []).map((tag: string) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag}
|
key={tag}
|
||||||
|
|
@ -162,8 +270,14 @@ const ListPage: React.FC = () => {
|
||||||
email: record.email,
|
email: record.email,
|
||||||
tag,
|
tag,
|
||||||
});
|
});
|
||||||
return false;
|
if (!success) {
|
||||||
|
message.error(msg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
}}
|
}}
|
||||||
|
style={{ marginBottom: 4 }}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</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: '操作',
|
title: '操作',
|
||||||
dataIndex: 'option',
|
dataIndex: 'option',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
|
width: 120,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space direction="vertical" size="small">
|
||||||
<AddTag
|
<AddTag
|
||||||
email={record.email}
|
email={record.email}
|
||||||
tags={record.tags}
|
tags={record.tags}
|
||||||
tableRef={actionRef}
|
tableRef={actionRef}
|
||||||
/>
|
/>
|
||||||
<HistoryOrder
|
<Button
|
||||||
email={record.email}
|
type="link"
|
||||||
tags={record.tags}
|
size="small"
|
||||||
tableRef={actionRef}
|
onClick={() => {
|
||||||
/>
|
// 这里可以添加查看客户详情的逻辑
|
||||||
|
message.info('客户详情功能开发中...');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer ghost>
|
<PageContainer header={{ title: '客户列表' }}>
|
||||||
<ProTable
|
<ProTable
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
headerTitle="查询表格"
|
headerTitle="查询表格"
|
||||||
|
|
@ -207,7 +345,9 @@ const ListPage: React.FC = () => {
|
||||||
const key = Object.keys(sorter)[0];
|
const key = Object.keys(sorter)[0];
|
||||||
const { data, success } = await customercontrollerGetcustomerlist({
|
const { data, success } = await customercontrollerGetcustomerlist({
|
||||||
...params,
|
...params,
|
||||||
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
|
current: params.current?.toString(),
|
||||||
|
pageSize: params.pageSize?.toString(),
|
||||||
|
...(key ? { sorterKey: key, sorterValue: sorter[key] as string } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -217,12 +357,37 @@ const ListPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
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>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddTag: React.FC<{
|
const AddTag: React.FC<{
|
||||||
email: string;
|
email: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
|
@ -233,7 +398,11 @@ export const AddTag: React.FC<{
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
title={`修改标签 - ${email}`}
|
title={`修改标签 - ${email}`}
|
||||||
trigger={<Button>修改标签</Button>}
|
trigger={
|
||||||
|
<Button type="link" size="small">
|
||||||
|
修改标签
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
width={800}
|
width={800}
|
||||||
modalProps={{
|
modalProps={{
|
||||||
destroyOnHidden: true,
|
destroyOnHidden: true,
|
||||||
|
|
@ -250,16 +419,16 @@ export const AddTag: React.FC<{
|
||||||
if (!success) return [];
|
if (!success) return [];
|
||||||
setTagList(tags || []);
|
setTagList(tags || []);
|
||||||
return data
|
return data
|
||||||
.filter((tag) => {
|
.filter((tag: string) => {
|
||||||
return !(tags || []).includes(tag);
|
return !(tags || []).includes(tag);
|
||||||
})
|
})
|
||||||
.map((tag) => ({ label: tag, value: tag }));
|
.map((tag: string) => ({ label: tag, value: tag }));
|
||||||
}}
|
}}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
value: tagList, // 当前值
|
value: tagList, // 当前值
|
||||||
onChange: async (newValue) => {
|
onChange: async (newValue) => {
|
||||||
const added = newValue.filter((x) => !tagList.includes(x));
|
const added = newValue.filter((x) => !(tags || []).includes(x));
|
||||||
const removed = tagList.filter((x) => !newValue.includes(x));
|
const removed = (tags || []).filter((x) => !newValue.includes(x));
|
||||||
|
|
||||||
if (added.length) {
|
if (added.length) {
|
||||||
const { success, message: msg } = await customercontrollerAddtag({
|
const { success, message: msg } = await customercontrollerAddtag({
|
||||||
|
|
@ -282,7 +451,6 @@ export const AddTag: React.FC<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tableRef?.current?.reload();
|
tableRef?.current?.reload();
|
||||||
|
|
||||||
setTagList(newValue);
|
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 { UploadOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
|
|
@ -16,7 +17,6 @@ import {
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import * as dictApi from '@/servers/api/dict';
|
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
const { Sider, Content } = Layout;
|
||||||
|
|
||||||
|
|
@ -26,19 +26,21 @@ const DictPage: React.FC = () => {
|
||||||
const [loadingDicts, setLoadingDicts] = useState(false);
|
const [loadingDicts, setLoadingDicts] = useState(false);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [selectedDict, setSelectedDict] = useState<any>(null);
|
const [selectedDict, setSelectedDict] = useState<any>(null);
|
||||||
|
|
||||||
// 添加字典 modal 状态
|
// 添加字典 modal 状态
|
||||||
const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false);
|
const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false);
|
||||||
const [addDictName, setAddDictName] = useState('');
|
const [addDictName, setAddDictName] = useState('');
|
||||||
const [addDictTitle, setAddDictTitle] = useState('');
|
const [addDictTitle, setAddDictTitle] = useState('');
|
||||||
|
|
||||||
// 编辑字典 modal 状态
|
// 编辑字典 modal 状态
|
||||||
const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false);
|
const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false);
|
||||||
const [editDictData, setEditDictData] = useState<any>(null);
|
const [editDictData, setEditDictData] = useState<any>(null);
|
||||||
|
|
||||||
// 右侧字典项列表的状态
|
// 右侧字典项列表的状态
|
||||||
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] = useState(false);
|
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] =
|
||||||
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] = useState(false);
|
useState(false);
|
||||||
|
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] =
|
||||||
|
useState(false);
|
||||||
const [editDictItemData, setEditDictItemData] = useState<any>(null);
|
const [editDictItemData, setEditDictItemData] = useState<any>(null);
|
||||||
const [dictItemForm] = Form.useForm();
|
const [dictItemForm] = Form.useForm();
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
@ -95,15 +97,15 @@ const DictPage: React.FC = () => {
|
||||||
const handleDeleteDict = async (id: number) => {
|
const handleDeleteDict = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const result = await dictApi.dictcontrollerDeletedict({ id });
|
const result = await dictApi.dictcontrollerDeletedict({ id });
|
||||||
if(!result.success){
|
if (!result.success) {
|
||||||
throw new Error(result.message || '删除失败')
|
throw new Error(result.message || '删除失败');
|
||||||
}
|
}
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
fetchDicts();
|
fetchDicts();
|
||||||
if (selectedDict?.id === id) {
|
if (selectedDict?.id === id) {
|
||||||
setSelectedDict(null);
|
setSelectedDict(null);
|
||||||
}
|
}
|
||||||
} catch (error:any) {
|
} catch (error: any) {
|
||||||
message.error(`删除失败,原因为:${error.message}`);
|
message.error(`删除失败,原因为:${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -157,16 +159,16 @@ const DictPage: React.FC = () => {
|
||||||
const handleDeleteDictItem = async (id: number) => {
|
const handleDeleteDictItem = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
const result = await dictApi.dictcontrollerDeletedictitem({ id });
|
const result = await dictApi.dictcontrollerDeletedictitem({ id });
|
||||||
if(!result.success){
|
if (!result.success) {
|
||||||
throw new Error(result.message || '删除失败')
|
throw new Error(result.message || '删除失败');
|
||||||
}
|
}
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
|
|
||||||
// 强制刷新字典项列表
|
// 强制刷新字典项列表
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error:any) {
|
} catch (error: any) {
|
||||||
message.error(`删除失败,原因为:${error.message}`);
|
message.error(`删除失败,原因为:${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -174,18 +176,18 @@ const DictPage: React.FC = () => {
|
||||||
// 添加字典项表单提交
|
// 添加字典项表单提交
|
||||||
const handleAddDictItemFormSubmit = async (values: any) => {
|
const handleAddDictItemFormSubmit = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
const result = await dictApi.dictcontrollerCreatedictitem({
|
const result = await dictApi.dictcontrollerCreatedictitem({
|
||||||
...values,
|
...values,
|
||||||
dictId: selectedDict.id
|
dictId: selectedDict.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.message || '添加失败');
|
throw new Error(result.message || '添加失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('添加成功');
|
message.success('添加成功');
|
||||||
setIsAddDictItemModalVisible(false);
|
setIsAddDictItemModalVisible(false);
|
||||||
|
|
||||||
// 强制刷新字典项列表
|
// 强制刷新字典项列表
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
|
|
@ -199,16 +201,19 @@ const DictPage: React.FC = () => {
|
||||||
const handleEditDictItemFormSubmit = async (values: any) => {
|
const handleEditDictItemFormSubmit = async (values: any) => {
|
||||||
if (!editDictItemData) return;
|
if (!editDictItemData) return;
|
||||||
try {
|
try {
|
||||||
const result = await dictApi.dictcontrollerUpdatedictitem({ id: editDictItemData.id }, values);
|
const result = await dictApi.dictcontrollerUpdatedictitem(
|
||||||
|
{ id: editDictItemData.id },
|
||||||
|
values,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.message || '更新失败');
|
throw new Error(result.message || '更新失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
setIsEditDictItemModalVisible(false);
|
setIsEditDictItemModalVisible(false);
|
||||||
setEditDictItemData(null);
|
setEditDictItemData(null);
|
||||||
|
|
||||||
// 强制刷新字典项列表
|
// 强制刷新字典项列表
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
|
|
@ -227,7 +232,9 @@ const DictPage: React.FC = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取当前字典的所有数据
|
// 获取当前字典的所有数据
|
||||||
const response = await dictApi.dictcontrollerGetdictitems({ dictId: selectedDict.id });
|
const response = await dictApi.dictcontrollerGetdictitems({
|
||||||
|
dictId: selectedDict.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (!response || response.length === 0) {
|
if (!response || response.length === 0) {
|
||||||
message.warning('当前字典没有数据可导出');
|
message.warning('当前字典没有数据可导出');
|
||||||
|
|
@ -390,13 +397,13 @@ const DictPage: React.FC = () => {
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setIsAddDictModalVisible(true)}
|
onClick={() => setIsAddDictModalVisible(true)}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
添加字典
|
添加字典
|
||||||
</Button>
|
</Button>
|
||||||
<Upload
|
<Upload
|
||||||
name="file"
|
name="file"
|
||||||
action="/api/dict/import"
|
action="/api/dict/import"
|
||||||
|
|
@ -417,7 +424,7 @@ const DictPage: React.FC = () => {
|
||||||
<Button size="small" onClick={handleDownloadDictTemplate}>
|
<Button size="small" onClick={handleDownloadDictTemplate}>
|
||||||
导出模板
|
导出模板
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<Table
|
<Table
|
||||||
dataSource={dicts}
|
dataSource={dicts}
|
||||||
columns={dictColumns}
|
columns={dictColumns}
|
||||||
|
|
@ -459,7 +466,7 @@ const DictPage: React.FC = () => {
|
||||||
name,
|
name,
|
||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 适配新的响应格式,检查是否有 successResponse 包裹
|
// 适配新的响应格式,检查是否有 successResponse 包裹
|
||||||
if (res && res.success !== undefined) {
|
if (res && res.success !== undefined) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -468,7 +475,7 @@ const DictPage: React.FC = () => {
|
||||||
total: res.data?.length || 0,
|
total: res.data?.length || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容旧的响应格式(直接返回数组)
|
// 兼容旧的响应格式(直接返回数组)
|
||||||
return {
|
return {
|
||||||
data: res || [],
|
data: res || [],
|
||||||
|
|
@ -503,10 +510,11 @@ const DictPage: React.FC = () => {
|
||||||
customRequest={async (options) => {
|
customRequest={async (options) => {
|
||||||
const { file, onSuccess, onError } = options;
|
const { file, onSuccess, onError } = options;
|
||||||
try {
|
try {
|
||||||
const result = await dictApi.dictcontrollerImportdictitems(
|
const result =
|
||||||
{ dictId: selectedDict?.id },
|
await dictApi.dictcontrollerImportdictitems(
|
||||||
[file as File]
|
{ dictId: selectedDict?.id },
|
||||||
);
|
[file as File],
|
||||||
|
);
|
||||||
onSuccess?.(result);
|
onSuccess?.(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onError?.(error as Error);
|
onError?.(error as Error);
|
||||||
|
|
@ -515,7 +523,7 @@ const DictPage: React.FC = () => {
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
disabled={!selectedDict}
|
disabled={!selectedDict}
|
||||||
onChange={(info) => {
|
onChange={(info) => {
|
||||||
console.log(`info`,info)
|
console.log(`info`, info);
|
||||||
if (info.file.status === 'done') {
|
if (info.file.status === 'done') {
|
||||||
message.success(`${info.file.name} 文件上传成功`);
|
message.success(`${info.file.name} 文件上传成功`);
|
||||||
// 重新加载字典项列表
|
// 重新加载字典项列表
|
||||||
|
|
@ -677,14 +685,18 @@ const DictPage: React.FC = () => {
|
||||||
<Input
|
<Input
|
||||||
placeholder="字典名称 (e.g., brand)"
|
placeholder="字典名称 (e.g., brand)"
|
||||||
value={editDictData?.name || ''}
|
value={editDictData?.name || ''}
|
||||||
onChange={(e) => setEditDictData({ ...editDictData, name: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setEditDictData({ ...editDictData, name: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="字典标题">
|
<Form.Item label="字典标题">
|
||||||
<Input
|
<Input
|
||||||
placeholder="字典标题 (e.g., 品牌)"
|
placeholder="字典标题 (e.g., 品牌)"
|
||||||
value={editDictData?.title || ''}
|
value={editDictData?.title || ''}
|
||||||
onChange={(e) => setEditDictData({ ...editDictData, title: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setEditDictData({ ...editDictData, title: e.target.value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,6 @@ const ListPage: React.FC = () => {
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
onChange: (keys) => setSelectedRowKeys(keys),
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
rowClassName={(record) => {
|
rowClassName={(record) => {
|
||||||
return record.id === activeLine
|
return record.id === activeLine
|
||||||
? styles['selected-line-order-protable']
|
? styles['selected-line-order-protable']
|
||||||
|
|
@ -482,9 +481,9 @@ const ListPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
tableRef={actionRef}
|
tableRef={actionRef}
|
||||||
/>,
|
/>,
|
||||||
// <Button
|
// <Button
|
||||||
// type="primary"
|
// type="primary"
|
||||||
// disabled={selectedRowKeys.length === 0}
|
// disabled={selectedRowKeys.length === 0}
|
||||||
// onClick={handleBatchExport}
|
// onClick={handleBatchExport}
|
||||||
// >
|
// >
|
||||||
// 批量导出
|
// 批量导出
|
||||||
|
|
@ -493,7 +492,6 @@ const ListPage: React.FC = () => {
|
||||||
title="批量导出"
|
title="批量导出"
|
||||||
description="确认导出选中的订单吗?"
|
description="确认导出选中的订单吗?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await ordercontrollerExportorder({
|
await ordercontrollerExportorder({
|
||||||
|
|
@ -508,15 +506,16 @@ const ListPage: React.FC = () => {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error?.message || '导出失败');
|
message.error(error?.message || '导出失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button type="primary" disabled={selectedRowKeys.length === 0} ghost>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
批量导出
|
批量导出
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>,
|
||||||
]}
|
]}
|
||||||
request={async ({ date, ...param }: any) => {
|
request={async ({ date, ...param }: any) => {
|
||||||
if (param.status === 'all') {
|
if (param.status === 'all') {
|
||||||
|
|
@ -605,33 +604,33 @@ const Detail: React.FC<{
|
||||||
)
|
)
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.siteId || !record.externalOrderId) {
|
if (!record.siteId || !record.externalOrderId) {
|
||||||
message.error('站点ID或外部订单ID不存在');
|
message.error('站点ID或外部订单ID不存在');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerSyncorderbyid({
|
||||||
|
siteId: record.siteId,
|
||||||
|
orderId: record.externalOrderId,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('同步成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.message || '同步失败');
|
||||||
}
|
}
|
||||||
const { success, message: errMsg } =
|
}}
|
||||||
await ordercontrollerSyncorderbyid({
|
>
|
||||||
siteId: record.siteId,
|
同步订单
|
||||||
orderId: record.externalOrderId,
|
</Button>,
|
||||||
});
|
]),
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
message.success('同步成功');
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error?.message || '同步失败');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
同步订单
|
|
||||||
</Button>,
|
|
||||||
]),
|
|
||||||
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
|
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
|
||||||
// ? [
|
// ? [
|
||||||
// <Divider type="vertical" />,
|
// <Divider type="vertical" />,
|
||||||
|
|
@ -650,152 +649,152 @@ const Detail: React.FC<{
|
||||||
'pending_refund',
|
'pending_refund',
|
||||||
].includes(record.orderStatus)
|
].includes(record.orderStatus)
|
||||||
? [
|
? [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="转至售后"
|
title="转至售后"
|
||||||
description="确认转至售后?"
|
description="确认转至售后?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.id) {
|
if (!record.id) {
|
||||||
message.error('订单ID不存在');
|
message.error('订单ID不存在');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{
|
||||||
|
id: record.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'after_sale_pending',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
const { success, message: errMsg } =
|
}}
|
||||||
await ordercontrollerChangestatus(
|
>
|
||||||
{
|
<Button type="primary" ghost>
|
||||||
id: record.id,
|
转至售后
|
||||||
},
|
</Button>
|
||||||
{
|
</Popconfirm>,
|
||||||
status: 'after_sale_pending',
|
]
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至售后
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
]
|
|
||||||
: []),
|
: []),
|
||||||
...(record.orderStatus === 'after_sale_pending'
|
...(record.orderStatus === 'after_sale_pending'
|
||||||
? [
|
? [
|
||||||
<Divider type="vertical" />,
|
<Divider type="vertical" />,
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="转至取消"
|
title="转至取消"
|
||||||
description="确认转至取消?"
|
description="确认转至取消?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
if (!record.id) {
|
if (!record.id) {
|
||||||
message.error('订单ID不存在');
|
message.error('订单ID不存在');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await ordercontrollerCancelorder({
|
await ordercontrollerCancelorder({
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至取消
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至退款"
|
|
||||||
description="确认转至退款?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (!record.id) {
|
|
||||||
message.error('订单ID不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerRefundorder({
|
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至退款
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至完成"
|
|
||||||
description="确认转至完成?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (!record.id) {
|
|
||||||
message.error('订单ID不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerCompletedorder({
|
|
||||||
id: record.id,
|
|
||||||
});
|
|
||||||
if (!success) {
|
|
||||||
throw new Error(errMsg);
|
|
||||||
}
|
|
||||||
tableRef.current?.reload();
|
|
||||||
} catch (error: any) {
|
|
||||||
message.error(error.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button type="primary" ghost>
|
|
||||||
转至完成
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
<Divider type="vertical" />,
|
|
||||||
<Popconfirm
|
|
||||||
title="转至待补发"
|
|
||||||
description="确认转至待补发?"
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
const { success, message: errMsg } =
|
|
||||||
await ordercontrollerChangestatus(
|
|
||||||
{
|
|
||||||
id: record.id,
|
id: record.id,
|
||||||
},
|
});
|
||||||
{
|
if (!success) {
|
||||||
status: 'pending_reshipment',
|
throw new Error(errMsg);
|
||||||
},
|
}
|
||||||
);
|
tableRef.current?.reload();
|
||||||
if (!success) {
|
} catch (error: any) {
|
||||||
throw new Error(errMsg);
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
tableRef.current?.reload();
|
}}
|
||||||
} catch (error: any) {
|
>
|
||||||
message.error(error.message);
|
<Button type="primary" ghost>
|
||||||
}
|
转至取消
|
||||||
}}
|
</Button>
|
||||||
>
|
</Popconfirm>,
|
||||||
<Button type="primary" ghost>
|
<Divider type="vertical" />,
|
||||||
转至待补发
|
<Popconfirm
|
||||||
</Button>
|
title="转至退款"
|
||||||
</Popconfirm>,
|
description="确认转至退款?"
|
||||||
]
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (!record.id) {
|
||||||
|
message.error('订单ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerRefundorder({
|
||||||
|
id: record.id,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至退款
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
title="转至完成"
|
||||||
|
description="确认转至完成?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
if (!record.id) {
|
||||||
|
message.error('订单ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerCompletedorder({
|
||||||
|
id: record.id,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至完成
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
title="转至待补发"
|
||||||
|
description="确认转至待补发?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{
|
||||||
|
id: record.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'pending_reshipment',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至待补发
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|
@ -1057,31 +1056,31 @@ const Detail: React.FC<{
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
v.state === 'waiting-for-scheduling' ||
|
v.state === 'waiting-for-scheduling' ||
|
||||||
v.state === 'waiting-for-transit'
|
v.state === 'waiting-for-transit'
|
||||||
? [
|
? [
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="取消运单"
|
title="取消运单"
|
||||||
description="确认取消运单?"
|
description="确认取消运单?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
try {
|
try {
|
||||||
const { success, message: errMsg } =
|
const { success, message: errMsg } =
|
||||||
await logisticscontrollerDelshipment({
|
await logisticscontrollerDelshipment({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
tableRef.current?.reload();
|
||||||
|
initRequest();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
}
|
}
|
||||||
tableRef.current?.reload();
|
}}
|
||||||
initRequest();
|
>
|
||||||
} catch (error: any) {
|
<DeleteFilled />
|
||||||
message.error(error.message);
|
取消运单
|
||||||
}
|
</Popconfirm>,
|
||||||
}}
|
]
|
||||||
>
|
|
||||||
<DeleteFilled />
|
|
||||||
取消运单
|
|
||||||
</Popconfirm>,
|
|
||||||
]
|
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -1469,16 +1468,16 @@ const Shipping: React.FC<{
|
||||||
<ProFormList
|
<ProFormList
|
||||||
label="发货产品"
|
label="发货产品"
|
||||||
name="sales"
|
name="sales"
|
||||||
// rules={[
|
// rules={[
|
||||||
// {
|
// {
|
||||||
// required: true,
|
// required: true,
|
||||||
// message: '至少需要一个商品',
|
// message: '至少需要一个商品',
|
||||||
// validator: (_, value) =>
|
// validator: (_, value) =>
|
||||||
// value && value.length > 0
|
// value && value.length > 0
|
||||||
// ? Promise.resolve()
|
// ? Promise.resolve()
|
||||||
// : Promise.reject('至少需要一个商品'),
|
// : Promise.reject('至少需要一个商品'),
|
||||||
// },
|
// },
|
||||||
// ]}
|
// ]}
|
||||||
>
|
>
|
||||||
<ProForm.Group>
|
<ProForm.Group>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
|
|
@ -1903,7 +1902,7 @@ const Shipping: React.FC<{
|
||||||
name="description"
|
name="description"
|
||||||
placeholder="请输入描述"
|
placeholder="请输入描述"
|
||||||
width="lg"
|
width="lg"
|
||||||
// rules={[{ required: true, message: '请输入描述' }]}
|
// rules={[{ required: true, message: '请输入描述' }]}
|
||||||
/>
|
/>
|
||||||
</ProForm.Group>
|
</ProForm.Group>
|
||||||
</ProFormList>
|
</ProFormList>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
export const notAttributes = new Set([
|
export const notAttributes = new Set(['zh-cn', 'en-us', 'category']);
|
||||||
'zh-cn',
|
|
||||||
'en-us',
|
|
||||||
'category'
|
|
||||||
]);
|
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,14 @@ const AttributePage: React.FC = () => {
|
||||||
setLoadingDicts(true);
|
setLoadingDicts(true);
|
||||||
try {
|
try {
|
||||||
const res = await request('/dict/list', { params: { title } });
|
const res = await request('/dict/list', { params: { title } });
|
||||||
// 条件判断,过滤只保留 allowedDictNames 中的字典
|
// 条件判断,确保res是数组再进行过滤
|
||||||
const filtered = (res || []).filter((d: any) => !notAttributes.has(d?.name));
|
const dataList = Array.isArray(res) ? res : res?.data || [];
|
||||||
|
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
|
||||||
setDicts(filtered);
|
setDicts(filtered);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('获取字典列表失败:', error);
|
||||||
message.error('获取字典列表失败');
|
message.error('获取字典列表失败');
|
||||||
|
setDicts([]);
|
||||||
}
|
}
|
||||||
setLoadingDicts(false);
|
setLoadingDicts(false);
|
||||||
};
|
};
|
||||||
|
|
@ -114,16 +117,23 @@ const AttributePage: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedDict?.id) {
|
if (selectedDict?.id) {
|
||||||
const list = await request('/dict/items', {
|
try {
|
||||||
params: {
|
const list = await request('/dict/items', {
|
||||||
dictId: selectedDict.id,
|
params: {
|
||||||
},
|
dictId: selectedDict.id,
|
||||||
});
|
},
|
||||||
const exists =
|
});
|
||||||
Array.isArray(list) && list.some((it: any) => it.id === itemId);
|
// 确保list是数组再进行some操作
|
||||||
if (exists) {
|
const dataList = Array.isArray(list) ? list : list?.data || [];
|
||||||
message.error('删除失败');
|
const exists = dataList.some((it: any) => it.id === itemId);
|
||||||
} else {
|
if (exists) {
|
||||||
|
message.error('删除失败');
|
||||||
|
} else {
|
||||||
|
message.success('删除成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证删除结果失败:', error);
|
||||||
message.success('删除成功');
|
message.success('删除成功');
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
}
|
}
|
||||||
|
|
@ -245,24 +255,44 @@ const AttributePage: React.FC = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { name, title } = params;
|
const { name, title } = params;
|
||||||
const res = await request('/dict/items', {
|
try {
|
||||||
params: {
|
const res = await request('/dict/items', {
|
||||||
dictId: selectedDict.id,
|
params: {
|
||||||
name,
|
dictId: selectedDict.id,
|
||||||
title,
|
name,
|
||||||
},
|
title,
|
||||||
});
|
},
|
||||||
return {
|
});
|
||||||
data: res,
|
// 确保返回的是数组
|
||||||
success: true,
|
const data = Array.isArray(res) ? res : res?.data || [];
|
||||||
};
|
return {
|
||||||
|
data: data,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取字典项失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
search={{
|
search={{
|
||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
}}
|
}}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
options={false}
|
options={{
|
||||||
|
reload: true,
|
||||||
|
density: false,
|
||||||
|
setting: {
|
||||||
|
draggable: true,
|
||||||
|
checkable: true,
|
||||||
|
checkedReset: false,
|
||||||
|
},
|
||||||
|
search: false,
|
||||||
|
fullScreen: false,
|
||||||
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
key={selectedDict?.id}
|
key={selectedDict?.id}
|
||||||
headerTitle={
|
headerTitle={
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,10 @@ const CategoryPage: React.FC = () => {
|
||||||
</Popconfirm>,
|
</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>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -609,7 +609,7 @@ const List: React.FC = () => {
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
// 新建按钮
|
// 新建按钮
|
||||||
<CreateForm tableRef={actionRef} />,
|
<CreateForm tableRef={actionRef} />,
|
||||||
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
|
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
|
||||||
<Upload
|
<Upload
|
||||||
name="file"
|
name="file"
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,18 @@ const PermutationPage: React.FC = () => {
|
||||||
// 2. Fetch Attribute Values (Dict Items)
|
// 2. Fetch Attribute Values (Dict Items)
|
||||||
const valuesMap: Record<string, any[]> = {};
|
const valuesMap: Record<string, any[]> = {};
|
||||||
for (const attr of attrs) {
|
for (const attr of attrs) {
|
||||||
const dictId = attr.dict?.id || attr.dictId;
|
// 使用属性中直接包含的items,而不是额外请求
|
||||||
if (dictId) {
|
if (attr.items && Array.isArray(attr.items)) {
|
||||||
const itemsRes = await request('/dict/items', {
|
valuesMap[attr.name] = attr.items;
|
||||||
params: { dictId },
|
} else {
|
||||||
});
|
// 如果没有items,尝试通过dictId获取
|
||||||
valuesMap[attr.name] = itemsRes || [];
|
const dictId = attr.dict?.id || attr.dictId;
|
||||||
|
if (dictId) {
|
||||||
|
const itemsRes = await request('/dict/items', {
|
||||||
|
params: { dictId },
|
||||||
|
});
|
||||||
|
valuesMap[attr.name] = itemsRes || [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAttributeValues(valuesMap);
|
setAttributeValues(valuesMap);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,16 @@ import {
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { request } from '@umijs/max';
|
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 React, { useEffect, useRef, useState } from 'react';
|
||||||
import EditForm from '../List/EditForm';
|
import EditForm from '../List/EditForm';
|
||||||
|
|
||||||
|
|
@ -89,7 +98,13 @@ const ProductSyncPage: React.FC = () => {
|
||||||
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
|
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
|
||||||
const [syncProgress, setSyncProgress] = useState(0);
|
const [syncProgress, setSyncProgress] = useState(0);
|
||||||
const [syncing, setSyncing] = useState(false);
|
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 产品
|
// 初始化数据:获取站点和所有 WP 产品
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -197,41 +212,51 @@ const ProductSyncPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 批量同步产品到指定站点
|
// 批量同步产品到指定站点
|
||||||
const batchSyncProducts = async () => {
|
const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => {
|
||||||
if (!selectedSiteId) {
|
if (!selectedSiteId) {
|
||||||
message.error('请选择要同步到的站点');
|
message.error('请选择要同步到的站点');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetSite = sites.find(site => site.id === selectedSiteId);
|
const targetSite = sites.find((site) => site.id === selectedSiteId);
|
||||||
if (!targetSite) {
|
if (!targetSite) {
|
||||||
message.error('选择的站点不存在');
|
message.error('选择的站点不存在');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有传入产品列表,则使用选中的产品
|
||||||
|
let products = productsToSync || selectedRows;
|
||||||
|
|
||||||
|
// 如果既没有传入产品也没有选中产品,则同步所有产品
|
||||||
|
if (!products || products.length === 0) {
|
||||||
|
try {
|
||||||
|
const { data, success } = await productcontrollerGetproductlist({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10000, // 获取所有产品
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (!success || !data?.items) {
|
||||||
|
message.error('获取产品列表失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
products = data.items as ProductWithWP[];
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取产品列表失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
setSyncProgress(0);
|
setSyncProgress(0);
|
||||||
setSyncResults({ success: 0, failed: 0, errors: [] });
|
setSyncResults({ success: 0, failed: 0, errors: [] });
|
||||||
|
|
||||||
|
const totalProducts = products.length;
|
||||||
|
let processed = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取所有产品
|
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10000, // 获取所有产品
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
if (!success || !data?.items) {
|
|
||||||
message.error('获取产品列表失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const products = data.items as ProductWithWP[];
|
|
||||||
const totalProducts = products.length;
|
|
||||||
let processed = 0;
|
|
||||||
let successCount = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
// 逐个同步产品
|
// 逐个同步产品
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -239,7 +264,10 @@ const ProductSyncPage: React.FC = () => {
|
||||||
let siteProductSku = '';
|
let siteProductSku = '';
|
||||||
if (product.siteSkus && product.siteSkus.length > 0) {
|
if (product.siteSkus && product.siteSkus.length > 0) {
|
||||||
const siteSkuInfo = product.siteSkus.find((sku: any) => {
|
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) {
|
if (siteSkuInfo) {
|
||||||
siteProductSku = siteSkuInfo.siteSku;
|
siteProductSku = siteSkuInfo.siteSku;
|
||||||
|
|
@ -247,15 +275,15 @@ const ProductSyncPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到实际的siteSku,则根据模板生成
|
// 如果没有找到实际的siteSku,则根据模板生成
|
||||||
const expectedSku = siteProductSku || (
|
const expectedSku =
|
||||||
skuTemplate
|
siteProductSku ||
|
||||||
|
(skuTemplate
|
||||||
? renderSiteSku(skuTemplate, { site: targetSite, product })
|
? renderSiteSku(skuTemplate, { site: targetSite, product })
|
||||||
: `${targetSite.skuPrefix || ''}-${product.sku}`
|
: `${targetSite.skuPrefix || ''}-${product.sku}`);
|
||||||
);
|
|
||||||
|
|
||||||
// 检查是否已存在
|
// 检查是否已存在
|
||||||
const existingProduct = wpProductMap.get(expectedSku);
|
const existingProduct = wpProductMap.get(expectedSku);
|
||||||
|
|
||||||
// 准备同步数据
|
// 准备同步数据
|
||||||
const syncData = {
|
const syncData = {
|
||||||
name: product.name,
|
name: product.name,
|
||||||
|
|
@ -272,10 +300,13 @@ const ProductSyncPage: React.FC = () => {
|
||||||
let res;
|
let res;
|
||||||
if (existingProduct?.externalProductId) {
|
if (existingProduct?.externalProductId) {
|
||||||
// 更新现有产品
|
// 更新现有产品
|
||||||
res = await request(`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, {
|
res = await request(
|
||||||
method: 'PUT',
|
`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`,
|
||||||
data: syncData,
|
{
|
||||||
});
|
method: 'PUT',
|
||||||
|
data: syncData,
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 创建新产品
|
// 创建新产品
|
||||||
res = await request(`/site-api/${targetSite.id}/products`, {
|
res = await request(`/site-api/${targetSite.id}/products`, {
|
||||||
|
|
@ -300,7 +331,6 @@ const ProductSyncPage: React.FC = () => {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
|
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
|
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
|
||||||
|
|
@ -311,16 +341,17 @@ const ProductSyncPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSyncResults({ success: successCount, failed: failedCount, errors });
|
setSyncResults({ success: successCount, failed: failedCount, errors });
|
||||||
|
|
||||||
if (failedCount === 0) {
|
if (failedCount === 0) {
|
||||||
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
|
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
|
||||||
} else {
|
} else {
|
||||||
message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`);
|
message.warning(
|
||||||
|
`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新表格
|
// 刷新表格
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error('批量同步失败: ' + (error.message || error.toString()));
|
message.error('批量同步失败: ' + (error.message || error.toString()));
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -453,7 +484,9 @@ const ProductSyncPage: React.FC = () => {
|
||||||
const siteSkuInfo = record.siteSkus.find((sku: any) => {
|
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) {
|
if (siteSkuInfo) {
|
||||||
siteProductSku = siteSkuInfo.siteSku;
|
siteProductSku = siteSkuInfo.siteSku;
|
||||||
|
|
@ -461,18 +494,21 @@ const ProductSyncPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU
|
// 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU
|
||||||
const expectedSku = siteProductSku || (
|
const expectedSku =
|
||||||
skuTemplate
|
siteProductSku ||
|
||||||
|
(skuTemplate
|
||||||
? renderSiteSku(skuTemplate, { site, product: record })
|
? renderSiteSku(skuTemplate, { site, product: record })
|
||||||
: `${site.skuPrefix || ''}-${record.sku}`
|
: `${site.skuPrefix || ''}-${record.sku}`);
|
||||||
);
|
|
||||||
|
|
||||||
// 尝试用确定的SKU获取WP产品
|
// 尝试用确定的SKU获取WP产品
|
||||||
let wpProduct = wpProductMap.get(expectedSku);
|
let wpProduct = wpProductMap.get(expectedSku);
|
||||||
|
|
||||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
||||||
if (!wpProduct && siteProductSku && skuTemplate) {
|
if (!wpProduct && siteProductSku && skuTemplate) {
|
||||||
const templateSku = renderSiteSku(skuTemplate, { site, product: record });
|
const templateSku = renderSiteSku(skuTemplate, {
|
||||||
|
site,
|
||||||
|
product: record,
|
||||||
|
});
|
||||||
wpProduct = wpProductMap.get(templateSku);
|
wpProduct = wpProductMap.get(templateSku);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,12 +526,12 @@ const ProductSyncPage: React.FC = () => {
|
||||||
return await syncProductToSite(values, record, site);
|
return await syncProductToSite(values, record, site);
|
||||||
}}
|
}}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
sku: siteProductSku || (
|
sku:
|
||||||
skuTemplate
|
siteProductSku ||
|
||||||
? renderSiteSku(skuTemplate, { site, product: record })
|
(skuTemplate
|
||||||
: `${site.skuPrefix || ''}-${record.sku}`
|
? renderSiteSku(skuTemplate, { site, product: record })
|
||||||
),
|
: `${site.skuPrefix || ''}-${record.sku}`),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ProFormText
|
<ProFormText
|
||||||
name="sku"
|
name="sku"
|
||||||
|
|
@ -587,36 +623,46 @@ const ProductSyncPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card title="商品同步状态" className="product-sync-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>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ProTable<ProductWithWP>
|
<ProTable<ProductWithWP>
|
||||||
columns={generateColumns()}
|
columns={generateColumns()}
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
rowKey="id"
|
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) => {
|
request={async (params, sort, filter) => {
|
||||||
// 调用本地获取产品列表 API
|
// 调用本地获取产品列表 API
|
||||||
const { data, success } = await productcontrollerGetproductlist({
|
const { data, success } = await productcontrollerGetproductlist({
|
||||||
|
|
@ -650,7 +696,7 @@ const ProductSyncPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
dateFormatter="string"
|
dateFormatter="string"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 批量同步模态框 */}
|
{/* 批量同步模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
title="批量同步产品"
|
title="批量同步产品"
|
||||||
|
|
@ -661,10 +707,19 @@ const ProductSyncPage: React.FC = () => {
|
||||||
maskClosable={!syncing}
|
maskClosable={!syncing}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<p>目标站点:<strong>{sites.find(s => s.id === selectedSiteId)?.name}</strong></p>
|
<p>
|
||||||
<p>此操作将同步所有库存产品到指定站点,请确认是否继续?</p>
|
目标站点:
|
||||||
|
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
|
||||||
|
</p>
|
||||||
|
{selectedRows.length > 0 ? (
|
||||||
|
<p>
|
||||||
|
已选择 <strong>{selectedRows.length}</strong> 个产品进行同步
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>此操作将同步所有库存产品到指定站点,请确认是否继续?</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{syncing && (
|
{syncing && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ marginBottom: 8 }}>同步进度:</div>
|
<div style={{ marginBottom: 8 }}>同步进度:</div>
|
||||||
|
|
@ -674,32 +729,37 @@ const ProductSyncPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{syncResults.errors.length > 0 && (
|
{syncResults.errors.length > 0 && (
|
||||||
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
|
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
|
||||||
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
||||||
{syncResults.errors.slice(0, 10).map((error, index) => (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{syncResults.errors.length > 10 && (
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setBatchSyncModalVisible(false)}
|
onClick={() => setBatchSyncModalVisible(false)}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
style={{ marginRight: 8 }}
|
style={{ marginRight: 8 }}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={batchSyncProducts}
|
onClick={() => batchSyncProducts()}
|
||||||
loading={syncing}
|
loading={syncing}
|
||||||
disabled={syncing}
|
disabled={syncing}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
|
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 React, { useEffect, useState } from 'react';
|
||||||
import type { SiteItem } from '../List/index';
|
import type { SiteItem } from '../List/index';
|
||||||
import EditSiteForm from './EditSiteForm';
|
import EditSiteForm from './EditSiteForm';
|
||||||
import Sider from 'antd/es/layout/Sider';
|
|
||||||
|
|
||||||
const ShopLayout: React.FC = () => {
|
const ShopLayout: React.FC = () => {
|
||||||
const [sites, setSites] = useState<any[]>([]);
|
const [sites, setSites] = useState<any[]>([]);
|
||||||
|
|
@ -91,10 +91,7 @@ const ShopLayout: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer header={{ title: null, breadcrumb: undefined }}>
|
||||||
header={{ title: null, breadcrumb: undefined }}
|
|
||||||
>
|
|
||||||
|
|
||||||
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
|
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
|
||||||
<Col span={4} style={{ height: '100%' }}>
|
<Col span={4} style={{ height: '100%' }}>
|
||||||
<Sider
|
<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 { LinkOutlined } from '@ant-design/icons';
|
||||||
import { PageHeader } from '@ant-design/pro-layout';
|
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 {
|
interface LinkItem {
|
||||||
|
|
@ -20,7 +19,7 @@ const LinksPage: React.FC = () => {
|
||||||
// 获取链接列表的函数
|
// 获取链接列表的函数
|
||||||
const fetchLinks = async () => {
|
const fetchLinks = async () => {
|
||||||
if (!siteId) return;
|
if (!siteId) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await request(`/site-api/${siteId}/links`);
|
const response = await request(`/site-api/${siteId}/links`);
|
||||||
|
|
@ -48,10 +47,7 @@ const LinksPage: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader title="站点链接" breadcrumb={{ items: [] }} />
|
||||||
title="站点链接"
|
|
||||||
breadcrumb={{ items: [] }}
|
|
||||||
/>
|
|
||||||
<Card
|
<Card
|
||||||
title="常用链接"
|
title="常用链接"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
|
|
@ -76,7 +72,7 @@ const LinksPage: React.FC = () => {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
访问
|
访问
|
||||||
</Button>
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
|
|
@ -100,4 +96,4 @@ const LinksPage: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LinksPage;
|
export default LinksPage;
|
||||||
|
|
|
||||||
|
|
@ -208,30 +208,32 @@ const OrdersPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
发货
|
发货
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
{record.status === 'completed' && (
|
||||||
title="确定取消发货?"
|
<Popconfirm
|
||||||
description="取消发货后订单状态将恢复为处理中"
|
title="确定取消发货?"
|
||||||
onConfirm={async () => {
|
description="取消发货后订单状态将恢复为处理中"
|
||||||
try {
|
onConfirm={async () => {
|
||||||
const res = await request(
|
try {
|
||||||
`/site-api/${siteId}/orders/${record.id}/cancel-ship`,
|
const res = await request(
|
||||||
{ method: 'POST' },
|
`/site-api/${siteId}/orders/${record.id}/cancel-ship`,
|
||||||
);
|
{ method: 'POST' },
|
||||||
if (res.success) {
|
);
|
||||||
message.success('取消发货成功');
|
if (res.success) {
|
||||||
actionRef.current?.reload();
|
message.success('取消发货成功');
|
||||||
} else {
|
actionRef.current?.reload();
|
||||||
message.error(res.message || '取消发货失败');
|
} else {
|
||||||
|
message.error(res.message || '取消发货失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error('取消发货失败');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
}}
|
||||||
message.error('取消发货失败');
|
>
|
||||||
}
|
<Button type="link" danger title="取消发货">
|
||||||
}}
|
取消发货
|
||||||
>
|
</Button>
|
||||||
<Button type="link" danger title="取消发货">
|
</Popconfirm>
|
||||||
取消发货
|
)}
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确定删除订单?"
|
title="确定删除订单?"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,9 @@ const ProductsPage: React.FC = () => {
|
||||||
<strong>分类:</strong> {record.erpProduct.category.name}
|
<strong>分类:</strong> {record.erpProduct.category.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<strong>库存:</strong> {record.erpProduct.stock_quantity ?? '-'}
|
<strong>库存:</strong> {record.erpProduct.stock_quantity ?? '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
Select,
|
|
||||||
message,
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
import {
|
||||||
siteapicontrollerCreatereview,
|
siteapicontrollerCreatereview,
|
||||||
siteapicontrollerUpdatereview,
|
siteapicontrollerUpdatereview,
|
||||||
} from '@/servers/api/siteApi';
|
} from '@/servers/api/siteApi';
|
||||||
|
import { Form, Input, InputNumber, Modal, Select, message } from 'antd';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
@ -52,7 +45,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
const handleSubmit = async (values: any) => {
|
const handleSubmit = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
// 更新评论
|
// 更新评论
|
||||||
response = await siteapicontrollerUpdatereview(
|
response = await siteapicontrollerUpdatereview(
|
||||||
|
|
@ -64,7 +57,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
review: values.content,
|
review: values.content,
|
||||||
rating: values.rating,
|
rating: values.rating,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 创建新评论
|
// 创建新评论
|
||||||
|
|
@ -78,7 +71,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
rating: values.rating,
|
rating: values.rating,
|
||||||
author: values.author,
|
author: values.author,
|
||||||
author_email: values.email,
|
author_email: values.email,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,27 +129,27 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
label="邮箱"
|
label="邮箱"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入邮箱' },
|
{ required: true, message: '请输入邮箱' },
|
||||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入邮箱" />
|
<Input placeholder="请输入邮箱" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="content"
|
name="content"
|
||||||
label="评论内容"
|
label="评论内容"
|
||||||
rules={[{ required: true, message: '请输入评论内容' }]}
|
rules={[{ required: true, message: '请输入评论内容' }]}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
placeholder="请输入评论内容"
|
placeholder="请输入评论内容"
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
showCount
|
showCount
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="rating"
|
name="rating"
|
||||||
label="评分"
|
label="评分"
|
||||||
|
|
@ -170,7 +163,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="status"
|
name="status"
|
||||||
label="状态"
|
label="状态"
|
||||||
|
|
@ -188,4 +181,4 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReviewForm;
|
export default ReviewForm;
|
||||||
|
|
|
||||||
|
|
@ -101,14 +101,22 @@ const ReviewsPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 确保 response.data.items 是数组
|
// 确保 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
|
// 确保每个 item 有有效的 id
|
||||||
const processedItems = items.map((item, index) => ({
|
const processedItems = items.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
// 如果 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 是对象,转换为字符串
|
||||||
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 {
|
return {
|
||||||
data: processedItems,
|
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 {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
ProCard,
|
ProCard,
|
||||||
|
|
@ -6,22 +11,32 @@ import {
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { useParams } from '@umijs/max';
|
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';
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
const WebhooksPage: React.FC = () => {
|
const WebhooksPage: React.FC = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const siteId = Number(params.siteId);
|
const siteId = Number(params.siteId);
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [isEditMode, setIsEditMode] = 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();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
// webhook主题选项
|
// webhook主题选项
|
||||||
const webhookTopics = [
|
const webhookTopics = [
|
||||||
{ label: '订单创建', value: 'order.created' },
|
{ label: '订单创建', value: 'order.created' },
|
||||||
|
|
@ -34,7 +49,7 @@ const WebhooksPage: React.FC = () => {
|
||||||
{ label: '客户更新', value: 'customer.updated' },
|
{ label: '客户更新', value: 'customer.updated' },
|
||||||
{ label: '客户删除', value: 'customer.deleted' },
|
{ label: '客户删除', value: 'customer.deleted' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// webhook状态选项
|
// webhook状态选项
|
||||||
const webhookStatuses = [
|
const webhookStatuses = [
|
||||||
{ label: '活跃', value: 'active' },
|
{ label: '活跃', value: 'active' },
|
||||||
|
|
@ -78,13 +93,13 @@ const WebhooksPage: React.FC = () => {
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
|
||||||
// 准备提交数据
|
// 准备提交数据
|
||||||
const webhookData = {
|
const webhookData = {
|
||||||
...values,
|
...values,
|
||||||
siteId,
|
siteId,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (isEditMode && currentWebhook?.id) {
|
if (isEditMode && currentWebhook?.id) {
|
||||||
// 更新webhook
|
// 更新webhook
|
||||||
|
|
@ -96,7 +111,7 @@ const WebhooksPage: React.FC = () => {
|
||||||
// 创建新webhook
|
// 创建新webhook
|
||||||
response = await siteapicontrollerCreatewebhook(webhookData);
|
response = await siteapicontrollerCreatewebhook(webhookData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
message.success(isEditMode ? '更新成功' : '创建成功');
|
message.success(isEditMode ? '更新成功' : '创建成功');
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
|
|
@ -114,10 +129,25 @@ const WebhooksPage: React.FC = () => {
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||||
{ title: '主题', dataIndex: 'topic', key: 'topic' },
|
{ 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: '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: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
|
|
@ -184,12 +214,17 @@ const WebhooksPage: React.FC = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 确保 response.data.items 是数组
|
// 确保 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
|
// 确保每个 item 有有效的 id
|
||||||
const processedItems = items.map((item, index) => ({
|
const processedItems = items.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
|
// 如果 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 {
|
return {
|
||||||
data: processedItems,
|
data: processedItems,
|
||||||
|
|
@ -211,10 +246,7 @@ const WebhooksPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
headerTitle="Webhooks列表"
|
headerTitle="Webhooks列表"
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
<Button
|
<Button type="primary" onClick={showCreateModal}>
|
||||||
type="primary"
|
|
||||||
onClick={showCreateModal}
|
|
||||||
>
|
|
||||||
新建Webhook
|
新建Webhook
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
|
|
@ -245,7 +277,10 @@ const WebhooksPage: React.FC = () => {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="name"
|
name="name"
|
||||||
label="名称"
|
label="名称"
|
||||||
rules={[{ required: true, message: '请输入webhook名称' }, { max: 100, message: '名称不能超过100个字符' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入webhook名称' },
|
||||||
|
{ max: 100, message: '名称不能超过100个字符' },
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入webhook名称" />
|
<Input placeholder="请输入webhook名称" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -265,7 +300,10 @@ const WebhooksPage: React.FC = () => {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="delivery_url"
|
name="delivery_url"
|
||||||
label="回调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" />
|
<Input placeholder="请输入回调URL,如:https://example.com/webhook" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -283,10 +321,7 @@ const WebhooksPage: React.FC = () => {
|
||||||
label="状态"
|
label="状态"
|
||||||
rules={[{ required: true, message: '请选择webhook状态' }]}
|
rules={[{ required: true, message: '请选择webhook状态' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select placeholder="请选择webhook状态" options={webhookStatuses} />
|
||||||
placeholder="请选择webhook状态"
|
|
||||||
options={webhookStatuses}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -294,4 +329,4 @@ const WebhooksPage: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WebhooksPage;
|
export default WebhooksPage;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import {
|
||||||
templatecontrollerCreatetemplate,
|
templatecontrollerCreatetemplate,
|
||||||
templatecontrollerDeletetemplate,
|
templatecontrollerDeletetemplate,
|
||||||
templatecontrollerGettemplatelist,
|
templatecontrollerGettemplatelist,
|
||||||
templatecontrollerRendertemplate,
|
|
||||||
templatecontrollerRendertemplatedirect,
|
templatecontrollerRendertemplatedirect,
|
||||||
templatecontrollerUpdatetemplate,
|
templatecontrollerUpdatetemplate,
|
||||||
} from '@/servers/api/template';
|
} from '@/servers/api/template';
|
||||||
|
|
@ -75,12 +74,10 @@ const useTemplatePreview = () => {
|
||||||
renderedResult,
|
renderedResult,
|
||||||
handlePreview,
|
handlePreview,
|
||||||
refreshPreview,
|
refreshPreview,
|
||||||
setPreviewData
|
setPreviewData,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const List: React.FC = () => {
|
const List: React.FC = () => {
|
||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
@ -176,7 +173,8 @@ const CreateForm: React.FC<{
|
||||||
}> = ({ tableRef }) => {
|
}> = ({ tableRef }) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = ProForm.useForm();
|
const [form] = ProForm.useForm();
|
||||||
const { renderedResult, handlePreview, refreshPreview } = useTemplatePreview();
|
const { renderedResult, handlePreview, refreshPreview } =
|
||||||
|
useTemplatePreview();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerForm<API.CreateTemplateDTO>
|
<DrawerForm<API.CreateTemplateDTO>
|
||||||
|
|
@ -261,10 +259,19 @@ const CreateForm: React.FC<{
|
||||||
/>
|
/>
|
||||||
</ProForm.Item>
|
</ProForm.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>实时预览</Typography.Title>
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
|
实时预览
|
||||||
|
</Typography.Title>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
|
|
@ -283,7 +290,7 @@ const CreateForm: React.FC<{
|
||||||
height: '600px',
|
height: '600px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
||||||
|
|
@ -302,7 +309,8 @@ const UpdateForm: React.FC<{
|
||||||
}> = ({ tableRef, values: initialValues }) => {
|
}> = ({ tableRef, values: initialValues }) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = ProForm.useForm();
|
const [form] = ProForm.useForm();
|
||||||
const { renderedResult, handlePreview, refreshPreview, setPreviewData } = useTemplatePreview();
|
const { renderedResult, handlePreview, refreshPreview, setPreviewData } =
|
||||||
|
useTemplatePreview();
|
||||||
|
|
||||||
// 组件挂载时初始化预览数据
|
// 组件挂载时初始化预览数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -310,12 +318,12 @@ const UpdateForm: React.FC<{
|
||||||
setPreviewData({
|
setPreviewData({
|
||||||
name: initialValues.name,
|
name: initialValues.name,
|
||||||
value: initialValues.value,
|
value: initialValues.value,
|
||||||
testData: initialValues.testData
|
testData: initialValues.testData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [initialValues, setPreviewData]);
|
}, [initialValues, setPreviewData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerForm<API.UpdateTemplateDTO>
|
<DrawerForm<API.UpdateTemplateDTO>
|
||||||
title="编辑"
|
title="编辑"
|
||||||
form={form}
|
form={form}
|
||||||
|
|
@ -403,10 +411,19 @@ const UpdateForm: React.FC<{
|
||||||
/>
|
/>
|
||||||
</ProForm.Item>
|
</ProForm.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
<div
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>实时预览</Typography.Title>
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
|
实时预览
|
||||||
|
</Typography.Title>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
|
|
@ -425,7 +442,7 @@ const UpdateForm: React.FC<{
|
||||||
height: '600px',
|
height: '600px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,8 @@ const WpToolPage: React.FC = () => {
|
||||||
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
|
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
|
||||||
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
|
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
|
||||||
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
|
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
|
||||||
|
const [isConfigLoading, setIsConfigLoading] = useState(false); // 是否正在加载配置
|
||||||
|
const [configLoadAttempts, setConfigLoadAttempts] = useState(0); // 配置加载重试次数
|
||||||
const [config, setConfig] = useState<TagConfig>({
|
const [config, setConfig] = useState<TagConfig>({
|
||||||
// 动态配置
|
// 动态配置
|
||||||
brands: [],
|
brands: [],
|
||||||
|
|
@ -237,22 +239,34 @@ const WpToolPage: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAllConfigs = async () => {
|
const fetchAllConfigs = async () => {
|
||||||
try {
|
try {
|
||||||
|
message.loading({ content: '正在加载字典配置...', key: 'loading-config' });
|
||||||
|
|
||||||
// 1. 获取所有字典列表以找到对应的 ID
|
// 1. 获取所有字典列表以找到对应的 ID
|
||||||
const dictList = await request('/dict/list');
|
const dictListResponse = await request('/dict/list');
|
||||||
|
// 处理后端统一响应格式
|
||||||
|
const dictList = dictListResponse?.data || dictListResponse || [];
|
||||||
|
|
||||||
// 2. 根据字典名称获取字典项
|
// 2. 根据字典名称获取字典项
|
||||||
const getItems = async (dictName: string) => {
|
const getItems = async (dictName: string) => {
|
||||||
const dict = dictList.find((d: any) => d.name === dictName);
|
try {
|
||||||
if (!dict) {
|
const dict = dictList.find((d: any) => d.name === dictName);
|
||||||
console.warn(`Dictionary ${dictName} not found`);
|
if (!dict) {
|
||||||
|
console.warn(`Dictionary ${dictName} not found`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const response = await request('/dict/items', {
|
||||||
|
params: { dictId: dict.id },
|
||||||
|
});
|
||||||
|
// 处理后端统一响应格式,获取数据数组
|
||||||
|
const items = response?.data || response || [];
|
||||||
|
return items.map((item: any) => item.name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch items for ${dictName}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const res = await request('/dict/items', {
|
|
||||||
params: { dictId: dict.id },
|
|
||||||
});
|
|
||||||
return res.map((item: any) => item.name);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 3. 并行获取所有字典项
|
||||||
const [
|
const [
|
||||||
brands,
|
brands,
|
||||||
fruitKeys,
|
fruitKeys,
|
||||||
|
|
@ -264,9 +278,9 @@ const WpToolPage: React.FC = () => {
|
||||||
categoryKeys,
|
categoryKeys,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getItems('brand'),
|
getItems('brand'),
|
||||||
getItems('fruit'), // 假设字典名为 fruit
|
getItems('fruit'),
|
||||||
getItems('mint'), // 假设字典名为 mint
|
getItems('mint'),
|
||||||
getItems('flavor'), // 假设字典名为 flavor
|
getItems('flavor'),
|
||||||
getItems('strength'),
|
getItems('strength'),
|
||||||
getItems('size'),
|
getItems('size'),
|
||||||
getItems('humidity'),
|
getItems('humidity'),
|
||||||
|
|
@ -283,11 +297,19 @@ const WpToolPage: React.FC = () => {
|
||||||
humidityKeys,
|
humidityKeys,
|
||||||
categoryKeys,
|
categoryKeys,
|
||||||
};
|
};
|
||||||
|
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
form.setFieldsValue(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) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch configs:', 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 */
|
/** 此处后端没有提供注释 GET /customer/gettags */
|
||||||
export async function customercontrollerGettags(options?: {
|
export async function customercontrollerGettags(options?: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
@ -71,3 +86,18 @@ export async function customercontrollerSetrate(
|
||||||
...(options || {}),
|
...(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 */
|
/** 此处后端没有提供注释 GET /site/all */
|
||||||
export async function sitecontrollerAll(options?: { [key: string]: any }) {
|
export async function sitecontrollerAll(options?: { [key: string]: any }) {
|
||||||
return request<API.WpSitesResponse>('/site/all', {
|
return request<API.SitesResponse>('/site/all', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,18 @@ declare namespace API {
|
||||||
customerId?: number;
|
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 = {
|
type CustomerTagDTO = {
|
||||||
email?: string;
|
email?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
|
@ -1893,6 +1905,17 @@ declare namespace API {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SitesResponse = {
|
||||||
|
/** 状态码 */
|
||||||
|
code?: number;
|
||||||
|
/** 是否成功 */
|
||||||
|
success?: boolean;
|
||||||
|
/** 消息内容 */
|
||||||
|
message?: string;
|
||||||
|
/** 响应数据 */
|
||||||
|
data?: SiteConfig[];
|
||||||
|
};
|
||||||
|
|
||||||
type statisticscontrollerGetinativeusersbymonthParams = {
|
type statisticscontrollerGetinativeusersbymonthParams = {
|
||||||
month?: string;
|
month?: string;
|
||||||
};
|
};
|
||||||
|
|
@ -3014,15 +3037,4 @@ declare namespace API {
|
||||||
/** 数据列表 */
|
/** 数据列表 */
|
||||||
items?: WpProductDTO[];
|
items?: WpProductDTO[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type WpSitesResponse = {
|
|
||||||
/** 状态码 */
|
|
||||||
code?: number;
|
|
||||||
/** 是否成功 */
|
|
||||||
success?: boolean;
|
|
||||||
/** 消息内容 */
|
|
||||||
message?: string;
|
|
||||||
/** 响应数据 */
|
|
||||||
data?: SiteConfig[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue