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
parent ffa77560fa
commit 1a68e469dd
25 changed files with 1426 additions and 619 deletions

View File

@ -131,6 +131,11 @@ export default defineConfig({
path: '/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
try {
const res = await request('/dict/list');
const filtered = (res || []).filter((d: any) => !notAttributes.has(d.name));
const filtered = (res || []).filter(
(d: any) => !notAttributes.has(d.name),
);
// Filter out already added attributes
const existingDictIds = new Set(
categoryAttributes.map((ca: any) => ca.dict.id),

View File

@ -1,11 +1,12 @@
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
customercontrollerAddtag,
customercontrollerDeltag,
customercontrollerGetcustomerlist,
customercontrollerGettags,
customercontrollerSetrate,
customercontrollerSynccustomers,
} from '@/servers/api/customer';
import { sitecontrollerAll } from '@/servers/api/site';
import {
ActionType,
ModalForm,
@ -14,97 +15,219 @@ import {
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Rate, Space, Tag } from 'antd';
import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
const ListPage: React.FC = () => {
// 地址格式化函数
const formatAddress = (address: any) => {
if (!address) return '-';
if (typeof address === 'string') {
try {
address = JSON.parse(address);
} catch (e) {
return address;
}
}
const {
first_name,
last_name,
company,
address_1,
address_2,
city,
state,
postcode,
country,
phone: addressPhone,
email: addressEmail
} = address;
const parts = [];
// 姓名
const fullName = [first_name, last_name].filter(Boolean).join(' ');
if (fullName) parts.push(fullName);
// 公司
if (company) parts.push(company);
// 地址行
if (address_1) parts.push(address_1);
if (address_2) parts.push(address_2);
// 城市、州、邮编
const locationParts = [city, state, postcode].filter(Boolean).join(', ');
if (locationParts) parts.push(locationParts);
// 国家
if (country) parts.push(country);
// 联系方式
if (addressPhone) parts.push(`电话: ${addressPhone}`);
if (addressEmail) parts.push(`邮箱: ${addressEmail}`);
return parts.join(', ');
};
// 地址卡片组件
const AddressCell: React.FC<{ address: any; title: string }> = ({ address, title }) => {
const formattedAddress = formatAddress(address);
if (formattedAddress === '-') {
return <span>-</span>;
}
return (
<Tooltip
title={
<div style={{ maxWidth: 300, whiteSpace: 'pre-line' }}>
<strong>{title}:</strong>
<br />
{formattedAddress}
</div>
}
placement="topLeft"
>
<div style={{
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}>
{formattedAddress}
</div>
</Tooltip>
);
};
const CustomerList: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const columns: ProColumns[] = [
const [syncModalVisible, setSyncModalVisible] = useState(false);
const [syncLoading, setSyncLoading] = useState(false);
const [sites, setSites] = useState<any[]>([]); // 添加站点数据状态
// 获取站点数据
const fetchSites = async () => {
try {
const { data, success } = await sitecontrollerAll();
if (success) {
setSites(data || []);
}
} catch (error) {
console.error('获取站点数据失败:', error);
}
};
// 根据站点ID获取站点名称
const getSiteName = (siteId: number | undefined | null) => {
if (!siteId) return '-';
const site = sites.find(s => s.id === siteId);
console.log(`site`,site)
return site ? site.name : String(siteId);
};
// 组件加载时获取站点数据
useEffect(() => {
fetchSites();
}, []);
const columns: ProColumns<API.UnifiedCustomerDTO>[] = [
{
title: 'ID',
dataIndex: 'id',
hideInSearch: true,
},
{
title: '站点',
dataIndex: 'site_id',
valueType: 'select',
request: async () => {
try {
const { data, success } = await sitecontrollerAll();
if (success && data) {
return data.map((site: any) => ({
label: site.name,
value: site.id,
}));
}
return [];
} catch (error) {
console.error('获取站点列表失败:', error);
return [];
}
},
render: (siteId, record) => {
return siteId
return getSiteName(record.site_id) || '-';
},
},
{
title: '头像',
dataIndex: 'avatar',
hideInSearch: true,
width: 60,
render: (_, record) => (
<Avatar
src={record.avatar}
size="small"
style={{ backgroundColor: '#1890ff' }}
>
{!record.avatar && record.fullname?.charAt(0)?.toUpperCase()}
</Avatar>
),
},
{
title: '姓名',
dataIndex: 'fullname',
sorter: true,
render: (_, record) => {
return (
record.fullName ||
`${record.firstName || ''} ${record.lastName || ''}`.trim() ||
record.username ||
'-'
);
},
},
{
title: '用户名',
dataIndex: 'username',
hideInSearch: true,
render: (_, record) => {
if (record.billing.first_name || record.billing.last_name)
return record.billing.first_name + ' ' + record.billing.last_name;
return record.shipping.first_name + ' ' + record.shipping.last_name;
},
},
{
title: '邮箱',
dataIndex: 'email',
copyable: true,
},
{
title: '客户编号',
dataIndex: 'customerId',
render: (_, record) => {
if (!record.customerId) return '-';
return String(record.customerId).padStart(6, 0);
},
sorter: true,
},
{
title: '首单时间',
dataIndex: 'first_purchase_date',
valueType: 'dateMonth',
sorter: true,
render: (_, record) =>
record.first_purchase_date
? dayjs(record.first_purchase_date).format('YYYY-MM-DD HH:mm:ss')
: '-',
// search: {
// transform: (value: string) => {
// return { month: value };
// },
// },
},
{
title: '尾单时间',
title: '电话',
dataIndex: 'phone',
hideInSearch: true,
dataIndex: 'last_purchase_date',
valueType: 'dateTime',
sorter: true,
copyable: true,
},
{
title: '订单数',
dataIndex: 'orders',
title: '账单地址',
dataIndex: 'billing',
hideInSearch: true,
sorter: true,
},
{
title: '金额',
dataIndex: 'total',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE订单数',
dataIndex: 'yoone_orders',
hideInSearch: true,
sorter: true,
},
{
title: 'YOONE金额',
dataIndex: 'yoone_total',
hideInSearch: true,
sorter: true,
},
{
title: '等级',
hideInSearch: true,
render: (_, record) => {
if (!record.yoone_orders || !record.yoone_total) return '-';
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
return 'B';
return '-';
},
},
{
title: '评星',
dataIndex: 'rate',
width: 200,
render: (billing) => <AddressCell address={billing} title="账单地址" />,
},
{
title: '物流地址',
dataIndex: 'shipping',
hideInSearch: true,
width: 200,
render: (shipping) => <AddressCell address={shipping} title="物流地址" />,
},
{
title: '评分',
dataIndex: 'rate',
width: 120,
render: (_, record) => {
return (
<Rate
@ -112,46 +235,31 @@ const ListPage: React.FC = () => {
try {
const { success, message: msg } =
await customercontrollerSetrate({
id: record.customerId,
id: record.id,
rate: val,
});
if (success) {
message.success(msg);
actionRef.current?.reload();
}
} catch (e) {
message.error(e.message);
} catch (e: any) {
message.error(e?.message || '设置评分失败');
}
}}
value={record.rate}
allowHalf
/>
);
},
},
{
title: 'phone',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
title: 'state',
dataIndex: 'state',
render: (_, record) => record?.billing.state || record?.shipping.state,
},
{
title: 'city',
dataIndex: 'city',
hideInSearch: true,
render: (_, record) => record?.billing.city || record?.shipping.city,
},
{
title: '标签',
dataIndex: 'tags',
hideInSearch: true,
render: (_, record) => {
return (
<Space>
{(record.tags || []).map((tag) => {
<Space size={[0, 8]} wrap>
{(record.tags || []).map((tag: string) => {
return (
<Tag
key={tag}
@ -162,8 +270,14 @@ const ListPage: React.FC = () => {
email: record.email,
tag,
});
if (!success) {
message.error(msg);
return false;
}
actionRef.current?.reload();
return true;
}}
style={{ marginBottom: 4 }}
>
{tag}
</Tag>
@ -173,31 +287,55 @@ const ListPage: React.FC = () => {
);
},
},
{
title: '创建时间',
dataIndex: 'site_created_at',
valueType: 'dateTime',
hideInSearch: true,
sorter: true,
width: 140,
},
{
title: '更新时间',
dataIndex: 'site_created_at',
valueType: 'dateTime',
hideInSearch: true,
sorter: true,
width: 140,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
width: 120,
render: (_, record) => {
return (
<Space>
<Space direction="vertical" size="small">
<AddTag
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<HistoryOrder
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<Button
type="link"
size="small"
onClick={() => {
// 这里可以添加查看客户详情的逻辑
message.info('客户详情功能开发中...');
}}
>
</Button>
</Space>
);
},
},
];
return (
<PageContainer ghost>
<PageContainer header={{ title: '客户列表' }}>
<ProTable
scroll={{ x: 'max-content' }}
headerTitle="查询表格"
@ -207,7 +345,9 @@ const ListPage: React.FC = () => {
const key = Object.keys(sorter)[0];
const { data, success } = await customercontrollerGetcustomerlist({
...params,
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
current: params.current?.toString(),
pageSize: params.pageSize?.toString(),
...(key ? { sorterKey: key, sorterValue: sorter[key] as string } : {}),
});
return {
@ -217,12 +357,37 @@ const ListPage: React.FC = () => {
};
}}
columns={columns}
search={{
labelWidth: 'auto',
span: 6,
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
}}
toolBarRender={() => [
<Button
key="sync"
type="primary"
onClick={() => setSyncModalVisible(true)}
>
</Button>,
// 这里可以添加导出、导入等功能按钮
]}
/>
<SyncCustomersModal
visible={syncModalVisible}
onClose={() => setSyncModalVisible(false)}
tableRef={actionRef}
/>
</PageContainer>
);
};
export const AddTag: React.FC<{
const AddTag: React.FC<{
email: string;
tags?: string[];
tableRef: React.MutableRefObject<ActionType | undefined>;
@ -233,7 +398,11 @@ export const AddTag: React.FC<{
return (
<ModalForm
title={`修改标签 - ${email}`}
trigger={<Button></Button>}
trigger={
<Button type="link" size="small">
</Button>
}
width={800}
modalProps={{
destroyOnHidden: true,
@ -250,16 +419,16 @@ export const AddTag: React.FC<{
if (!success) return [];
setTagList(tags || []);
return data
.filter((tag) => {
.filter((tag: string) => {
return !(tags || []).includes(tag);
})
.map((tag) => ({ label: tag, value: tag }));
.map((tag: string) => ({ label: tag, value: tag }));
}}
fieldProps={{
value: tagList, // 当前值
onChange: async (newValue) => {
const added = newValue.filter((x) => !tagList.includes(x));
const removed = tagList.filter((x) => !newValue.includes(x));
const added = newValue.filter((x) => !(tags || []).includes(x));
const removed = (tags || []).filter((x) => !newValue.includes(x));
if (added.length) {
const { success, message: msg } = await customercontrollerAddtag({
@ -282,7 +451,6 @@ export const AddTag: React.FC<{
}
}
tableRef?.current?.reload();
setTagList(newValue);
},
}}
@ -291,4 +459,124 @@ export const AddTag: React.FC<{
);
};
export default ListPage;
const SyncCustomersModal: React.FC<{
visible: boolean;
onClose: () => void;
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ visible, onClose, tableRef }) => {
const { message } = App.useApp();
const [sites, setSites] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 获取站点列表
useEffect(() => {
if (visible) {
setLoading(true);
sitecontrollerAll()
.then((res: any) => {
setSites(res?.data || []);
})
.catch((error: any) => {
message.error('获取站点列表失败: ' + (error.message || '未知错误'));
})
.finally(() => {
setLoading(false);
});
}
}, [visible]);
const handleSync = async (values: { siteId: number }) => {
try {
setLoading(true);
const { success, message: msg, data } = await customercontrollerSynccustomers({
siteId: values.siteId,
});
if (success) {
// 显示详细的同步结果
const result = data || {};
const {
total = 0,
synced = 0,
created = 0,
updated = 0,
errors = []
} = result;
let resultMessage = `同步完成!共处理 ${total} 个客户:`;
if (created > 0) resultMessage += ` 新建 ${created}`;
if (updated > 0) resultMessage += ` 更新 ${updated}`;
if (synced > 0) resultMessage += ` 同步成功 ${synced}`;
if (errors.length > 0) resultMessage += ` 失败 ${errors.length}`;
if (errors.length > 0) {
// 如果有错误,显示警告消息
message.warning({
content: (
<div>
<div>{resultMessage}</div>
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
{errors.slice(0, 3).map((err: any) => err.email || err.error).join(', ')}
{errors.length > 3 && `${errors.length - 3} 个错误...`}
</div>
</div>
),
duration: 8,
key: 'sync-result'
});
} else {
// 完全成功
message.success({
content: resultMessage,
duration: 4,
key: 'sync-result'
});
}
onClose();
// 刷新表格数据
tableRef.current?.reload();
return true;
} else {
message.error(msg || '同步失败');
return false;
}
} catch (error: any) {
message.error('同步失败: ' + (error.message || '未知错误'));
return false;
} finally {
setLoading(false);
}
};
return (
<ModalForm
title="同步客户数据"
open={visible}
onOpenChange={(open) => !open && onClose()}
modalProps={{
destroyOnClose: true,
confirmLoading: loading,
}}
onFinish={handleSync}
>
<ProFormSelect
name="siteId"
label="选择站点"
placeholder="请选择要同步的站点"
options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
rules={[{ required: true, message: '请选择站点' }]}
fieldProps={{
loading: loading,
}}
/>
</ModalForm>
);
};
export { AddTag };
export default CustomerList;

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 {
ActionType,
@ -16,7 +17,6 @@ import {
message,
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import * as dictApi from '@/servers/api/dict';
const { Sider, Content } = Layout;
@ -37,8 +37,10 @@ const DictPage: React.FC = () => {
const [editDictData, setEditDictData] = useState<any>(null);
// 右侧字典项列表的状态
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] = useState(false);
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] = useState(false);
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] =
useState(false);
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] =
useState(false);
const [editDictItemData, setEditDictItemData] = useState<any>(null);
const [dictItemForm] = Form.useForm();
const actionRef = useRef<ActionType>();
@ -95,15 +97,15 @@ const DictPage: React.FC = () => {
const handleDeleteDict = async (id: number) => {
try {
const result = await dictApi.dictcontrollerDeletedict({ id });
if(!result.success){
throw new Error(result.message || '删除失败')
if (!result.success) {
throw new Error(result.message || '删除失败');
}
message.success('删除成功');
fetchDicts();
if (selectedDict?.id === id) {
setSelectedDict(null);
}
} catch (error:any) {
} catch (error: any) {
message.error(`删除失败,原因为:${error.message}`);
}
};
@ -157,8 +159,8 @@ const DictPage: React.FC = () => {
const handleDeleteDictItem = async (id: number) => {
try {
const result = await dictApi.dictcontrollerDeletedictitem({ id });
if(!result.success){
throw new Error(result.message || '删除失败')
if (!result.success) {
throw new Error(result.message || '删除失败');
}
message.success('删除成功');
@ -166,7 +168,7 @@ const DictPage: React.FC = () => {
setTimeout(() => {
actionRef.current?.reload();
}, 100);
} catch (error:any) {
} catch (error: any) {
message.error(`删除失败,原因为:${error.message}`);
}
};
@ -176,7 +178,7 @@ const DictPage: React.FC = () => {
try {
const result = await dictApi.dictcontrollerCreatedictitem({
...values,
dictId: selectedDict.id
dictId: selectedDict.id,
});
if (!result.success) {
@ -199,7 +201,10 @@ const DictPage: React.FC = () => {
const handleEditDictItemFormSubmit = async (values: any) => {
if (!editDictItemData) return;
try {
const result = await dictApi.dictcontrollerUpdatedictitem({ id: editDictItemData.id }, values);
const result = await dictApi.dictcontrollerUpdatedictitem(
{ id: editDictItemData.id },
values,
);
if (!result.success) {
throw new Error(result.message || '更新失败');
@ -227,7 +232,9 @@ const DictPage: React.FC = () => {
try {
// 获取当前字典的所有数据
const response = await dictApi.dictcontrollerGetdictitems({ dictId: selectedDict.id });
const response = await dictApi.dictcontrollerGetdictitems({
dictId: selectedDict.id,
});
if (!response || response.length === 0) {
message.warning('当前字典没有数据可导出');
@ -503,9 +510,10 @@ const DictPage: React.FC = () => {
customRequest={async (options) => {
const { file, onSuccess, onError } = options;
try {
const result = await dictApi.dictcontrollerImportdictitems(
const result =
await dictApi.dictcontrollerImportdictitems(
{ dictId: selectedDict?.id },
[file as File]
[file as File],
);
onSuccess?.(result);
} catch (error) {
@ -515,7 +523,7 @@ const DictPage: React.FC = () => {
showUploadList={false}
disabled={!selectedDict}
onChange={(info) => {
console.log(`info`,info)
console.log(`info`, info);
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
// 重新加载字典项列表
@ -677,14 +685,18 @@ const DictPage: React.FC = () => {
<Input
placeholder="字典名称 (e.g., brand)"
value={editDictData?.name || ''}
onChange={(e) => setEditDictData({ ...editDictData, name: e.target.value })}
onChange={(e) =>
setEditDictData({ ...editDictData, name: e.target.value })
}
/>
</Form.Item>
<Form.Item label="字典标题">
<Input
placeholder="字典标题 (e.g., 品牌)"
value={editDictData?.title || ''}
onChange={(e) => setEditDictData({ ...editDictData, title: e.target.value })}
onChange={(e) =>
setEditDictData({ ...editDictData, title: e.target.value })
}
/>
</Form.Item>
</Form>

View File

@ -453,7 +453,6 @@ const ListPage: React.FC = () => {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
rowClassName={(record) => {
return record.id === activeLine
? styles['selected-line-order-protable']
@ -493,7 +492,6 @@ const ListPage: React.FC = () => {
title="批量导出"
description="确认导出选中的订单吗?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await ordercontrollerExportorder({
@ -508,15 +506,16 @@ const ListPage: React.FC = () => {
} catch (error: any) {
message.error(error?.message || '导出失败');
}
}}
>
<Button type="primary" disabled={selectedRowKeys.length === 0} ghost>
<Button
type="primary"
disabled={selectedRowKeys.length === 0}
ghost
>
</Button>
</Popconfirm>
</Popconfirm>,
]}
request={async ({ date, ...param }: any) => {
if (param.status === 'all') {

View File

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

View File

@ -41,11 +41,14 @@ const AttributePage: React.FC = () => {
setLoadingDicts(true);
try {
const res = await request('/dict/list', { params: { title } });
// 条件判断,过滤只保留 allowedDictNames 中的字典
const filtered = (res || []).filter((d: any) => !notAttributes.has(d?.name));
// 条件判断,确保res是数组再进行过滤
const dataList = Array.isArray(res) ? res : res?.data || [];
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
setDicts(filtered);
} catch (error) {
console.error('获取字典列表失败:', error);
message.error('获取字典列表失败');
setDicts([]);
}
setLoadingDicts(false);
};
@ -114,19 +117,26 @@ const AttributePage: React.FC = () => {
return;
}
if (selectedDict?.id) {
try {
const list = await request('/dict/items', {
params: {
dictId: selectedDict.id,
},
});
const exists =
Array.isArray(list) && list.some((it: any) => it.id === itemId);
// 确保list是数组再进行some操作
const dataList = Array.isArray(list) ? list : list?.data || [];
const exists = dataList.some((it: any) => it.id === itemId);
if (exists) {
message.error('删除失败');
} else {
message.success('删除成功');
actionRef.current?.reload();
}
} catch (error) {
console.error('验证删除结果失败:', error);
message.success('删除成功');
actionRef.current?.reload();
}
} else {
message.success('删除成功');
actionRef.current?.reload();
@ -245,6 +255,7 @@ const AttributePage: React.FC = () => {
};
}
const { name, title } = params;
try {
const res = await request('/dict/items', {
params: {
dictId: selectedDict.id,
@ -252,17 +263,36 @@ const AttributePage: React.FC = () => {
title,
},
});
// 确保返回的是数组
const data = Array.isArray(res) ? res : res?.data || [];
return {
data: res,
data: data,
success: true,
};
} catch (error) {
console.error('获取字典项失败:', error);
return {
data: [],
success: false,
};
}
}}
rowKey="id"
search={{
layout: 'vertical',
}}
pagination={false}
options={false}
options={{
reload: true,
density: false,
setting: {
draggable: true,
checkable: true,
checkedReset: false,
},
search: false,
fullScreen: false,
}}
size="small"
key={selectedDict?.id}
headerTitle={

View File

@ -244,7 +244,10 @@ const CategoryPage: React.FC = () => {
</Popconfirm>,
]}
>
<List.Item.Meta title={`${item.title}(${item.titleCN??'-'})`} description={item.name} />
<List.Item.Meta
title={`${item.title}(${item.titleCN ?? '-'})`}
description={item.name}
/>
</List.Item>
)}
/>

View File

@ -112,6 +112,11 @@ const PermutationPage: React.FC = () => {
// 2. Fetch Attribute Values (Dict Items)
const valuesMap: Record<string, any[]> = {};
for (const attr of attrs) {
// 使用属性中直接包含的items而不是额外请求
if (attr.items && Array.isArray(attr.items)) {
valuesMap[attr.name] = attr.items;
} else {
// 如果没有items尝试通过dictId获取
const dictId = attr.dict?.id || attr.dictId;
if (dictId) {
const itemsRes = await request('/dict/items', {
@ -120,6 +125,7 @@ const PermutationPage: React.FC = () => {
valuesMap[attr.name] = itemsRes || [];
}
}
}
setAttributeValues(valuesMap);
// 3. Fetch Existing Products

View File

@ -9,7 +9,16 @@ import {
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Card, Spin, Tag, message, Select, Progress, Modal } from 'antd';
import {
Button,
Card,
message,
Modal,
Progress,
Select,
Spin,
Tag,
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm';
@ -89,7 +98,13 @@ const ProductSyncPage: React.FC = () => {
const [batchSyncModalVisible, setBatchSyncModalVisible] = useState(false);
const [syncProgress, setSyncProgress] = useState(0);
const [syncing, setSyncing] = useState(false);
const [syncResults, setSyncResults] = useState<{ success: number; failed: number; errors: string[] }>({ success: 0, failed: 0, errors: [] });
const [syncResults, setSyncResults] = useState<{
success: number;
failed: number;
errors: string[];
}>({ success: 0, failed: 0, errors: [] });
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedRows, setSelectedRows] = useState<ProductWithWP[]>([]);
// 初始化数据:获取站点和所有 WP 产品
useEffect(() => {
@ -197,24 +212,24 @@ const ProductSyncPage: React.FC = () => {
};
// 批量同步产品到指定站点
const batchSyncProducts = async () => {
const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => {
if (!selectedSiteId) {
message.error('请选择要同步到的站点');
return;
}
const targetSite = sites.find(site => site.id === selectedSiteId);
const targetSite = sites.find((site) => site.id === selectedSiteId);
if (!targetSite) {
message.error('选择的站点不存在');
return;
}
setSyncing(true);
setSyncProgress(0);
setSyncResults({ success: 0, failed: 0, errors: [] });
// 如果没有传入产品列表,则使用选中的产品
let products = productsToSync || selectedRows;
// 如果既没有传入产品也没有选中产品,则同步所有产品
if (!products || products.length === 0) {
try {
// 获取所有产品
const { data, success } = await productcontrollerGetproductlist({
current: 1,
pageSize: 10000, // 获取所有产品
@ -224,14 +239,24 @@ const ProductSyncPage: React.FC = () => {
message.error('获取产品列表失败');
return;
}
products = data.items as ProductWithWP[];
} catch (error) {
message.error('获取产品列表失败');
return;
}
}
setSyncing(true);
setSyncProgress(0);
setSyncResults({ success: 0, failed: 0, errors: [] });
const products = data.items as ProductWithWP[];
const totalProducts = products.length;
let processed = 0;
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
try {
// 逐个同步产品
for (const product of products) {
try {
@ -239,7 +264,10 @@ const ProductSyncPage: React.FC = () => {
let siteProductSku = '';
if (product.siteSkus && product.siteSkus.length > 0) {
const siteSkuInfo = product.siteSkus.find((sku: any) => {
return sku.siteSku && sku.siteSku.includes(targetSite.skuPrefix || targetSite.name);
return (
sku.siteSku &&
sku.siteSku.includes(targetSite.skuPrefix || targetSite.name)
);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
@ -247,11 +275,11 @@ const ProductSyncPage: React.FC = () => {
}
// 如果没有找到实际的siteSku则根据模板生成
const expectedSku = siteProductSku || (
skuTemplate
const expectedSku =
siteProductSku ||
(skuTemplate
? renderSiteSku(skuTemplate, { site: targetSite, product })
: `${targetSite.skuPrefix || ''}-${product.sku}`
);
: `${targetSite.skuPrefix || ''}-${product.sku}`);
// 检查是否已存在
const existingProduct = wpProductMap.get(expectedSku);
@ -272,10 +300,13 @@ const ProductSyncPage: React.FC = () => {
let res;
if (existingProduct?.externalProductId) {
// 更新现有产品
res = await request(`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`, {
res = await request(
`/site-api/${targetSite.id}/products/${existingProduct.externalProductId}`,
{
method: 'PUT',
data: syncData,
});
},
);
} else {
// 创建新产品
res = await request(`/site-api/${targetSite.id}/products`, {
@ -300,7 +331,6 @@ const ProductSyncPage: React.FC = () => {
failedCount++;
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
}
} catch (error: any) {
failedCount++;
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
@ -315,12 +345,13 @@ const ProductSyncPage: React.FC = () => {
if (failedCount === 0) {
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
} else {
message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount}`);
message.warning(
`批量同步完成,成功 ${successCount} 个,失败 ${failedCount}`,
);
}
// 刷新表格
actionRef.current?.reload();
} catch (error: any) {
message.error('批量同步失败: ' + (error.message || error.toString()));
} finally {
@ -453,7 +484,9 @@ const ProductSyncPage: React.FC = () => {
const siteSkuInfo = record.siteSkus.find((sku: any) => {
// 这里假设可以根据站点名称或其他标识来匹配
// 如果需要更精确的匹配逻辑,可以根据实际需求调整
return sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name);
return (
sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name)
);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
@ -461,18 +494,21 @@ const ProductSyncPage: React.FC = () => {
}
// 如果没有找到实际的siteSku则根据模板或默认规则生成期望的SKU
const expectedSku = siteProductSku || (
skuTemplate
const expectedSku =
siteProductSku ||
(skuTemplate
? renderSiteSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`
);
: `${site.skuPrefix || ''}-${record.sku}`);
// 尝试用确定的SKU获取WP产品
let wpProduct = wpProductMap.get(expectedSku);
// 如果根据实际SKU没找到再尝试用模板生成的SKU查找
if (!wpProduct && siteProductSku && skuTemplate) {
const templateSku = renderSiteSku(skuTemplate, { site, product: record });
const templateSku = renderSiteSku(skuTemplate, {
site,
product: record,
});
wpProduct = wpProductMap.get(templateSku);
}
@ -490,11 +526,11 @@ const ProductSyncPage: React.FC = () => {
return await syncProductToSite(values, record, site);
}}
initialValues={{
sku: siteProductSku || (
skuTemplate
sku:
siteProductSku ||
(skuTemplate
? renderSiteSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`
),
: `${site.skuPrefix || ''}-${record.sku}`),
}}
>
<ProFormText
@ -587,36 +623,46 @@ const ProductSyncPage: React.FC = () => {
}
return (
<Card
title="商品同步状态"
className="product-sync-card"
extra={
<div style={{ display: 'flex', gap: 8 }}>
<Select
style={{ width: 200 }}
placeholder="选择目标站点"
value={selectedSiteId}
onChange={setSelectedSiteId}
options={sites.map(site => ({
label: site.name,
value: site.id,
}))}
/>
<Button
type="primary"
icon={<SyncOutlined />}
onClick={() => setBatchSyncModalVisible(true)}
disabled={!selectedSiteId || sites.length === 0}
>
</Button>
</div>
}
>
<Card title="商品同步状态" className="product-sync-card">
<ProTable<ProductWithWP>
columns={generateColumns()}
actionRef={actionRef}
rowKey="id"
rowSelection={{
selectedRowKeys,
onChange: (keys, rows) => {
setSelectedRowKeys(keys);
setSelectedRows(rows);
},
}}
toolBarRender={() => [
<Select
key="site-select"
style={{ width: 200 }}
placeholder="选择目标站点"
value={selectedSiteId}
onChange={setSelectedSiteId}
options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
/>,
<Button
key="batch-sync"
type="primary"
icon={<SyncOutlined />}
onClick={() => {
if (!selectedSiteId) {
message.warning('请先选择目标站点');
return;
}
setBatchSyncModalVisible(true);
}}
disabled={!selectedSiteId || sites.length === 0}
>
</Button>,
]}
request={async (params, sort, filter) => {
// 调用本地获取产品列表 API
const { data, success } = await productcontrollerGetproductlist({
@ -661,8 +707,17 @@ const ProductSyncPage: React.FC = () => {
maskClosable={!syncing}
>
<div style={{ marginBottom: 16 }}>
<p><strong>{sites.find(s => s.id === selectedSiteId)?.name}</strong></p>
<p>
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
</p>
{selectedRows.length > 0 ? (
<p>
<strong>{selectedRows.length}</strong>
</p>
) : (
<p></p>
)}
</div>
{syncing && (
@ -679,12 +734,17 @@ const ProductSyncPage: React.FC = () => {
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
<div style={{ marginBottom: 8, color: '#ff4d4f' }}></div>
{syncResults.errors.slice(0, 10).map((error, index) => (
<div key={index} style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
<div
key={index}
style={{ fontSize: 12, color: '#666', marginBottom: 4 }}
>
{error}
</div>
))}
{syncResults.errors.length > 10 && (
<div style={{ fontSize: 12, color: '#999' }}>... {syncResults.errors.length - 10} </div>
<div style={{ fontSize: 12, color: '#999' }}>
... {syncResults.errors.length - 10}
</div>
)}
</div>
)}
@ -699,7 +759,7 @@ const ProductSyncPage: React.FC = () => {
</Button>
<Button
type="primary"
onClick={batchSyncProducts}
onClick={() => batchSyncProducts()}
loading={syncing}
disabled={syncing}
>

View File

@ -2,11 +2,11 @@ import { sitecontrollerAll } from '@/servers/api/site';
import { EditOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { Outlet, history, request, useLocation, useParams } from '@umijs/max';
import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd';
import { Button, Col, Menu, Row, Select, Spin, message } from 'antd';
import Sider from 'antd/es/layout/Sider';
import React, { useEffect, useState } from 'react';
import type { SiteItem } from '../List/index';
import EditSiteForm from './EditSiteForm';
import Sider from 'antd/es/layout/Sider';
const ShopLayout: React.FC = () => {
const [sites, setSites] = useState<any[]>([]);
@ -91,10 +91,7 @@ const ShopLayout: React.FC = () => {
};
return (
<PageContainer
header={{ title: null, breadcrumb: undefined }}
>
<PageContainer header={{ title: null, breadcrumb: undefined }}>
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
<Col span={4} style={{ height: '100%' }}>
<Sider

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 { PageHeader } from '@ant-design/pro-layout';
import { request, useParams } from '@umijs/max';
import { App, Button, Card, List } from 'antd';
import React, { useEffect, useState } from 'react';
// 定义链接项的类型
interface LinkItem {
@ -48,10 +47,7 @@ const LinksPage: React.FC = () => {
return (
<div>
<PageHeader
title="站点链接"
breadcrumb={{ items: [] }}
/>
<PageHeader title="站点链接" breadcrumb={{ items: [] }} />
<Card
title="常用链接"
bordered={false}
@ -76,7 +72,7 @@ const LinksPage: React.FC = () => {
target="_blank"
>
访
</Button>
</Button>,
]}
>
<List.Item.Meta

View File

@ -208,6 +208,7 @@ const OrdersPage: React.FC = () => {
>
</Button>
{record.status === 'completed' && (
<Popconfirm
title="确定取消发货?"
description="取消发货后订单状态将恢复为处理中"
@ -232,6 +233,7 @@ const OrdersPage: React.FC = () => {
</Button>
</Popconfirm>
)}
<Popconfirm
title="确定删除订单?"
onConfirm={async () => {

View File

@ -1,16 +1,9 @@
import React, { useEffect } from 'react';
import {
Modal,
Form,
Input,
InputNumber,
Select,
message,
} from 'antd';
import {
siteapicontrollerCreatereview,
siteapicontrollerUpdatereview,
} from '@/servers/api/siteApi';
import { Form, Input, InputNumber, Modal, Select, message } from 'antd';
import React, { useEffect } from 'react';
const { TextArea } = Input;
const { Option } = Select;
@ -64,7 +57,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
review: values.content,
rating: values.rating,
status: values.status,
}
},
);
} else {
// 创建新评论
@ -78,7 +71,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
rating: values.rating,
author: values.author,
author_email: values.email,
}
},
);
}
@ -136,7 +129,7 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' }
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input placeholder="请输入邮箱" />

View File

@ -101,14 +101,22 @@ const ReviewsPage: React.FC = () => {
};
}
// 确保 response.data.items 是数组
const items = Array.isArray(response.data.items) ? response.data.items : [];
const items = Array.isArray(response.data.items)
? response.data.items
: [];
// 确保每个 item 有有效的 id
const processedItems = items.map((item, index) => ({
...item,
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
id: typeof item.id === 'object' ? JSON.stringify(item.id) : (item.id || index),
id:
typeof item.id === 'object'
? JSON.stringify(item.id)
: item.id || index,
// 如果 product_id 是对象,转换为字符串
product_id: typeof item.product_id === 'object' ? JSON.stringify(item.product_id) : item.product_id,
product_id:
typeof item.product_id === 'object'
? JSON.stringify(item.product_id)
: item.product_id,
}));
return {
data: processedItems,

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 {
ActionType,
ProCard,
@ -6,7 +11,16 @@ import {
ProTable,
} from '@ant-design/pro-components';
import { useParams } from '@umijs/max';
import { Button, message, Popconfirm, Space, Modal, Form, Input, Select } from 'antd';
import {
Button,
Form,
Input,
message,
Modal,
Popconfirm,
Select,
Space,
} from 'antd';
import React, { useRef, useState } from 'react';
const WebhooksPage: React.FC = () => {
@ -17,7 +31,8 @@ const WebhooksPage: React.FC = () => {
// 模态框状态
const [isModalVisible, setIsModalVisible] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [currentWebhook, setCurrentWebhook] = useState<API.UnifiedWebhookDTO | null>(null);
const [currentWebhook, setCurrentWebhook] =
useState<API.UnifiedWebhookDTO | null>(null);
// 表单实例
const [form] = Form.useForm();
@ -114,10 +129,25 @@ const WebhooksPage: React.FC = () => {
{ title: 'ID', dataIndex: 'id', key: 'id', width: 50 },
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '主题', dataIndex: 'topic', key: 'topic' },
{ title: '回调URL', dataIndex: 'delivery_url', key: 'delivery_url', ellipsis: true },
{
title: '回调URL',
dataIndex: 'delivery_url',
key: 'delivery_url',
ellipsis: true,
},
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
{ title: '创建时间', dataIndex: 'date_created', key: 'date_created', valueType: 'dateTime' },
{ title: '更新时间', dataIndex: 'date_modified', key: 'date_modified', valueType: 'dateTime' },
{
title: '创建时间',
dataIndex: 'date_created',
key: 'date_created',
valueType: 'dateTime',
},
{
title: '更新时间',
dataIndex: 'date_modified',
key: 'date_modified',
valueType: 'dateTime',
},
{
title: '操作',
key: 'action',
@ -184,12 +214,17 @@ const WebhooksPage: React.FC = () => {
};
}
// 确保 response.data.items 是数组
const items = Array.isArray(response.data.items) ? response.data.items : [];
const items = Array.isArray(response.data.items)
? response.data.items
: [];
// 确保每个 item 有有效的 id
const processedItems = items.map((item, index) => ({
...item,
// 如果 id 是对象,转换为字符串,否则使用索引作为后备
id: typeof item.id === 'object' ? JSON.stringify(item.id) : (item.id || index),
id:
typeof item.id === 'object'
? JSON.stringify(item.id)
: item.id || index,
}));
return {
data: processedItems,
@ -211,10 +246,7 @@ const WebhooksPage: React.FC = () => {
}}
headerTitle="Webhooks列表"
toolBarRender={() => [
<Button
type="primary"
onClick={showCreateModal}
>
<Button type="primary" onClick={showCreateModal}>
Webhook
</Button>,
]}
@ -245,7 +277,10 @@ const WebhooksPage: React.FC = () => {
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入webhook名称' }, { max: 100, message: '名称不能超过100个字符' }]}
rules={[
{ required: true, message: '请输入webhook名称' },
{ max: 100, message: '名称不能超过100个字符' },
]}
>
<Input placeholder="请输入webhook名称" />
</Form.Item>
@ -265,7 +300,10 @@ const WebhooksPage: React.FC = () => {
<Form.Item
name="delivery_url"
label="回调URL"
rules={[{ required: true, message: '请输入回调URL' }, { type: 'url', message: '请输入有效的URL' }]}
rules={[
{ required: true, message: '请输入回调URL' },
{ type: 'url', message: '请输入有效的URL' },
]}
>
<Input placeholder="请输入回调URLhttps://example.com/webhook" />
</Form.Item>
@ -283,10 +321,7 @@ const WebhooksPage: React.FC = () => {
label="状态"
rules={[{ required: true, message: '请选择webhook状态' }]}
>
<Select
placeholder="请选择webhook状态"
options={webhookStatuses}
/>
<Select placeholder="请选择webhook状态" options={webhookStatuses} />
</Form.Item>
</Form>
</Modal>

View File

@ -2,7 +2,6 @@ import {
templatecontrollerCreatetemplate,
templatecontrollerDeletetemplate,
templatecontrollerGettemplatelist,
templatecontrollerRendertemplate,
templatecontrollerRendertemplatedirect,
templatecontrollerUpdatetemplate,
} from '@/servers/api/template';
@ -75,12 +74,10 @@ const useTemplatePreview = () => {
renderedResult,
handlePreview,
refreshPreview,
setPreviewData
setPreviewData,
};
};
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
@ -176,7 +173,8 @@ const CreateForm: React.FC<{
}> = ({ tableRef }) => {
const { message } = App.useApp();
const [form] = ProForm.useForm();
const { renderedResult, handlePreview, refreshPreview } = useTemplatePreview();
const { renderedResult, handlePreview, refreshPreview } =
useTemplatePreview();
return (
<DrawerForm<API.CreateTemplateDTO>
@ -263,8 +261,17 @@ const CreateForm: React.FC<{
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<Typography.Title level={5} style={{ margin: 0 }}></Typography.Title>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '8px',
}}
>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Button
type="text"
icon={<ReloadOutlined />}
@ -283,7 +290,7 @@ const CreateForm: React.FC<{
height: '600px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
}
},
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
@ -302,7 +309,8 @@ const UpdateForm: React.FC<{
}> = ({ tableRef, values: initialValues }) => {
const { message } = App.useApp();
const [form] = ProForm.useForm();
const { renderedResult, handlePreview, refreshPreview, setPreviewData } = useTemplatePreview();
const { renderedResult, handlePreview, refreshPreview, setPreviewData } =
useTemplatePreview();
// 组件挂载时初始化预览数据
useEffect(() => {
@ -310,7 +318,7 @@ const UpdateForm: React.FC<{
setPreviewData({
name: initialValues.name,
value: initialValues.value,
testData: initialValues.testData
testData: initialValues.testData,
});
}
}, [initialValues, setPreviewData]);
@ -405,8 +413,17 @@ const UpdateForm: React.FC<{
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<Typography.Title level={5} style={{ margin: 0 }}></Typography.Title>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '8px',
}}
>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Button
type="text"
icon={<ReloadOutlined />}
@ -425,7 +442,7 @@ const UpdateForm: React.FC<{
height: '600px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
}
},
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>

View File

@ -221,6 +221,8 @@ const WpToolPage: React.FC = () => {
const [csvData, setCsvData] = useState<any[]>([]); // 解析后的 CSV 数据
const [processedData, setProcessedData] = useState<any[]>([]); // 处理后待下载的数据
const [isProcessing, setIsProcessing] = useState(false); // 是否正在处理中
const [isConfigLoading, setIsConfigLoading] = useState(false); // 是否正在加载配置
const [configLoadAttempts, setConfigLoadAttempts] = useState(0); // 配置加载重试次数
const [config, setConfig] = useState<TagConfig>({
// 动态配置
brands: [],
@ -237,22 +239,34 @@ const WpToolPage: React.FC = () => {
useEffect(() => {
const fetchAllConfigs = async () => {
try {
message.loading({ content: '正在加载字典配置...', key: 'loading-config' });
// 1. 获取所有字典列表以找到对应的 ID
const dictList = await request('/dict/list');
const dictListResponse = await request('/dict/list');
// 处理后端统一响应格式
const dictList = dictListResponse?.data || dictListResponse || [];
// 2. 根据字典名称获取字典项
const getItems = async (dictName: string) => {
try {
const dict = dictList.find((d: any) => d.name === dictName);
if (!dict) {
console.warn(`Dictionary ${dictName} not found`);
return [];
}
const res = await request('/dict/items', {
const response = await request('/dict/items', {
params: { dictId: dict.id },
});
return res.map((item: any) => item.name);
// 处理后端统一响应格式,获取数据数组
const items = response?.data || response || [];
return items.map((item: any) => item.name);
} catch (error) {
console.error(`Failed to fetch items for ${dictName}:`, error);
return [];
}
};
// 3. 并行获取所有字典项
const [
brands,
fruitKeys,
@ -264,9 +278,9 @@ const WpToolPage: React.FC = () => {
categoryKeys,
] = await Promise.all([
getItems('brand'),
getItems('fruit'), // 假设字典名为 fruit
getItems('mint'), // 假设字典名为 mint
getItems('flavor'), // 假设字典名为 flavor
getItems('fruit'),
getItems('mint'),
getItems('flavor'),
getItems('strength'),
getItems('size'),
getItems('humidity'),
@ -283,11 +297,19 @@ const WpToolPage: React.FC = () => {
humidityKeys,
categoryKeys,
};
setConfig(newConfig);
form.setFieldsValue(newConfig);
message.success({ content: '字典配置加载成功', key: 'loading-config' });
// 显示加载结果统计
const totalItems = brands.length + fruitKeys.length + mintKeys.length + flavorKeys.length +
strengthKeys.length + sizeKeys.length + humidityKeys.length + categoryKeys.length;
console.log(`字典配置加载完成: 共 ${totalItems} 个配置项`);
} catch (error) {
console.error('Failed to fetch configs:', error);
message.error('获取字典配置失败');
message.error({ content: '获取字典配置失败,请刷新页面重试', key: 'loading-config' });
}
};

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 */
export async function customercontrollerGettags(options?: {
[key: string]: any;
@ -71,3 +86,18 @@ export async function customercontrollerSetrate(
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/sync */
export async function customercontrollerSynccustomers(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/sync', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
});
}

View File

@ -4,7 +4,7 @@ import { request } from 'umi';
/** 此处后端没有提供注释 GET /site/all */
export async function sitecontrollerAll(options?: { [key: string]: any }) {
return request<API.WpSitesResponse>('/site/all', {
return request<API.SitesResponse>('/site/all', {
method: 'GET',
...(options || {}),
});

View File

@ -285,6 +285,18 @@ declare namespace API {
customerId?: number;
};
type customercontrollerGetcustomerstatisticlistParams = {
current?: string;
pageSize?: string;
email?: string;
tags?: string;
sorterKey?: string;
sorterValue?: string;
state?: string;
first_purchase_date?: string;
customerId?: number;
};
type CustomerTagDTO = {
email?: string;
tag?: string;
@ -1893,6 +1905,17 @@ declare namespace API {
id: string;
};
type SitesResponse = {
/** 状态码 */
code?: number;
/** 是否成功 */
success?: boolean;
/** 消息内容 */
message?: string;
/** 响应数据 */
data?: SiteConfig[];
};
type statisticscontrollerGetinativeusersbymonthParams = {
month?: string;
};
@ -3014,15 +3037,4 @@ declare namespace API {
/** 数据列表 */
items?: WpProductDTO[];
};
type WpSitesResponse = {
/** 状态码 */
code?: number;
/** 是否成功 */
success?: boolean;
/** 消息内容 */
message?: string;
/** 响应数据 */
data?: SiteConfig[];
};
}