forked from yoone/WEB
1
0
Fork 0

feat(客户): 新增客户数据分析列表页面

refactor(产品): 优化属性字典项获取逻辑,增加错误处理

fix(订单): 修复取消发货按钮在已完成订单中显示的问题

style: 统一代码格式,修复缩进和导入顺序问题

perf(字典): 优化字典配置加载逻辑,增加重试机制

docs(API): 更新API类型定义,添加客户统计相关接口

chore: 更新package.json文件格式
This commit is contained in:
tikkhun 2025-12-23 19:38:51 +08:00 committed by 黄珑
parent 39a64d3714
commit 8524cc1ec0
25 changed files with 1426 additions and 619 deletions

View File

@ -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',
},
], ],
}, },
{ {

View File

@ -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),

View File

@ -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;

View File

@ -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;

View File

@ -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;
@ -37,8 +37,10 @@ const DictPage: React.FC = () => {
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,8 +159,8 @@ 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('删除成功');
@ -166,7 +168,7 @@ const DictPage: React.FC = () => {
setTimeout(() => { setTimeout(() => {
actionRef.current?.reload(); actionRef.current?.reload();
}, 100); }, 100);
} catch (error:any) { } catch (error: any) {
message.error(`删除失败,原因为:${error.message}`); message.error(`删除失败,原因为:${error.message}`);
} }
}; };
@ -176,7 +178,7 @@ const DictPage: React.FC = () => {
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) {
@ -199,7 +201,10 @@ 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 || '更新失败');
@ -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}
@ -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>

View File

@ -456,7 +456,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']
@ -496,7 +495,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({
@ -511,15 +509,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') {
@ -608,33 +607,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" />,
@ -653,152 +652,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>,
]
: []), : []),
]} ]}
> >
@ -1060,31 +1059,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>,
]
: [] : []
} }
> >
@ -1472,16 +1471,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
@ -1906,7 +1905,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>

View File

@ -1,5 +1 @@
export const notAttributes = new Set([ export const notAttributes = new Set(['zh-cn', 'en-us', 'category']);
'zh-cn',
'en-us',
'category'
]);

View File

@ -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={

View File

@ -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>
)} )}
/> />

View File

@ -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"

View File

@ -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);

View File

@ -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,11 +275,11 @@ 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);
@ -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 || '未知错误'}`);
@ -315,12 +345,13 @@ const ProductSyncPage: React.FC = () => {
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({
@ -661,8 +707,17 @@ 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 && (
@ -679,12 +734,17 @@ const ProductSyncPage: React.FC = () => {
<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>
)} )}
@ -699,7 +759,7 @@ const ProductSyncPage: React.FC = () => {
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={batchSyncProducts} onClick={() => batchSyncProducts()}
loading={syncing} loading={syncing}
disabled={syncing} disabled={syncing}
> >

View File

@ -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

View File

@ -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 {
@ -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

View File

@ -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 () => {

View File

@ -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>
); );
} }

View File

@ -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;
@ -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,7 +129,7 @@ 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="请输入邮箱" />

View File

@ -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,

View File

@ -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,7 +11,16 @@ 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 = () => {
@ -17,7 +31,8 @@ const WebhooksPage: React.FC = () => {
// 模态框状态 // 模态框状态
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();
@ -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="请输入回调URLhttps://example.com/webhook" /> <Input placeholder="请输入回调URLhttps://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>

View File

@ -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>
@ -263,8 +261,17 @@ const CreateForm: React.FC<{
</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}
@ -405,8 +413,17 @@ const UpdateForm: React.FC<{
</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 }}>

View File

@ -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' });
} }
}; };

View File

@ -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 || {}),
});
}

View File

@ -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 || {}),
}); });

View File

@ -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[];
};
} }