Compare commits

..

2 Commits

Author SHA1 Message Date
zhuotianyuan 82364e1650 feat(订单): 添加批量导出功能并优化导出逻辑
feat(模板): 新增直接渲染模板接口
feat(站点API): 添加webhooks相关接口和订单发货功能
feat(统计): 支持按周/月/日分组统计订单数据
refactor(订单导出): 修改导出接口为GET方式并优化实现
style(统计图表): 调整标签显示格式和颜色
2025-12-23 16:57:52 +08:00
tikkhun ce75777195 feat: 完善产品管理站点管理区域管理
refactor: 优化代码结构,移除无用代码和修复格式问题

fix: 修复登录验证、订单同步和库存管理相关问题

docs: 更新README和类型定义文件

chore: 更新依赖包和配置脚本

style: 统一代码风格,修复缩进和标点符号问题
2025-12-19 17:35:18 +08:00
76 changed files with 3071 additions and 8647 deletions

View File

@ -16,7 +16,6 @@ export default defineConfig({
layout: {
title: 'YOONE',
},
esbuildMinifyIIFE: true,
define: {
UMI_APP_API_URL,
},
@ -101,18 +100,6 @@ export default defineConfig({
path: '/site/shop/:siteId/customers',
component: './Site/Shop/Customers',
},
{
path: '/site/shop/:siteId/reviews',
component: './Site/Shop/Reviews',
},
{
path: '/site/shop/:siteId/webhooks',
component: './Site/Shop/Webhooks',
},
{
path: '/site/shop/:siteId/links',
component: './Site/Shop/Links',
},
],
},
{
@ -132,16 +119,6 @@ export default defineConfig({
path: '/customer/list',
component: './Customer/List',
},
{
name: '数据分析列表',
path: '/customer/statistic/list',
component: './Customer/StatisticList',
},
// {
// name: '客户统计',
// path: '/customer/statistic/home',
// component: './Customer/Statistic',
// }
],
},
{
@ -154,6 +131,11 @@ export default defineConfig({
path: '/product/list',
component: './Product/List',
},
{
name: '产品属性排列',
path: '/product/permutation',
component: './Product/Permutation',
},
{
name: '产品分类',
path: '/product/category',
@ -164,27 +146,12 @@ export default defineConfig({
path: '/product/attribute',
component: './Product/Attribute',
},
{
name: '产品属性排列',
path: '/product/permutation',
component: './Product/Permutation',
},
{
name: '产品品牌空间',
path: '/product/groupBy',
component: './Product/GroupBy',
},
// sync
{
name: '同步产品',
path: '/product/sync',
component: './Product/Sync',
},
{
name: '产品CSV 工具',
path: '/product/csvtool',
component: './Product/CsvTool',
},
],
},
{

View File

@ -1,32 +0,0 @@
# 构建阶段
FROM node:18-alpine as builder
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖(使用 --legacy-peer-deps 解决依赖冲突)
RUN npm install --legacy-peer-deps
# 复制源代码
COPY . .
# 构建项目
RUN npm run build
# 生产阶段
FROM nginx:alpine
# 复制构建产物到 Nginx 静态目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露端口
EXPOSE 80
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,33 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# API 代理配置
location /api {
proxy_pass http://api:7001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态文件缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 错误页面配置
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -47,4 +47,4 @@
"prettier-plugin-packagejson": "^2.4.3",
"typescript": "^5.7.3"
}
}
}

View File

@ -1,38 +0,0 @@
import React from 'react';
interface AddressProps {
address: {
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
};
style?: React.CSSProperties;
}
const Address: React.FC<AddressProps> = ({ address, style }) => {
if (!address) {
return <span>-</span>;
}
const { address_1, address_2, city, state, postcode, country, phone } =
address;
return (
<div style={{ fontSize: 12, ...style }}>
<div>
{address_1} {address_2}
</div>
<div>
{city}, {state}, {postcode}
</div>
<div>{country}</div>
<div>{phone}</div>
</div>
);
};
export default Address;

View File

@ -4,11 +4,9 @@ import {
ActionType,
DrawerForm,
ProForm,
ProFormDateRangePicker,
ProFormSelect,
} from '@ant-design/pro-components';
import { Button } from 'antd';
import dayjs from 'dayjs';
import React from 'react';
// 定义SyncForm组件的props类型
@ -16,7 +14,6 @@ interface SyncFormProps {
tableRef: React.MutableRefObject<ActionType | undefined>;
onFinish: (values: any) => Promise<void>;
siteId?: string;
dateRange?: [dayjs.Dayjs, dayjs.Dayjs];
}
/**
@ -24,12 +21,7 @@ interface SyncFormProps {
* @param {SyncFormProps} props
* @returns {React.ReactElement}
*/
const SyncForm: React.FC<SyncFormProps> = ({
tableRef,
onFinish,
siteId,
dateRange,
}) => {
const SyncForm: React.FC<SyncFormProps> = ({ tableRef, onFinish, siteId }) => {
// 使用 antd 的 App 组件提供的 message API
const [loading, setLoading] = React.useState(false);
@ -57,9 +49,6 @@ const SyncForm: React.FC<SyncFormProps> = ({
// 返回一个抽屉表单
return (
<DrawerForm<API.ordercontrollerSyncorderParams>
initialValues={{
dateRange: [dayjs().subtract(1, 'week'), dayjs()],
}}
title="同步订单"
// 表单的触发器,一个带图标的按钮
trigger={
@ -94,21 +83,6 @@ const SyncForm: React.FC<SyncFormProps> = ({
}));
}}
/>
<ProFormDateRangePicker
name="dateRange"
label="同步日期范围"
placeholder={['开始日期', '结束日期']}
transform={(value) => {
return {
dateRange: value,
};
}}
fieldProps={{
showTime: false,
style: { width: '100%' },
}}
/>
</ProForm.Group>
</DrawerForm>
);

View File

@ -1,91 +0,0 @@
import { message } from 'antd';
import React from 'react';
// 定义同步结果的数据类型
export interface SyncResultData {
total?: number;
processed?: number;
synced?: number;
created?: number;
updated?: number;
errors?: Array<{
identifier: string;
error: string;
}>;
}
// 定义组件的 Props 类型
interface SyncResultMessageProps {
data?: SyncResultData;
entityType?: string; // 实体类型,如"订单"、"客户"等
}
// 显示同步结果的函数
export const showSyncResult = (
data: SyncResultData,
entityType: string = '订单',
) => {
const result = data || {};
const {
total = 0,
processed = 0,
synced = 0,
created = 0,
updated = 0,
errors = [],
} = result;
// 构建结果消息
let resultMessage = `同步完成!共处理 ${processed}${entityType}(总数 ${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.identifier}: ${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',
});
}
};
// 同步结果显示组件
const SyncResultMessage: React.FC<SyncResultMessageProps> = ({
data,
entityType = '订单',
}) => {
// 当组件挂载时显示结果
React.useEffect(() => {
if (data) {
showSyncResult(data, entityType);
}
}, [data, entityType]);
// 这个组件不渲染任何内容,只用于显示消息
return null;
};
export default SyncResultMessage;

View File

@ -1,92 +0,0 @@
import { sitecontrollerAll } from '@/servers/api/site';
import { useEffect, useState } from 'react';
// 站点数据的类型定义
interface Site {
id: number;
name: string;
[key: string]: any;
}
// 自定义 Hook:管理站点数据
const useSites = () => {
// 添加站点数据状态
const [sites, setSites] = useState<Site[]>([]);
// 添加加载状态
const [loading, setLoading] = useState<boolean>(false);
// 添加错误状态
const [error, setError] = useState<string | null>(null);
// 获取站点数据
const fetchSites = async () => {
// 设置加载状态为 true
setLoading(true);
// 清空之前的错误信息
setError(null);
try {
// 调用 API 获取所有站点数据
const { data, success } = await sitecontrollerAll();
// 判断请求是否成功
if (success) {
// 将站点数据保存到状态中
setSites(data || []);
} else {
// 如果请求失败,设置错误信息
setError('获取站点数据失败');
}
} catch (error) {
// 捕获异常并打印错误日志
console.error('获取站点数据失败:', error);
// 设置错误信息
setError('获取站点数据时发生错误');
} finally {
// 无论成功与否,都将加载状态设置为 false
setLoading(false);
}
};
// 根据站点ID获取站点名称
const getSiteName = (siteId: number | undefined | null) => {
// 如果站点ID不存在返回默认值
if (!siteId) return '-';
// 如果站点ID是字符串类型直接返回
if (typeof siteId === 'string') {
return siteId;
}
// 在站点列表中查找对应的站点
const site = sites.find((s) => s.id === siteId);
// 如果找到站点返回站点名称否则返回站点ID的字符串形式
return site ? site.name : String(siteId);
};
// 根据站点ID获取站点对象
const getSiteById = (siteId: number | undefined | null) => {
// 如果站点ID不存在返回 null
if (!siteId) return null;
// 在站点列表中查找对应的站点
const site = sites.find((s) => s.id === siteId);
// 返回找到的站点对象,如果找不到则返回 null
return site || null;
};
// 组件加载时获取站点数据
useEffect(() => {
// 调用获取站点数据的函数
fetchSites();
}, []); // 空依赖数组表示只在组件挂载时执行一次
// 返回站点数据和相关方法
return {
sites, // 站点数据列表
loading, // 加载状态
error, // 错误信息
fetchSites, // 重新获取站点数据的方法
getSiteName, // 根据ID获取站点名称的方法
getSiteById, // 根据ID获取站点对象的方法
};
};
// 导出 useSites Hook
export default useSites;

View File

@ -23,7 +23,7 @@ import {
message,
} from 'antd';
import React, { useEffect, useState } from 'react';
import { notAttributes } from '../Product/Attribute/consts';
import { attributes } from '../Attribute/consts';
const { Sider, Content } = Layout;
@ -114,9 +114,7 @@ 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) => attributes.has(d.name));
// Filter out already added attributes
const existingDictIds = new Set(
categoryAttributes.map((ca: any) => ca.dict.id),

View File

@ -1,254 +0,0 @@
import { ordercontrollerGetorders } from '@/servers/api/order';
import {
App,
Col,
Modal,
Row,
Spin,
Statistic,
Table,
Tag,
Typography,
} from 'antd';
import dayjs from 'dayjs';
import { useState } from 'react';
const { Text, Title } = Typography;
interface HistoryOrdersProps {
customer: API.UnifiedCustomerDTO;
siteId?: number;
}
interface OrderStats {
totalOrders: number;
totalAmount: number;
yooneOrders: number;
yooneAmount: number;
}
const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
const { message } = App.useApp();
const [modalVisible, setModalVisible] = useState(false);
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<OrderStats>({
totalOrders: 0,
totalAmount: 0,
yooneOrders: 0,
yooneAmount: 0,
});
// 计算订单统计信息
const calculateStats = (orders: any[]) => {
let totalOrders = 0;
let totalAmount = 0;
let yooneOrders = 0;
let yooneAmount = 0;
orders.forEach((order) => {
totalOrders++;
// total是字符串需要转换为数字
const orderTotal = parseFloat(order.total || '0');
totalAmount += orderTotal;
// 检查订单中是否包含yoone商品
let hasYoone = false;
let orderYooneAmount = 0;
// 优先使用line_items如果没有则使用items
const items = order.line_items || order.items || [];
if (Array.isArray(items)) {
items.forEach((item: any) => {
// 检查商品名称或SKU是否包含yoone(不区分大小写)
const itemName = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
if (itemName.includes('yoone') || sku.includes('yoone')) {
hasYoone = true;
const itemTotal = parseFloat(item.total || item.price || '0');
orderYooneAmount += itemTotal;
}
});
}
if (hasYoone) {
yooneOrders++;
yooneAmount += orderYooneAmount;
}
});
return {
totalOrders,
totalAmount,
yooneOrders,
yooneAmount,
};
};
// 获取客户订单数据
const fetchOrders = async () => {
setLoading(true);
try {
const response = await ordercontrollerGetorders({
where: {
customer_email: customer.email,
},
});
if (response) {
const orderList = response.items || [];
setOrders(orderList);
const calculatedStats = calculateStats(orderList);
setStats(calculatedStats);
} else {
message.error('获取订单数据失败');
}
} catch (error) {
console.error('获取订单失败:', error);
message.error('获取订单失败');
} finally {
setLoading(false);
}
};
// 打开弹框时获取数据
const handleOpenModal = () => {
setModalVisible(true);
fetchOrders();
};
// 订单表格列配置
const orderColumns = [
{
title: '订单号',
dataIndex: 'externalOrderId',
key: 'externalOrderId',
width: 120,
},
{
title: '订单状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const statusMap: Record<string, string> = {
pending: '待处理',
processing: '处理中',
'on-hold': '等待中',
completed: '已完成',
cancelled: '已取消',
refunded: '已退款',
failed: '失败',
};
return <Tag color="blue">{statusMap[status] || status}</Tag>;
},
},
{
title: '订单金额',
dataIndex: 'total',
key: 'total',
width: 100,
render: (total: string, record: any) => (
<Text>
{record.currency_symbol || '$'}
{parseFloat(total || '0').toFixed(2)}
</Text>
),
},
{
title: '创建时间',
dataIndex: 'date_created',
key: 'date_created',
width: 140,
render: (date: string) => (
<Text>{date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-'}</Text>
),
},
{
title: '包含Yoone',
key: 'hasYoone',
width: 80,
render: (_: any, record: any) => {
let hasYoone = false;
const items = record.line_items || record.items || [];
if (Array.isArray(items)) {
hasYoone = items.some((item: any) => {
const itemName = (item.name || '').toLowerCase();
const sku = (item.sku || '').toLowerCase();
return itemName.includes('yoone') || sku.includes('yoone');
});
}
return hasYoone ? <Tag color="green"></Tag> : <Tag></Tag>;
},
},
];
return (
<>
<a onClick={handleOpenModal}></a>
<Modal
title={`${customer.fullname || customer.email} 的历史订单`}
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={null}
width={1000}
>
<Spin spinning={loading}>
{/* 统计信息 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Statistic
title="总订单数"
value={stats.totalOrders}
prefix="#"
/>
</Col>
<Col span={6}>
<Statistic
title="总金额"
value={stats.totalAmount}
precision={2}
prefix="$"
/>
</Col>
<Col span={6}>
<Statistic
title="Yoone订单数"
value={stats.yooneOrders}
prefix="#"
/>
</Col>
<Col span={6}>
<Statistic
title="Yoone金额"
value={stats.yooneAmount}
precision={2}
prefix="$"
/>
</Col>
</Row>
{/* 订单列表 */}
<Title level={4} style={{ marginTop: 24 }}>
</Title>
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 800 }}
/>
</Spin>
</Modal>
</>
);
};
export default HistoryOrders;

View File

@ -1,216 +1,110 @@
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,
PageContainer,
ProColumns,
ProFormDateTimeRangePicker,
ProFormSelect,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { App, Avatar, Button, Form, Rate, Space, Tag, Tooltip } from 'antd';
import { useEffect, useRef, useState } from 'react';
import HistoryOrders from './HistoryOrders';
import { App, Button, Rate, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
// 地址格式化函数
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 ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const [syncModalVisible, setSyncModalVisible] = useState(false);
const columns: ProColumns<API.GetCustomerDTO>[] = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: '原始 ID',
dataIndex: 'origin_id',
sorter: 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 [];
}
},
},
{
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.first_name || ''} ${record.last_name || ''}`.trim() ||
record.username ||
'-'
);
},
},
const columns: ProColumns[] = [
{
title: '用户名',
dataIndex: 'username',
copyable: true,
sorter: 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: '邮箱',
dataIndex: 'email',
copyable: true,
},
{
title: '客户编号',
dataIndex: 'customerId',
render: (_, record) => {
if (!record.customerId) return '-';
return String(record.customerId).padStart(6, 0);
},
sorter: true,
},
{
title: '电话',
dataIndex: 'phone',
copyable: 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: '账单地址',
dataIndex: 'billing',
title: '尾单时间',
hideInSearch: true,
width: 200,
render: (billing) => <AddressCell address={billing} title="账单地址" />,
dataIndex: 'last_purchase_date',
valueType: 'dateTime',
sorter: true,
},
{
title: '物流地址',
dataIndex: 'shipping',
title: '订单数',
dataIndex: 'orders',
hideInSearch: true,
width: 200,
render: (shipping) => <AddressCell address={shipping} title="物流地址" />,
sorter: true,
},
{
title: '评分',
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',
hideInSearch: true,
sorter: true,
width: 200,
render: (_, record) => {
return (
<Rate
@ -218,32 +112,46 @@ const CustomerList: React.FC = () => {
try {
const { success, message: msg } =
await customercontrollerSetrate({
id: record.id,
id: record.customerId,
rate: val,
});
if (success) {
message.success(msg);
actionRef.current?.reload();
}
} catch (e: any) {
message.error(e?.message || '设置评分失败');
} catch (e) {
message.error(e.message);
}
}}
value={record.rate || 0}
allowHalf
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',
hideInSearch: true,
render: (_, record) => {
const tags = record?.tags || [];
return (
<Space size={[0, 8]} wrap>
{tags.map((tag: string) => {
<Space>
{(record.tags || []).map((tag) => {
return (
<Tag
key={tag}
@ -254,14 +162,8 @@ const CustomerList: React.FC = () => {
email: record.email,
tag,
});
if (!success) {
message.error(msg);
return false;
}
actionRef.current?.reload();
return true;
return false;
}}
style={{ marginBottom: 4 }}
>
{tag}
</Tag>
@ -271,110 +173,56 @@ const CustomerList: 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 direction="vertical" size="small">
<Space>
<AddTag
email={record.email || ''}
tags={record.raw?.tags || []}
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
<HistoryOrder
email={record.email}
tags={record.tags}
tableRef={actionRef}
/>
{/* 订单 */}
<HistoryOrders customer={record} siteId={record.site_id} />
</Space>
);
},
},
];
return (
<PageContainer header={{ title: '客户列表' }}>
<PageContainer ghost>
<ProTable
scroll={{ x: 'max-content' }}
headerTitle="查询表格"
actionRef={actionRef}
columns={columns}
rowKey="id"
request={async (params, sorter, filter) => {
console.log('custoemr request', params, sorter, filter);
const { current, pageSize, ...restParams } = params;
const orderBy: any = {};
Object.entries(sorter).forEach(([key, value]) => {
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
request={async (params, sorter) => {
const key = Object.keys(sorter)[0];
const { data, success } = await customercontrollerGetcustomerlist({
...params,
...(key ? { sorterKey: key, sorterValue: sorter[key] } : {}),
});
// 构建查询参数
const queryParams: any = {
page: current || 1,
per_page: pageSize || 20,
where: {
...filter,
...restParams,
},
orderBy,
};
const result = await customercontrollerGetcustomerlist({
params: queryParams,
});
console.log(queryParams, result);
return {
total: result?.data?.total || 0,
data: result?.data?.items || [],
success: true,
total: data?.total || 0,
data: data?.items || [],
success,
};
}}
search={{
labelWidth: 'auto',
span: 6,
}}
pagination={{
pageSize: 20,
showSizeChanger: true,
showQuickJumper: true,
}}
toolBarRender={() => [
<Button
key="sync"
type="primary"
onClick={() => setSyncModalVisible(true)}
>
</Button>,
// 这里可以添加导出、导入等功能按钮
]}
/>
<SyncCustomersModal
visible={syncModalVisible}
onClose={() => setSyncModalVisible(false)}
tableRef={actionRef}
columns={columns}
/>
</PageContainer>
);
};
const AddTag: React.FC<{
export const AddTag: React.FC<{
email: string;
tags?: string[];
tableRef: React.MutableRefObject<ActionType | undefined>;
@ -385,11 +233,7 @@ const AddTag: React.FC<{
return (
<ModalForm
title={`修改标签 - ${email}`}
trigger={
<Button type="link" size="small">
</Button>
}
trigger={<Button></Button>}
width={800}
modalProps={{
destroyOnHidden: true,
@ -406,16 +250,16 @@ const AddTag: React.FC<{
if (!success) return [];
setTagList(tags || []);
return data
.filter((tag: string) => {
.filter((tag) => {
return !(tags || []).includes(tag);
})
.map((tag: string) => ({ label: tag, value: tag }));
.map((tag) => ({ label: tag, value: tag }));
}}
fieldProps={{
value: tagList, // 当前值
onChange: async (newValue) => {
const added = newValue.filter((x) => !(tags || []).includes(x));
const removed = (tags || []).filter((x) => !newValue.includes(x));
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({
@ -438,6 +282,7 @@ const AddTag: React.FC<{
}
}
tableRef?.current?.reload();
setTagList(newValue);
},
}}
@ -446,228 +291,4 @@ const AddTag: React.FC<{
);
};
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);
const [form] = Form.useForm(); // 添加表单实例
// 获取站点列表
useEffect(() => {
if (visible) {
setLoading(true);
sitecontrollerAll()
.then((res: any) => {
setSites(res?.data || []);
})
.catch((error: any) => {
message.error('获取站点列表失败: ' + (error.message || '未知错误'));
})
.finally(() => {
setLoading(false);
});
}
}, [visible]);
// 定义同步参数类型
type SyncParams = {
siteId: number;
search?: string;
role?: string;
dateRange?: [string, string];
orderBy?: string;
};
const handleSync = async (values: SyncParams) => {
try {
setLoading(true);
// 构建过滤参数
const params: any = {};
// 添加搜索关键词
if (values.search) {
params.search = values.search;
}
// 添加角色过滤
if (values.role) {
params.where = {
...params.where,
role: values.role,
};
}
// 添加日期范围过滤(使用 after 和 before 参数)
if (values.dateRange && values.dateRange[0] && values.dateRange[1]) {
params.where = {
...params.where,
after: values.dateRange[0],
before: values.dateRange[1],
};
}
// 添加排序
if (values.orderBy) {
params.orderBy = values.orderBy;
}
const {
success,
message: msg,
data,
} = await customercontrollerSynccustomers({
siteId: values.siteId,
params: Object.keys(params).length > 0 ? params : undefined,
});
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}
form={form}
>
<ProFormSelect
name="siteId"
label="选择站点"
placeholder="请选择要同步的站点"
options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
rules={[{ required: true, message: '请选择站点' }]}
fieldProps={{
loading: loading,
}}
/>
<ProFormText
name="search"
label="搜索关键词"
placeholder="输入邮箱、姓名或用户名进行搜索"
tooltip="支持搜索邮箱、姓名、用户名等字段"
/>
<ProFormSelect
name="role"
label="客户角色"
placeholder="选择客户角色进行过滤"
options={[
{ label: '所有角色', value: '' },
{ label: '管理员', value: 'administrator' },
{ label: '编辑', value: 'editor' },
{ label: '作者', value: 'author' },
{ label: '订阅者', value: 'subscriber' },
{ label: '客户', value: 'customer' },
]}
fieldProps={{
allowClear: true,
}}
/>
<ProFormDateTimeRangePicker
name="dateRange"
label="注册日期范围"
placeholder={['开始日期', '结束日期']}
transform={(value) => {
return {
dateRange: value,
};
}}
fieldProps={{
showTime: false,
style: { width: '100%' },
}}
/>
<ProFormSelect
name="orderBy"
label="排序方式"
placeholder="选择排序方式"
options={[
{ label: '默认排序', value: '' },
{ label: '注册时间(升序)', value: 'date_created:asc' },
{ label: '注册时间(降序)', value: 'date_created:desc' },
{ label: '邮箱(升序)', value: 'email:asc' },
{ label: '邮箱(降序)', value: 'email:desc' },
{ label: '姓名(升序)', value: 'first_name:asc' },
{ label: '姓名(降序)', value: 'first_name:desc' },
]}
fieldProps={{
allowClear: true,
}}
/>
</ModalForm>
);
};
export { AddTag };
export default CustomerList;
export default ListPage;

View File

@ -1,7 +0,0 @@
export default function Statistic() {
return (
<div>
<h1></h1>
</div>
);
}

View File

@ -1,289 +0,0 @@
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: '联系电话',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
title: '账单地址',
dataIndex: 'billing',
render: (_, record) =>
JSON.stringify(record?.billing || record?.shipping),
},
{
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,10 +1,10 @@
import * as dictApi from '@/servers/api/dict';
import { UploadOutlined } from '@ant-design/icons';
import {
ActionType,
PageContainer,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
Button,
Form,
@ -17,8 +17,6 @@ import {
message,
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import DictItemActions from '../components/DictItemActions';
import DictItemModal from '../components/DictItemModal';
const { Sider, Content } = Layout;
@ -28,27 +26,22 @@ const DictPage: React.FC = () => {
const [loadingDicts, setLoadingDicts] = useState(false);
const [searchText, setSearchText] = useState('');
const [selectedDict, setSelectedDict] = useState<any>(null);
// 添加字典 modal 状态
const [isAddDictModalVisible, setIsAddDictModalVisible] = useState(false);
const [addDictName, setAddDictName] = useState('');
const [addDictTitle, setAddDictTitle] = useState('');
const [editingDict, setEditingDict] = useState<any>(null);
const [newDictName, setNewDictName] = useState('');
const [newDictTitle, setNewDictTitle] = useState('');
// 编辑字典 modal 状态
const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false);
const [editDictData, setEditDictData] = useState<any>(null);
// 字典项模态框状态(由 DictItemModal 组件管理)
// 右侧字典项列表的状态
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
const [isEditDictItem, setIsEditDictItem] = useState(false);
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
const [editingDictItem, setEditingDictItem] = useState<any>(null);
const [dictItemForm] = Form.useForm();
const actionRef = useRef<ActionType>();
// 获取字典列表
const fetchDicts = async (name = '') => {
setLoadingDicts(true);
try {
const res = await dictApi.dictcontrollerGetdicts({ name });
const res = await request('/dict/list', { params: { name } });
setDicts(res);
} catch (error) {
message.error('获取字典列表失败');
@ -62,66 +55,60 @@ const DictPage: React.FC = () => {
fetchDicts(value);
};
// 添加字典
// 添加或编辑字典
const handleAddDict = async () => {
const values = { name: addDictName, title: addDictTitle };
const values = { name: newDictName, title: newDictTitle };
try {
await dictApi.dictcontrollerCreatedict(values);
message.success('添加成功');
if (editingDict) {
await request(`/dict/${editingDict.id}`, {
method: 'PUT',
data: values,
});
message.success('更新成功');
} else {
await request('/dict', { method: 'POST', data: values });
message.success('添加成功');
}
setIsAddDictModalVisible(false);
setAddDictName('');
setAddDictTitle('');
setEditingDict(null);
setNewDictName('');
setNewDictTitle('');
fetchDicts(); // 重新获取列表
} catch (error) {
message.error('添加失败');
}
};
// 编辑字典
const handleEditDict = async () => {
if (!editDictData) return;
const values = { name: editDictData.name, title: editDictData.title };
try {
await dictApi.dictcontrollerUpdatedict({ id: editDictData.id }, values);
message.success('更新成功');
setIsEditDictModalVisible(false);
setEditDictData(null);
fetchDicts(); // 重新获取列表
} catch (error) {
message.error('更新失败');
message.error(editingDict ? '更新失败' : '添加失败');
}
};
// 删除字典
const handleDeleteDict = async (id: number) => {
try {
const result = await dictApi.dictcontrollerDeletedict({ id });
if (!result.success) {
throw new Error(result.message || '删除失败');
}
await request(`/dict/${id}`, { method: 'DELETE' });
message.success('删除成功');
fetchDicts();
if (selectedDict?.id === id) {
setSelectedDict(null);
}
} catch (error: any) {
message.error(`删除失败,原因为:${error.message}`);
} catch (error) {
message.error('删除失败');
}
};
// 打开编辑字典 modal
const openEditDictModal = (record: any) => {
setEditDictData(record);
setIsEditDictModalVisible(true);
// 编辑字典
const handleEditDict = (record: any) => {
setEditingDict(record);
setNewDictName(record.name);
setNewDictTitle(record.title);
setIsAddDictModalVisible(true);
};
// 下载字典导入模板
const handleDownloadDictTemplate = async () => {
try {
// 使用 dictApi.dictcontrollerDownloaddicttemplate 获取字典模板
const response = await dictApi.dictcontrollerDownloaddicttemplate({
responseType: 'blob',
skipErrorHandler: true,
// 使用 request 函数获取带认证的文件数据
const response = await request('/dict/template', {
method: 'GET',
responseType: 'blob', // 指定响应类型为 blob
skipErrorHandler: true, // 跳过默认错误处理,自己处理错误
});
// 创建 blob 对象和下载链接
@ -137,78 +124,52 @@ const DictPage: React.FC = () => {
link.remove();
window.URL.revokeObjectURL(url);
} catch (error: any) {
message.error('下载字典模板失败:' + (error.message || '未知错误'));
message.error('下载字典模板失败' + (error.message || '未知错误'));
}
};
// 添加字典项
const handleAddDictItem = () => {
setIsEditDictItem(false);
setEditingDictItemData(null);
setEditingDictItem(null);
dictItemForm.resetFields();
setIsDictItemModalVisible(true);
};
// 编辑字典项
const handleEditDictItem = (record: any) => {
setIsEditDictItem(true);
setEditingDictItemData(record);
setEditingDictItem(record);
dictItemForm.setFieldsValue(record);
setIsDictItemModalVisible(true);
};
// 删除字典项
const handleDeleteDictItem = async (id: number) => {
try {
const result = await dictApi.dictcontrollerDeletedictitem({ id });
if (!result.success) {
throw new Error(result.message || '删除失败');
}
await request(`/dict/item/${id}`, { method: 'DELETE' });
message.success('删除成功');
// 强制刷新字典项列表
setTimeout(() => {
actionRef.current?.reload();
}, 100);
} catch (error: any) {
message.error(`删除失败,原因为:${error.message}`);
actionRef.current?.reload();
} catch (error) {
message.error('删除失败');
}
};
// 处理字典项模态框提交(添加或编辑)
const handleDictItemModalOk = async (values: any) => {
try {
if (isEditDictItem && editingDictItemData) {
// 编辑字典项
const result = await dictApi.dictcontrollerUpdatedictitem(
{ id: editingDictItemData.id },
values,
);
if (!result.success) {
throw new Error(result.message || '更新失败');
}
message.success('更新成功');
} else {
// 添加字典项
const result = await dictApi.dictcontrollerCreatedictitem({
...values,
dictId: selectedDict.id,
});
if (!result.success) {
throw new Error(result.message || '添加失败');
}
message.success('添加成功');
}
setIsDictItemModalVisible(false);
// 字典项表单提交
const handleDictItemFormSubmit = async (values: any) => {
const url = editingDictItem
? `/dict/item/${editingDictItem.id}`
: '/dict/item';
const method = editingDictItem ? 'PUT' : 'POST';
const data = editingDictItem
? { ...values }
: { ...values, dict: { id: selectedDict.id } };
// 强制刷新字典项列表
setTimeout(() => {
actionRef.current?.reload();
}, 100);
} catch (error: any) {
message.error(
`${isEditDictItem ? '更新' : '添加'}失败:${
error.message || '未知错误'
}`,
);
try {
await request(url, { method, data });
message.success(editingDictItem ? '更新成功' : '添加成功');
setIsDictItemModalVisible(false);
actionRef.current?.reload();
} catch (error) {
message.error(editingDictItem ? '更新失败' : '添加失败');
}
};
@ -221,8 +182,9 @@ const DictPage: React.FC = () => {
try {
// 获取当前字典的所有数据
const response = await dictApi.dictcontrollerGetdictitems({
dictId: selectedDict.id,
const response = await request('/dict/items', {
method: 'GET',
params: { dictId: selectedDict.id },
});
if (!response || response.length === 0) {
@ -275,7 +237,7 @@ const DictPage: React.FC = () => {
message.success(`成功导出 ${response.length} 条数据`);
} catch (error: any) {
message.error('导出字典项失败:' + (error.message || '未知错误'));
message.error('导出字典项失败' + (error.message || '未知错误'));
}
};
@ -295,7 +257,7 @@ const DictPage: React.FC = () => {
<Button
type="link"
size="small"
onClick={() => openEditDictModal(record)}
onClick={() => handleEditDict(record)}
>
</Button>
@ -326,7 +288,13 @@ const DictPage: React.FC = () => {
key: 'shortName',
copyable: true,
},
{
title: '图片',
dataIndex: 'image',
key: 'image',
valueType: 'image',
width: 80,
},
{
title: '标题',
dataIndex: 'title',
@ -339,13 +307,6 @@ const DictPage: React.FC = () => {
key: 'titleCN',
copyable: true,
},
{
title: '图片',
dataIndex: 'image',
key: 'image',
valueType: 'image',
width: 80,
},
{
title: '操作',
key: 'action',
@ -370,7 +331,7 @@ const DictPage: React.FC = () => {
<PageContainer>
<Layout style={{ background: '#fff' }}>
<Sider
width={300}
width={240}
style={{
background: '#fff',
padding: '8px',
@ -386,28 +347,17 @@ const DictPage: React.FC = () => {
enterButton
allowClear
/>
<Button
type="primary"
onClick={() => setIsAddDictModalVisible(true)}
size="small"
>
</Button>
<Space size="small">
<Button
type="primary"
onClick={() => setIsAddDictModalVisible(true)}
size="small"
>
</Button>
<Upload
name="file"
action={undefined}
customRequest={async (options) => {
const { file, onSuccess, onError } = options;
try {
const result = await dictApi.dictcontrollerImportdicts({}, [
file as File,
]);
onSuccess?.(result);
} catch (error) {
onError?.(error as Error);
}
}}
action="/dict/import"
showUploadList={false}
onChange={(info) => {
if (info.file.status === 'done') {
@ -451,6 +401,53 @@ const DictPage: React.FC = () => {
</Sider>
<Content style={{ padding: '8px' }}>
<Space direction="vertical" style={{ width: '100%' }}>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
gap: '2px',
}}
>
<Button
type="primary"
onClick={handleAddDictItem}
disabled={!selectedDict}
size="small"
>
</Button>
<Upload
name="file"
action={`/dict/item/import`}
data={{ dictId: selectedDict?.id }}
showUploadList={false}
disabled={!selectedDict}
onChange={(info) => {
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
actionRef.current?.reload();
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
}}
>
<Button
size="small"
icon={<UploadOutlined />}
disabled={!selectedDict}
>
</Button>
</Upload>
<Button
onClick={handleExportDictItems}
disabled={!selectedDict}
size="small"
>
</Button>
</div>
<ProTable
columns={dictItemColumns}
request={async (params) => {
@ -462,24 +459,15 @@ const DictPage: React.FC = () => {
};
}
const { name, title } = params;
const res = await dictApi.dictcontrollerGetdictitems({
dictId: selectedDict?.id,
name,
title,
const res = await request('/dict/items', {
params: {
dictId: selectedDict?.id,
name,
title,
},
});
// 适配新的响应格式,检查是否有 successResponse 包裹
if (res && res.success !== undefined) {
return {
data: res.data || [],
success: res.success,
total: res.data?.length || 0,
};
}
// 兼容旧的响应格式(直接返回数组)
return {
data: res || [],
data: res,
success: true,
};
}}
@ -488,111 +476,74 @@ const DictPage: React.FC = () => {
layout: 'vertical',
}}
pagination={false}
options={{
reload: true,
density: true,
setting: true,
}}
options={false}
size="small"
key={selectedDict?.id}
toolBarRender={() => [
<DictItemActions
key="dictItemActions"
selectedDict={selectedDict}
actionRef={actionRef}
showExport={true}
onImport={async (file: File, dictId: number) => {
// 创建 FormData 对象
const formData = new FormData();
// 添加文件到 FormData
formData.append('file', file);
// 添加字典 ID 到 FormData
formData.append('dictId', String(dictId));
// 调用导入字典项的 API直接返回解析后的 JSON 对象
const result = await dictApi.dictcontrollerImportdictitems(
formData,
);
return result;
}}
onExport={handleExportDictItems}
onAdd={handleAddDictItem}
onRefreshDicts={fetchDicts}
/>,
]}
/>
</Space>
</Content>
</Layout>
{/* 字典项 Modal(添加或编辑) */}
<DictItemModal
visible={isDictItemModalVisible}
isEdit={isEditDictItem}
editingData={editingDictItemData}
selectedDict={selectedDict}
onCancel={() => {
setIsDictItemModalVisible(false);
setEditingDictItemData(null);
}}
onOk={handleDictItemModalOk}
/>
{/* 添加字典 Modal */}
<Modal
title="添加新字典"
open={isAddDictModalVisible}
onOk={handleAddDict}
onCancel={() => {
setIsAddDictModalVisible(false);
setAddDictName('');
setAddDictTitle('');
}}
title={editingDictItem ? '编辑字典项' : '添加字典项'}
open={isDictItemModalVisible}
onOk={() => dictItemForm.submit()}
onCancel={() => setIsDictItemModalVisible(false)}
destroyOnClose
>
<Form layout="vertical">
<Form.Item label="字典名称">
<Input
placeholder="字典名称 (e.g., brand)"
value={addDictName}
onChange={(e) => setAddDictName(e.target.value)}
/>
<Form
form={dictItemForm}
layout="vertical"
onFinish={handleDictItemFormSubmit}
>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="名称 (e.g., zyn)" />
</Form.Item>
<Form.Item label="字典标题">
<Input
placeholder="字典标题 (e.g., 品牌)"
value={addDictTitle}
onChange={(e) => setAddDictTitle(e.target.value)}
/>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="标题 (e.g., ZYN)" />
</Form.Item>
<Form.Item label="中文标题" name="titleCN">
<Input placeholder="中文标题 (e.g., 品牌)" />
</Form.Item>
<Form.Item label="简称 (可选)" name="shortName">
<Input placeholder="简称 (可选)" />
</Form.Item>
<Form.Item label="图片 (可选)" name="image">
<Input placeholder="图片链接 (可选)" />
</Form.Item>
<Form.Item label="值 (可选)" name="value">
<Input placeholder="值 (可选)" />
</Form.Item>
</Form>
</Modal>
{/* 编辑字典 Modal */}
<Modal
title="编辑字典"
open={isEditDictModalVisible}
onOk={handleEditDict}
onCancel={() => {
setIsEditDictModalVisible(false);
setEditDictData(null);
}}
title={editingDict ? '编辑字典' : '添加新字典'}
visible={isAddDictModalVisible}
onOk={handleAddDict}
onCancel={() => setIsAddDictModalVisible(false)}
>
<Form layout="vertical">
<Form.Item label="字典名称">
<Input
placeholder="字典名称 (e.g., brand)"
value={editDictData?.name || ''}
onChange={(e) =>
setEditDictData({ ...editDictData, name: e.target.value })
}
value={newDictName}
onChange={(e) => setNewDictName(e.target.value)}
/>
</Form.Item>
<Form.Item label="字典标题">
<Input
placeholder="字典标题 (e.g., 品牌)"
value={editDictData?.title || ''}
onChange={(e) =>
setEditDictData({ ...editDictData, title: e.target.value })
}
value={newDictTitle}
onChange={(e) => setNewDictTitle(e.target.value)}
/>
</Form.Item>
</Form>

View File

@ -1,57 +0,0 @@
import { ActionType } from '@ant-design/pro-components';
import { Space } from 'antd';
import React from 'react';
import DictItemAddButton from './DictItemAddButton';
import DictItemExportButton from './DictItemExportButton';
import DictItemImportButton from './DictItemImportButton';
// 字典项操作组合组件的属性接口
interface DictItemActionsProps {
// 当前选中的字典
selectedDict?: any;
// ProTable 的 actionRef用于刷新列表
actionRef?: React.MutableRefObject<ActionType | undefined>;
// 是否显示导出按钮(某些页面可能不需要导出功能)
showExport?: boolean;
// 导入字典项的回调函数(如果不提供,则使用默认的导入逻辑)
onImport?: (file: File, dictId: number) => Promise<any>;
// 导出字典项的回调函数
onExport?: () => Promise<void>;
// 添加字典项的回调函数
onAdd?: () => void;
// 刷新字典列表的回调函数(导入成功后可能需要刷新左侧字典列表)
onRefreshDicts?: () => void;
}
// 字典项操作组合组件(包含添加、导入、导出按钮)
const DictItemActions: React.FC<DictItemActionsProps> = ({
selectedDict,
actionRef,
showExport = true,
onImport,
onExport,
onAdd,
onRefreshDicts,
}) => {
return (
<Space>
{/* 添加字典项按钮 */}
{onAdd && <DictItemAddButton disabled={!selectedDict} onClick={onAdd} />}
{/* 导入字典项按钮 */}
<DictItemImportButton
selectedDict={selectedDict}
actionRef={actionRef}
onImport={onImport}
onRefreshDicts={onRefreshDicts}
/>
{/* 导出字典项按钮 */}
{showExport && (
<DictItemExportButton selectedDict={selectedDict} onExport={onExport} />
)}
</Space>
);
};
export default DictItemActions;

View File

@ -1,24 +0,0 @@
import { Button } from 'antd';
import React from 'react';
// 字典项添加按钮组件的属性接口
interface DictItemAddButtonProps {
// 是否禁用按钮
disabled?: boolean;
// 点击按钮时的回调函数
onClick: () => void;
}
// 字典项添加按钮组件
const DictItemAddButton: React.FC<DictItemAddButtonProps> = ({
disabled = false,
onClick,
}) => {
return (
<Button type="primary" size="small" onClick={onClick} disabled={disabled}>
</Button>
);
};
export default DictItemAddButton;

View File

@ -1,53 +0,0 @@
import { DownloadOutlined } from '@ant-design/icons';
import { Button, message } from 'antd';
import React from 'react';
// 字典项导出按钮组件的属性接口
interface DictItemExportButtonProps {
// 当前选中的字典
selectedDict?: any;
// 是否禁用按钮
disabled?: boolean;
// 自定义导出函数
onExport?: () => Promise<void>;
}
// 字典项导出按钮组件
const DictItemExportButton: React.FC<DictItemExportButtonProps> = ({
selectedDict,
disabled = false,
onExport,
}) => {
// 处理导出操作
const handleExport = async () => {
if (!selectedDict) {
message.warning('请先选择字典');
return;
}
try {
// 如果提供了自定义导出函数,则使用自定义函数
if (onExport) {
await onExport();
} else {
// 如果没有提供自定义导出函数,这里可以添加默认逻辑
message.warning('未提供导出函数');
}
} catch (error: any) {
message.error('导出字典项失败:' + (error.message || '未知错误'));
}
};
return (
<Button
size="small"
icon={<DownloadOutlined />}
onClick={handleExport}
disabled={disabled || !selectedDict}
>
</Button>
);
};
export default DictItemExportButton;

View File

@ -1,118 +0,0 @@
import { UploadOutlined } from '@ant-design/icons';
import { ActionType } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Upload, message } from 'antd';
import React from 'react';
// 字典项导入按钮组件的属性接口
interface DictItemImportButtonProps {
// 当前选中的字典
selectedDict?: any;
// ProTable 的 actionRef用于刷新列表
actionRef?: React.MutableRefObject<ActionType | undefined>;
// 是否禁用按钮
disabled?: boolean;
// 自定义导入函数,返回 Promise(如果不提供,则使用默认的导入逻辑)
onImport?: (file: File, dictId: number) => Promise<any>;
// 导入成功后刷新字典列表的回调函数
onRefreshDicts?: () => void;
}
// 字典项导入按钮组件
const DictItemImportButton: React.FC<DictItemImportButtonProps> = ({
selectedDict,
actionRef,
disabled = false,
onImport,
onRefreshDicts,
}) => {
// 处理导入文件上传
const handleImportUpload = async (options: any) => {
console.log(options);
const { file, onSuccess, onError } = options;
try {
// 条件判断,确保已选择字典
if (!selectedDict?.id) {
throw new Error('请先选择字典');
}
let result: any;
// 如果提供了自定义导入函数,则使用自定义函数
if (onImport) {
result = await onImport(file as File, selectedDict.id);
} else {
// 使用默认的导入逻辑,将 dictId 传入到 body 中
const formData = new FormData();
formData.append('file', file as File);
formData.append('dictId', String(selectedDict.id));
result = await request('/api/dict/item/import', {
method: 'POST',
body: formData,
});
}
// 显示导入结果详情
showImportResult(result);
// 导入成功后刷新列表
setTimeout(() => {
actionRef?.current?.reload();
onRefreshDicts?.();
}, 100);
} catch (error: any) {
onError?.(error as Error);
}
};
// 显示导入结果详情
const showImportResult = (result: any) => {
// 从 result.data 中获取实际数据(因为后端返回格式为 { success: true, data: {...} })
const data = result.data || result;
const { total, processed, updated, created, errors } = data;
// 构建结果消息
let messageContent = `总共处理 ${total} 条,成功处理 ${processed} 条,新增 ${created} 条,更新 ${updated}`;
if (errors && errors.length > 0) {
messageContent += `,失败 ${errors.length}`;
// 显示错误详情
const errorDetails = errors
.map((err: any) => `${err.identifier}: ${err.error}`)
.join('\n');
message.warning(messageContent + '\n\n错误详情: \n' + errorDetails);
} else {
message.success(messageContent);
}
};
// 处理上传状态变化
const handleUploadChange = (info: any) => {
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
};
return (
<Upload
name="file"
action={undefined}
customRequest={handleImportUpload}
showUploadList={false}
disabled={disabled || !selectedDict}
onChange={handleUploadChange}
>
<Button
size="small"
icon={<UploadOutlined />}
disabled={disabled || !selectedDict}
>
</Button>
</Upload>
);
};
export default DictItemImportButton;

View File

@ -1,96 +0,0 @@
import { Form, Input, Modal } from 'antd';
import React, { useEffect } from 'react';
interface DictItemModalProps {
// 模态框是否可见
visible: boolean;
// 是否为编辑模式
isEdit: boolean;
// 编辑时的字典项数据
editingData?: any;
// 当前选中的字典
selectedDict?: any;
// 取消回调
onCancel: () => void;
// 确认回调
onOk: (values: any) => Promise<void>;
}
const DictItemModal: React.FC<DictItemModalProps> = ({
visible,
isEdit,
editingData,
selectedDict,
onCancel,
onOk,
}) => {
const [form] = Form.useForm();
// 当模态框打开或编辑数据变化时,重置或设置表单值
useEffect(() => {
if (visible) {
if (isEdit && editingData) {
// 编辑模式,设置表单值为编辑数据
form.setFieldsValue(editingData);
} else {
// 新增模式,重置表单
form.resetFields();
}
}
}, [visible, isEdit, editingData, form]);
// 表单提交处理
const handleOk = async () => {
try {
const values = await form.validateFields();
await onOk(values);
} catch (error) {
// 表单验证失败,不关闭模态框
}
};
return (
<Modal
title={
isEdit
? '编辑字典项'
: `添加字典项 - ${selectedDict?.title || '未选择字典'}`
}
open={visible}
onOk={handleOk}
onCancel={onCancel}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="名称 (e.g., zyn)" />
</Form.Item>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="标题 (e.g., ZYN)" />
</Form.Item>
<Form.Item label="中文标题" name="titleCN">
<Input placeholder="中文标题 (e.g., 品牌)" />
</Form.Item>
<Form.Item label="简称 (可选)" name="shortName">
<Input placeholder="简称 (可选)" />
</Form.Item>
<Form.Item label="图片 (可选)" name="image">
<Input placeholder="图片链接 (可选)" />
</Form.Item>
<Form.Item label="值 (可选)" name="value">
<Input placeholder="值 (可选)" />
</Form.Item>
</Form>
</Modal>
);
};
export default DictItemModal;

View File

@ -393,14 +393,6 @@ const UpdateForm: React.FC<{
}));
}}
/>
<ProFormText
name={['email']}
label="邮箱"
width="lg"
placeholder="请输入邮箱"
required
rules={[{ required: true, message: '请输入邮箱' }]}
/>
<ProForm.Group title="地址">
<ProFormText
name={['address', 'country']}
@ -439,8 +431,6 @@ const UpdateForm: React.FC<{
required
rules={[{ required: true, message: '请输入详细地址' }]}
/>
</ProForm.Group>
<ProFormItem
name="contact"

View File

@ -151,10 +151,7 @@ const OrderItemsPage: React.FC = () => {
}
columns={columns}
request={request}
pagination={{
showSizeChanger: true,
showQuickJumper: true,
}}
pagination={{ showSizeChanger: true }}
search={{ labelWidth: 90, span: 6 }}
toolBarRender={false}
/>

View File

@ -2,7 +2,6 @@ import styles from '../../../style/order-list.css';
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
import SyncForm from '@/components/SyncForm';
import { showSyncResult, SyncResultData } from '@/components/SyncResultMessage';
import { ORDER_STATUS_ENUM } from '@/constants';
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
@ -22,12 +21,13 @@ import {
ordercontrollerGetorders,
ordercontrollerRefundorder,
ordercontrollerSyncorderbyid,
ordercontrollerSyncorders,
ordercontrollerUpdateorderitems,
ordercontrollerExportorder,
} from '@/servers/api/order';
import { productcontrollerSearchproducts } from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { wpproductcontrollerSearchproducts } from '@/servers/api/wpProduct';
import { formatShipmentState, formatSource } from '@/utils/format';
import {
CodeSandboxOutlined,
@ -56,7 +56,6 @@ import {
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
App,
Button,
@ -76,8 +75,8 @@ import {
Tag,
} from 'antd';
import React, { useMemo, useRef, useState } from 'react';
import { request, useParams } from '@umijs/max';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
import dayjs from 'dayjs';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
@ -190,20 +189,11 @@ const ListPage: React.FC = () => {
dataIndex: 'siteId',
valueType: 'select',
request: async () => {
try {
const result = await sitecontrollerAll();
const { success, data } = result;
if (success && data) {
return data.map((site: any) => ({
label: site.name,
value: site.id,
}));
}
return [];
} catch (error) {
console.error('获取站点列表失败:', error);
return [];
}
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
},
{
@ -212,21 +202,11 @@ const ListPage: React.FC = () => {
hideInTable: true,
},
{
title: '订单ID',
dataIndex: 'externalOrderId',
},
{
title: '订单创建日期',
title: '订单日期',
dataIndex: 'date_created',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '付款日期',
dataIndex: 'date_paid',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '金额',
dataIndex: 'total',
@ -274,23 +254,17 @@ const ListPage: React.FC = () => {
},
{
title: '物流',
dataIndex: 'fulfillments',
dataIndex: 'shipmentList',
hideInSearch: true,
render: (_, record) => {
return (
<div>
{(record as any)?.fulfillments?.map((item: any) => {
{(record as any)?.shipmentList?.map((item: any) => {
if (!item) return;
return (
<div
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}
>
<span>: {item.shipping_provider}</span>
<span>: {item.tracking_number}</span>
<div>
{item.tracking_provider}:{item.primary_tracking_number} (
{formatShipmentState(item.state)})
</div>
);
})}
@ -301,6 +275,7 @@ const ListPage: React.FC = () => {
{
title: 'IP',
dataIndex: 'customer_ip_address',
hideInSearch: true,
},
{
title: '设备',
@ -363,18 +338,15 @@ const ListPage: React.FC = () => {
message.error('站点ID或外部订单ID不存在');
return;
}
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorderbyid({
siteId: record.siteId,
orderId: record.externalOrderId,
});
const { success, message: errMsg } =
await ordercontrollerSyncorderbyid({
siteId: record.siteId,
orderId: record.externalOrderId,
});
if (!success) {
throw new Error(errMsg);
}
showSyncResult(data as SyncResultData, '订单');
message.success('同步成功');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
@ -483,6 +455,7 @@ const ListPage: React.FC = () => {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
rowClassName={(record) => {
return record.id === activeLine
? styles['selected-line-order-protable']
@ -491,27 +464,19 @@ const ListPage: React.FC = () => {
pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000'],
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10,
}}
toolBarRender={() => [
// <CreateOrder tableRef={actionRef} />,
<CreateOrder tableRef={actionRef} />,
<SyncForm
onFinish={async (values: any) => {
try {
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorders(values, {
after: values.dateRange?.[0] + 'T00:00:00Z',
before: values.dateRange?.[1] + 'T23:59:59Z',
});
const { success, message: errMsg } =
await ordercontrollerSyncorderbyid(values);
if (!success) {
throw new Error(errMsg);
}
// 使用 showSyncResult 函数显示详细的同步结果
showSyncResult(data as SyncResultData, '订单');
message.success('同步成功');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
@ -519,26 +484,31 @@ const ListPage: React.FC = () => {
}}
tableRef={actionRef}
/>,
<Popconfirm
// <Button
// type="primary"
// disabled={selectedRowKeys.length === 0}
// onClick={handleBatchExport}
// >
// 批量导出
// </Button>,
<Popconfirm
title="批量导出"
description="确认导出选中的订单吗?"
onConfirm={async () => {
console.log(selectedRowKeys);
try {
const res = await request('/order/export', {
method: 'POST',
data: {
const res = await request('/order/order/export', {
method: 'GET',
params: {
ids: selectedRowKeys,
},
}
});
if (res?.success && res.data) {
const blob = new Blob([res.data], {
type: 'text/csv;charset=utf-8;',
});
if (res?.success && res?.data?.csv) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'orders.csv';
a.download = 'customers.csv';
a.click();
URL.revokeObjectURL(url);
} else {
@ -549,12 +519,15 @@ const ListPage: React.FC = () => {
} catch (error: any) {
message.error(error?.message || '导出失败');
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
</Popconfirm>
]}
request={async ({ date, ...param }: any) => {
if (param.status === 'all') {
@ -643,36 +616,33 @@ const Detail: React.FC<{
)
? []
: [
<Divider type="vertical" />,
<Button
type="primary"
onClick={async () => {
try {
if (!record.siteId || !record.externalOrderId) {
message.error('站点ID或外部订单ID不存在');
return;
}
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorderbyid({
<Divider type="vertical" />,
<Button
type="primary"
onClick={async () => {
try {
if (!record.siteId || !record.externalOrderId) {
message.error('站点ID或外部订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerSyncorderbyid({
siteId: record.siteId,
orderId: record.externalOrderId,
});
if (!success) {
throw new Error(errMsg);
}
showSyncResult(data as SyncResultData, '订单');
tableRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
if (!success) {
throw new Error(errMsg);
}
}}
>
</Button>,
]),
message.success('同步成功');
tableRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
}
}}
>
</Button>,
]),
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
// ? [
// <Divider type="vertical" />,
@ -691,152 +661,152 @@ const Detail: React.FC<{
'pending_refund',
].includes(record.orderStatus)
? [
<Divider type="vertical" />,
<Popconfirm
title="转至售后"
description="确认转至售后?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
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);
<Divider type="vertical" />,
<Popconfirm
title="转至售后"
description="确认转至售后?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
]
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);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
]
: []),
...(record.orderStatus === 'after_sale_pending'
? [
<Divider type="vertical" />,
<Popconfirm
title="转至取消"
description="确认转至取消?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerCancelorder({
<Divider type="vertical" />,
<Popconfirm
title="转至取消"
description="确认转至取消?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
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,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
},
{
status: 'pending_reshipment',
},
);
if (!success) {
throw new Error(errMsg);
}
}}
>
<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,
},
{
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>,
]
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
]
: []),
]}
>
@ -994,14 +964,13 @@ const Detail: React.FC<{
<ul>
{record?.items?.map((item: any) => (
<li key={item.id}>
{item.name}({item.sku}):{item.quantity}
{item.name}:{item.quantity}
</li>
))}
</ul>
);
}}
/>
{/* 显示 related order */}
<ProDescriptions.Item
label="关联"
@ -1019,7 +988,7 @@ const Detail: React.FC<{
<ul>
{record?.sales?.map((item: any) => (
<li key={item.id}>
{item.name}({item.sku}):{item.quantity}
{item.name}:{item.quantity}
</li>
))}
</ul>
@ -1099,31 +1068,31 @@ const Detail: React.FC<{
}
actions={
v.state === 'waiting-for-scheduling' ||
v.state === 'waiting-for-transit'
v.state === 'waiting-for-transit'
? [
<Popconfirm
title="取消运单"
description="确认取消运单?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await logisticscontrollerDelshipment({
id: v.id,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
initRequest();
} catch (error: any) {
message.error(error.message);
<Popconfirm
title="取消运单"
description="确认取消运单?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await logisticscontrollerDelshipment({
id: v.id,
});
if (!success) {
throw new Error(errMsg);
}
}}
>
<DeleteFilled />
</Popconfirm>,
]
tableRef.current?.reload();
initRequest();
} catch (error: any) {
message.error(error.message);
}
}}
>
<DeleteFilled />
</Popconfirm>,
]
: []
}
>
@ -1268,10 +1237,7 @@ const Shipping: React.FC<{
const [rates, setRates] = useState<API.RateDTO[]>([]);
const [ratesLoading, setRatesLoading] = useState(false);
const { message } = App.useApp();
const [shipmentPlatforms, setShipmentPlatforms] = useState([
{ label: 'uniuni', value: 'uniuni' },
{ label: 'tms.freightwaves', value: 'freightwaves' },
]);
return (
<ModalForm
formRef={formRef}
@ -1300,7 +1266,6 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
await ordercontrollerGetorderdetail({
orderId: id,
});
console.log('success data',success,data)
if (!success || !data) return {};
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
@ -1323,8 +1288,7 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
if (reShipping) data.sales = [{}];
let shipmentInfo = localStorage.getItem('shipmentInfo');
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
const a = {
shipmentPlatform: 'uniuni',
return {
...data,
// payment_method_id: shipmentInfo?.payment_method_id,
stockPointId: shipmentInfo?.stockPointId,
@ -1384,7 +1348,6 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
},
},
};
return a
}}
onFinish={async ({
customer_note,
@ -1448,18 +1411,7 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
}
}}
>
<Row gutter={16}>
<Col span={8}>
<ProFormSelect
name="shipmentPlatform"
label="发货平台"
options={shipmentPlatforms}
placeholder="请选择发货平台"
rules={[{ required: true, message: '请选择一个选项' }]}
/>
</Col>
</Row>
<ProFormText label="订单号" readonly name='externalOrderId' />
<ProFormText label="订单号" readonly name={'externalOrderId'} />
<ProFormText label="客户备注" readonly name="customer_note" />
<ProFormList
label="后台备注"
@ -1528,16 +1480,16 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
<ProFormList
label="发货产品"
name="sales"
// rules={[
// {
// required: true,
// message: '至少需要一个商品',
// validator: (_, value) =>
// value && value.length > 0
//</Col> ? Promise.resolve()
// : Promise.reject('至少需要一个商品'),
// },
// ]}
// rules={[
// {
// required: true,
// message: '至少需要一个商品',
// validator: (_, value) =>
// value && value.length > 0
// ? Promise.resolve()
// : Promise.reject('至少需要一个商品'),
// },
// ]}
>
<ProForm.Group>
<ProFormSelect
@ -1591,21 +1543,16 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
title="发货信息"
extra={
<AddressPicker
onChange={(row) => {
console.log(row);
const {
address,
phone_number,
phone_number_extension,
stockPointId,
email,
} = row;
onChange={({
address,
phone_number,
phone_number_extension,
stockPointId,
}) => {
formRef?.current?.setFieldsValue({
stockPointId,
// address_id: row.id,
details: {
origin: {
email_addresses:email,
address,
phone_number: {
phone: phone_number,
@ -1618,11 +1565,6 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
/>
}
>
{/* <ProFormText
label="address_id"
name={'address_id'}
rules={[{ required: true, message: '请输入ID' }]}
/> */}
<ProFormSelect
name="stockPointId"
width="md"
@ -1715,6 +1657,7 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
<ProFormText
label="公司名称"
name={['details', 'destination', 'name']}
rules={[{ required: true, message: '请输入公司名称' }]}
/>
<ProFormItem
name={['details', 'destination', 'address', 'country']}
@ -1971,7 +1914,7 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
name="description"
placeholder="请输入描述"
width="lg"
// rules={[{ required: true, message: '请输入描述' }]}
// rules={[{ required: true, message: '请输入描述' }]}
/>
</ProForm.Group>
</ProFormList>
@ -2044,7 +1987,6 @@ const [shipmentPlatforms, setShipmentPlatforms] = useState([
details.origin.phone_number.number =
details.origin.phone_number.phone;
const res = await logisticscontrollerGetshipmentfee({
shipmentPlatform: data.shipmentPlatform,
stockPointId: data.stockPointId,
sender: details.origin.contact_name,
@ -2183,12 +2125,12 @@ const SalesChange: React.FC<{
params={{}}
request={async ({ keyWords }) => {
try {
const { data } = await productcontrollerSearchproducts({
const { data } = await wpproductcontrollerSearchproducts({
name: keyWords,
});
return data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
label: `${item.name}`,
value: item?.sku,
};
});
@ -2371,7 +2313,7 @@ const CreateOrder: React.FC<{
<ProFormText
label="公司名称"
name={['billing', 'company']}
rules={[{ message: '请输入公司名称' }]}
rules={[{ required: true, message: '请输入公司名称' }]}
/>
<ProFormItem
name={['billing', 'country']}
@ -2457,11 +2399,6 @@ const AddressPicker: React.FC<{
value: item.id,
}));
},
},
{
title: 'id',
dataIndex: ['id'],
hideInSearch: true,
},
{
title: '地区',
@ -2489,11 +2426,6 @@ const AddressPicker: React.FC<{
`+${record.phone_number_extension} ${record.phone_number}`,
hideInSearch: true,
},
{
title: '邮箱',
dataIndex: [ 'email'],
hideInSearch: true,
},
];
return (
<ModalForm

View File

@ -1 +1,8 @@
export const notAttributes = new Set(['zh-cn', 'en-us', 'category']);
// 限定允许管理的字典名称集合
export const attributes = new Set([
'brand',
'strength',
'flavor',
'size',
'humidity',
]);

View File

@ -1,19 +1,26 @@
import * as dictApi from '@/servers/api/dict';
import { UploadOutlined } from '@ant-design/icons';
import {
ActionType,
PageContainer,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Input, Layout, Space, Table, message } from 'antd';
import {
Button,
Form,
Input,
Layout,
Modal,
Space,
Table,
Upload,
message,
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import DictItemActions from '../../Dict/components/DictItemActions';
import DictItemModal from '../../Dict/components/DictItemModal';
const { Sider, Content } = Layout;
import { notAttributes } from './consts';
import { attributes } from './consts';
const AttributePage: React.FC = () => {
// 左侧字典列表状态
@ -25,97 +32,20 @@ const AttributePage: React.FC = () => {
// 右侧字典项 ProTable 的引用
const actionRef = useRef<ActionType>();
// 字典项模态框状态(由 DictItemModal 组件管理)
// 字典项新增/编辑模态框控制
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
const [isEditDictItem, setIsEditDictItem] = useState(false);
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
// 导出字典项数据
const handleExportDictItems = async () => {
// 条件判断,确保已选择字典
if (!selectedDict) {
message.warning('请先选择字典');
return;
}
try {
// 获取当前字典的所有数据
const response = await request('/dict/items', {
params: {
dictId: selectedDict.id,
},
});
// 确保返回的是数组
const data = Array.isArray(response) ? response : response?.data || [];
// 条件判断,检查是否有数据可导出
if (data.length === 0) {
message.warning('当前字典没有数据可导出');
return;
}
// 将数据转换为CSV格式
const headers = [
'name',
'title',
'titleCN',
'value',
'sort',
'image',
'shortName',
];
const csvContent = [
headers.join(','),
...data.map((item: any) =>
headers
.map((header) => {
const value = item[header] || '';
// 条件判断,如果值包含逗号或引号,需要转义
if (
typeof value === 'string' &&
(value.includes(',') || value.includes('"'))
) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
})
.join(','),
),
].join('\n');
// 创建blob并下载
const blob = new Blob(['\ufeff' + csvContent], {
// 添加BOM以支持中文
type: 'text/csv;charset=utf-8',
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
message.success(`成功导出 ${data.length} 条数据`);
} catch (error: any) {
message.error('导出字典项失败:' + (error.message || '未知错误'));
}
};
const [editingDictItem, setEditingDictItem] = useState<any>(null);
const [dictItemForm] = Form.useForm();
const fetchDicts = async (title?: string) => {
setLoadingDicts(true);
try {
const res = await request('/dict/list', { params: { title } });
// 条件判断,确保res是数组再进行过滤
const dataList = Array.isArray(res) ? res : res?.data || [];
const filtered = dataList.filter((d: any) => !notAttributes.has(d?.name));
// 条件判断,过滤只保留 allowedDictNames 中的字典
const filtered = (res || []).filter((d: any) => attributes.has(d?.name));
setDicts(filtered);
} catch (error) {
console.error('获取字典列表失败:', error);
message.error('获取字典列表失败');
setDicts([]);
}
setLoadingDicts(false);
};
@ -132,24 +62,24 @@ const AttributePage: React.FC = () => {
// 打开添加字典项模态框
const handleAddDictItem = () => {
setIsEditDictItem(false);
setEditingDictItemData(null);
setEditingDictItem(null);
dictItemForm.resetFields();
setIsDictItemModalVisible(true);
};
// 打开编辑字典项模态框
const handleEditDictItem = (item: any) => {
setIsEditDictItem(true);
setEditingDictItemData(item);
setEditingDictItem(item);
dictItemForm.setFieldsValue(item);
setIsDictItemModalVisible(true);
};
// 字典项表单提交(新增或编辑)
const handleDictItemFormSubmit = async (values: any) => {
try {
if (isEditDictItem && editingDictItemData) {
if (editingDictItem) {
// 条件判断,存在编辑项则执行更新
await request(`/dict/item/${editingDictItemData.id}`, {
await request(`/dict/item/${editingDictItem.id}`, {
method: 'PUT',
data: values,
});
@ -165,7 +95,7 @@ const AttributePage: React.FC = () => {
setIsDictItemModalVisible(false);
actionRef.current?.reload(); // 刷新 ProTable
} catch (error) {
message.error(isEditDictItem ? '更新失败' : '添加失败');
message.error(editingDictItem ? '更新失败' : '添加失败');
}
};
@ -184,23 +114,16 @@ const AttributePage: React.FC = () => {
return;
}
if (selectedDict?.id) {
try {
const list = await request('/dict/items', {
params: {
dictId: selectedDict.id,
},
});
// 确保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);
const list = await request('/dict/items', {
params: {
dictId: selectedDict.id,
},
});
const exists =
Array.isArray(list) && list.some((it: any) => it.id === itemId);
if (exists) {
message.error('删除失败');
} else {
message.success('删除成功');
actionRef.current?.reload();
}
@ -220,35 +143,11 @@ const AttributePage: React.FC = () => {
];
// 右侧字典项列表列定义(紧凑样式)
const dictItemColumns: ProColumns<any>[] = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
copyable: true,
sorter: true,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
copyable: true,
sorter: true,
},
{
title: '中文标题',
dataIndex: 'titleCN',
key: 'titleCN',
copyable: true,
sorter: true,
},
{
title: '简称',
dataIndex: 'shortName',
key: 'shortName',
copyable: true,
sorter: true,
},
const dictItemColumns: any[] = [
{ title: '名称', dataIndex: 'name', key: 'name', copyable: true },
{ title: '标题', dataIndex: 'title', key: 'title', copyable: true },
{ title: '中文标题', dataIndex: 'titleCN', key: 'titleCN', copyable: true },
{ title: '简称', dataIndex: 'shortName', key: 'shortName', copyable: true },
{
title: '图片',
dataIndex: 'image',
@ -346,86 +245,106 @@ const AttributePage: React.FC = () => {
};
}
const { name, title } = params;
try {
const res = await request('/dict/items', {
params: {
dictId: selectedDict.id,
name,
title,
},
});
// 确保返回的是数组
const data = Array.isArray(res) ? res : res?.data || [];
return {
data: data,
success: true,
};
} catch (error) {
console.error('获取字典项失败:', error);
return {
data: [],
success: false,
};
}
const res = await request('/dict/items', {
params: {
dictId: selectedDict.id,
name,
title,
},
});
return {
data: res,
success: true,
};
}}
rowKey="id"
search={{
layout: 'vertical',
}}
pagination={false}
options={{
reload: true,
density: false,
setting: {
draggable: true,
checkable: true,
checkedReset: false,
},
search: false,
fullScreen: false,
}}
options={false}
size="small"
key={selectedDict?.id}
headerTitle={
<DictItemActions
selectedDict={selectedDict}
actionRef={actionRef}
showExport={true}
onImport={async (file: File, dictId: number) => {
// 创建 FormData 对象
const formData = new FormData();
// 添加文件到 FormData
formData.append('file', file);
// 添加字典 ID 到 FormData
formData.append('dictId', String(dictId));
// 调用导入字典项的 API
const response = await dictApi.dictcontrollerImportdictitems(
formData,
);
// 返回 JSON 响应
return await response.json();
}}
onExport={handleExportDictItems}
onAdd={handleAddDictItem}
onRefreshDicts={fetchDicts}
/>
<Space>
<Button
type="primary"
size="small"
onClick={handleAddDictItem}
disabled={!selectedDict}
>
</Button>
<Upload
name="file"
action={`/dict/item/import`}
data={{ dictId: selectedDict?.id }}
showUploadList={false}
disabled={!selectedDict}
onChange={(info) => {
// 条件判断,上传状态处理
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功`);
actionRef.current?.reload();
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败`);
}
}}
>
<Button
size="small"
icon={<UploadOutlined />}
disabled={!selectedDict}
>
</Button>
</Upload>
</Space>
}
/>
</Content>
</Layout>
{/* 字典项 Modal(添加或编辑) */}
<DictItemModal
visible={isDictItemModalVisible}
isEdit={isEditDictItem}
editingData={editingDictItemData}
selectedDict={selectedDict}
onCancel={() => {
setIsDictItemModalVisible(false);
setEditingDictItemData(null);
}}
onOk={handleDictItemFormSubmit}
/>
<Modal
title={editingDictItem ? '编辑字典项' : '添加字典项'}
open={isDictItemModalVisible}
onOk={() => dictItemForm.submit()}
onCancel={() => setIsDictItemModalVisible(false)}
destroyOnClose
>
<Form
form={dictItemForm}
layout="vertical"
onFinish={handleDictItemFormSubmit}
>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input size="small" placeholder="名称 (e.g., zyn)" />
</Form.Item>
<Form.Item
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input size="small" placeholder="标题 (e.g., ZYN)" />
</Form.Item>
<Form.Item label="中文标题" name="titleCN">
<Input size="small" placeholder="中文标题 (e.g., 品牌)" />
</Form.Item>
<Form.Item label="简称 (可选)" name="shortName">
<Input size="small" placeholder="简称 (可选)" />
</Form.Item>
<Form.Item label="图片 (可选)" name="image">
<Input size="small" placeholder="图片链接 (可选)" />
</Form.Item>
<Form.Item label="值 (可选)" name="value">
<Input size="small" placeholder="值 (可选)" />
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
};

View File

@ -23,7 +23,7 @@ import {
message,
} from 'antd';
import React, { useEffect, useState } from 'react';
import { notAttributes } from '../Attribute/consts';
import { attributes } from '../Attribute/consts';
const { Sider, Content } = Layout;
@ -116,7 +116,7 @@ const CategoryPage: React.FC = () => {
const res = await request('/dict/list');
// Defensive check for response structure: handle both raw array and wrapped response
const list = Array.isArray(res) ? res : res?.data || [];
const filtered = list.filter((d: any) => !notAttributes.has(d.name));
const filtered = list.filter((d: any) => attributes.has(d.name));
// Filter out already added attributes
const existingDictIds = new Set(
categoryAttributes.map((ca: any) => ca.dictId),
@ -244,10 +244,7 @@ const CategoryPage: React.FC = () => {
</Popconfirm>,
]}
>
<List.Item.Meta
title={`${item.title}(${item.titleCN ?? '-'})`}
description={`${item.name} | ${item.shortName ?? '-'}`}
/>
<List.Item.Meta title={item.title} description={item.name} />
</List.Item>
)}
/>
@ -255,9 +252,7 @@ const CategoryPage: React.FC = () => {
<Content style={{ padding: '24px' }}>
{selectedCategory ? (
<Card
title={`分类:${selectedCategory.title} (${
selectedCategory.shortName ?? selectedCategory.name
})`}
title={`分类:${selectedCategory.title} (${selectedCategory.name})`}
extra={
<Button type="primary" onClick={handleAddAttribute}>
@ -315,21 +310,16 @@ const CategoryPage: React.FC = () => {
onFinish={handleCategorySubmit}
layout="vertical"
>
<Form.Item name="title" label="标题">
<Form.Item name="title" label="标题" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="titleCN" label="中文名称">
<Form.Item
name="name"
label="标识 (Code)"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item name="shortName" label="短名称">
<Input />
</Form.Item>
<Form.Item name="name" label="标识 (Code)">
<Input />
</Form.Item>
<Form.Item name="sort" label="排序">
<Input type="number" />
</Form.Item>
</Form>
</Modal>

View File

@ -1,908 +0,0 @@
import { productcontrollerGetcategoriesall } from '@/servers/api/product';
import { UploadOutlined } from '@ant-design/icons';
import {
PageContainer,
ProForm,
ProFormSelect,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Card, Checkbox, Col, Input, message, Row, Upload } from 'antd';
import React, { useEffect, useState } from 'react';
import * as XLSX from 'xlsx';
// 定义站点接口
interface Site {
id: number;
name: string;
skuPrefix?: string;
isDisabled?: boolean;
}
// 定义选项接口,用于下拉选择框的选项
interface Option {
name: string; // 显示名称
shortName: string; // 短名称用于生成SKU
}
// 定义配置接口
interface SkuConfig {
brands: Option[];
categories: Option[];
flavors: Option[];
strengths: Option[];
humidities: Option[];
versions: Option[];
sizes: Option[];
quantities: Option[];
}
// 定义通用属性映射接口用于存储属性名称和shortName的对应关系
interface AttributeMapping {
[attributeName: string]: string; // key: 属性名称, value: 属性shortName
}
// 定义所有属性映射的接口
interface AttributeMappings {
brands: AttributeMapping;
categories: AttributeMapping;
flavors: AttributeMapping;
strengths: AttributeMapping;
humidities: AttributeMapping;
versions: AttributeMapping;
sizes: AttributeMapping;
quantities: AttributeMapping;
}
/**
* @description CSV工具页面SKU
*/
const CsvTool: React.FC = () => {
// 状态管理
const [form] = ProForm.useForm();
const [file, setFile] = useState<File | null>(null);
const [csvData, setCsvData] = useState<any[]>([]);
const [processedData, setProcessedData] = useState<any[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [sites, setSites] = useState<Site[]>([]);
const [selectedSites, setSelectedSites] = useState<Site[]>([]); // 现在使用多选
const [generateBundleSkuForSingle, setGenerateBundleSkuForSingle] =
useState(true); // 是否为type为single的记录生成包含quantity的bundle SKU
const [config, setConfig] = useState<SkuConfig>({
brands: [],
categories: [],
flavors: [],
strengths: [],
humidities: [],
versions: [],
sizes: [],
quantities: [],
});
// 所有属性名称到shortName的映射
const [attributeMappings, setAttributeMappings] = useState<AttributeMappings>(
{
brands: {},
categories: {},
flavors: {},
strengths: {},
humidities: {},
versions: {},
sizes: {},
quantities: {},
},
);
// 在组件加载时获取站点列表和字典数据
useEffect(() => {
const fetchAllData = async () => {
try {
message.loading({ content: '正在加载数据...', key: 'loading' });
// 1. 获取站点列表
const sitesResponse = await request('/site/all');
const siteList = sitesResponse?.data || sitesResponse || [];
setSites(siteList);
// 默认选择所有站点
setSelectedSites(siteList);
// 2. 获取字典数据
const dictListResponse = await request('/dict/list');
const dictList = dictListResponse?.data || dictListResponse || [];
// 3. 根据字典名称获取字典项返回包含name和shortName的完整对象数组
const getDictItems = async (dictName: string) => {
try {
const dict = dictList.find((d: any) => d.name === dictName);
if (!dict) {
console.warn(`Dictionary ${dictName} not found`);
return { options: [], mapping: {} };
}
const itemsResponse = await request('/dict/items', {
params: { dictId: dict.id },
});
const items = itemsResponse?.data || itemsResponse || [];
// 创建完整的选项数组
const options = items.map((item: any) => ({
name: item.name,
shortName: item.shortName || item.name,
}));
// 创建name到shortName的映射
const mapping = items.reduce((acc: AttributeMapping, item: any) => {
acc[item.name] = item.shortName || item.name;
return acc;
}, {});
return { options, mapping };
} catch (error) {
console.error(`Failed to fetch items for ${dictName}:`, error);
return { options: [], mapping: {} };
}
};
// 4. 获取所有字典项(品牌、口味、强度、湿度、版本、尺寸、数量)
const [
brandResult,
flavorResult,
strengthResult,
humidityResult,
versionResult,
sizeResult,
quantityResult,
] = await Promise.all([
getDictItems('brand'),
getDictItems('flavor'),
getDictItems('strength'),
getDictItems('humidity'),
getDictItems('version'),
getDictItems('size'),
getDictItems('quantity'),
]);
// 5. 获取商品分类列表
const categoriesResponse = await productcontrollerGetcategoriesall();
const categoryOptions =
categoriesResponse?.data?.map((category: any) => ({
name: category.name,
shortName: category.shortName || category.name,
})) || [];
// 商品分类的映射如果分类有shortName的话
const categoryMapping =
categoriesResponse?.data?.reduce(
(acc: AttributeMapping, category: any) => {
acc[category.name] = category.shortName || category.name;
return acc;
},
{},
) || {};
// 6. 设置所有属性映射
setAttributeMappings({
brands: brandResult.mapping,
categories: categoryMapping,
flavors: flavorResult.mapping,
strengths: strengthResult.mapping,
humidities: humidityResult.mapping,
versions: versionResult.mapping,
sizes: sizeResult.mapping,
quantities: quantityResult.mapping,
});
// 更新配置状态
const newConfig = {
brands: brandResult.options,
categories: categoryOptions,
flavors: flavorResult.options,
strengths: strengthResult.options,
humidities: humidityResult.options,
versions: versionResult.options,
sizes: sizeResult.options,
quantities: quantityResult.options,
};
setConfig(newConfig);
// 设置表单值时只需要name数组
form.setFieldsValue({
brands: brandResult.options.map((opt) => opt.name),
categories: categoryOptions.map((opt) => opt.name),
flavors: flavorResult.options.map((opt) => opt.name),
strengths: strengthResult.options.map((opt) => opt.name),
humidities: humidityResult.options.map((opt) => opt.name),
versions: versionResult.options.map((opt) => opt.name),
sizes: sizeResult.options.map((opt) => opt.name),
quantities: quantityResult.options.map((opt) => opt.name),
generateBundleSkuForSingle: true,
});
message.success({ content: '数据加载成功', key: 'loading' });
} catch (error) {
console.error('Failed to fetch data:', error);
message.error({
content: '数据加载失败,请刷新页面重试',
key: 'loading',
});
}
};
fetchAllData();
}, [form]);
/**
* @description
*/
const handleFileUpload = (uploadedFile: File) => {
// 检查文件类型
if (!uploadedFile.name.match(/\.(csv|xlsx|xls)$/)) {
message.error('请上传 CSV 或 Excel 格式的文件!');
return false;
}
setFile(uploadedFile);
const reader = new FileReader();
// 检查是否为CSV文件
const isCsvFile = uploadedFile.name.match(/\.csv$/i);
if (isCsvFile) {
// 对于CSV文件使用readAsText并指定UTF-8编码以正确处理中文
reader.onload = (e) => {
try {
const textData = e.target?.result as string;
// 使用XLSX.read处理CSV文本数据指定type为'csv'并设置编码
const workbook = XLSX.read(textData, {
type: 'string',
codepage: 65001, // UTF-8 encoding
cellText: true,
cellDates: true,
});
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
message.error('文件为空或缺少表头!');
setCsvData([]);
return;
}
// 将数组转换为对象数组
const headers = jsonData[0] as string[];
const rows = jsonData.slice(1).map((rowArray: any) => {
const rowData: { [key: string]: any } = {};
headers.forEach((header, index) => {
rowData[header] = rowArray[index];
});
return rowData;
});
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('CSV文件解析失败,请检查文件格式和编码!');
console.error('CSV Parse Error:', error);
setCsvData([]);
}
};
reader.readAsText(uploadedFile, 'UTF-8');
} else {
// 对于Excel文件继续使用readAsArrayBuffer
reader.onload = (e) => {
try {
const data = e.target?.result;
// 如果是ArrayBuffer使用type: 'array'来处理
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
message.error('文件为空或缺少表头!');
setCsvData([]);
return;
}
// 将数组转换为对象数组
const headers = jsonData[0] as string[];
const rows = jsonData.slice(1).map((rowArray: any) => {
const rowData: { [key: string]: any } = {};
headers.forEach((header, index) => {
rowData[header] = rowArray[index];
});
return rowData;
});
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('Excel文件解析失败,请检查文件格式!');
console.error('Excel Parse Error:', error);
setCsvData([]);
}
};
reader.readAsArrayBuffer(uploadedFile);
}
reader.onerror = (error) => {
message.error('文件读取失败!');
console.error('File Read Error:', error);
};
return false; // 阻止antd Upload组件的默认上传行为
};
/**
* @description CSV并触发下载
*/
const downloadData = (data: any[]) => {
if (data.length === 0) return;
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(data);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products with SKU');
const fileName = `products_with_sku_${Date.now()}.xlsx`;
XLSX.writeFile(workbook, fileName);
message.success('下载任务已开始!');
};
/**
* @description SKU
* @param {string} brand -
* @param {string} category -
* @param {string} flavor -
* @param {string} strength -
* @param {string} humidity - 湿
* @param {string} version -
* @param {string} type -
* @returns {string} SKU
*/
const generateSku = (
brand: string,
version: string,
category: string,
flavor: string,
strength: string,
humidity: string,
size: string,
quantity?: any,
type?: string,
): string => {
// 构建SKU组件不包含站点前缀
const skuComponents: string[] = [];
// 按顺序添加SKU组件所有属性都使用shortName
if (brand) {
// 使用品牌的shortName如果没有则使用品牌名称
const brandShortName = attributeMappings.brands[brand] || brand;
skuComponents.push(brandShortName);
}
if (version) {
// 使用版本的shortName如果没有则使用版本名称
const versionShortName = attributeMappings.versions[version] || version;
skuComponents.push(versionShortName);
}
if (category) {
// 使用分类的shortName如果没有则使用分类名称
const categoryShortName =
attributeMappings.categories[category] || category;
skuComponents.push(categoryShortName);
}
if (flavor) {
// 使用口味的shortName如果没有则使用口味名称
const flavorShortName = attributeMappings.flavors[flavor] || flavor;
skuComponents.push(flavorShortName);
}
if (strength) {
// 使用强度的shortName如果没有则使用强度名称
const strengthShortName =
attributeMappings.strengths[strength] || strength;
skuComponents.push(strengthShortName);
}
if (humidity) {
// 使用湿度的shortName如果没有则使用湿度名称
const humidityShortName =
attributeMappings.humidities[humidity] || humidity;
skuComponents.push(humidityShortName);
}
if (size) {
// 使用尺寸的shortName如果没有则使用尺寸名称
const sizeShortName = attributeMappings.sizes[size] || size;
skuComponents.push(sizeShortName);
}
// 如果type为single且启用了生成bundle SKU则添加quantity
if (quantity) {
// 使用quantity的shortName如果没有则使用quantity但匹配 4 个零
const quantityShortName =
attributeMappings.quantities[quantity] ||
Number(quantity).toString().padStart(4, '0');
skuComponents.push(quantityShortName);
}
// 合并所有组件,使用短横线分隔
return skuComponents.join('-').toUpperCase();
};
/**
* @description 使
* @param {string} brand -
* @param {string} version -
* @param {string} category -
* @param {string} flavor -
* @param {string} strength -
* @param {string} humidity - 湿
* @param {string} size -
* @param {any} quantity -
* @param {string} type -
* @returns {string}
*/
const generateName = (
brand: string,
version: string,
category: string,
flavor: string,
strength: string,
humidity: string,
size: string,
quantity?: any,
type?: string,
): string => {
// 构建产品名称组件数组
const nameComponents: string[] = [];
// 按顺序添加组件:品牌 -> 版本 -> 品类 -> 风味 -> 毫克数(强度) -> 湿度 -> 型号 -> 数量
if (brand) nameComponents.push(brand);
if (version) nameComponents.push(version);
if (category) nameComponents.push(category);
if (flavor) nameComponents.push(flavor);
if (strength) nameComponents.push(strength);
if (humidity) nameComponents.push(humidity);
if (size) nameComponents.push(size);
// 如果有数量且类型为bundle或者生成bundle的single产品则添加数量
if (type === 'bundle' && quantity) {
nameComponents.push(String(quantity));
}
// 使用空格连接所有组件
return nameComponents.join(' ');
};
/**
* @description siteSkus
* @param {string} baseSku - SKU
* @returns {string} siteSkus
*/
const generateSiteSkus = (baseSku: string): string => {
// 如果没有站点或基础SKU为空返回空字符串
if (selectedSites.length === 0 || !baseSku) return '';
// 为每个站点生成siteSku
const siteSkus = selectedSites.map((site) => {
// 如果站点有shortName则添加前缀否则使用基础SKU
if (site.skuPrefix) {
return `${site.skuPrefix}-${baseSku}`;
}
return baseSku;
});
// 使用分号分隔所有站点的siteSkus
return [baseSku, ...siteSkus].join(';').toUpperCase();
};
/**
* @description 核心逻辑:根据配置处理CSV数据并生成SKU
*/
const handleProcessData = async () => {
if (csvData.length === 0) {
message.warning('请先上传并成功解析一个CSV文件.');
return;
}
if (selectedSites.length === 0) {
message.warning('没有可用的站点.');
return;
}
setIsProcessing(true);
message.loading({ content: '正在生成SKU...', key: 'processing' });
try {
// 获取表单中的最新配置
await form.validateFields();
// 处理每条数据生成SKU和siteSkus
const dataWithSku = csvData.map((row) => {
const brand = row.attribute_brand || '';
const category = row.category || '';
const flavor = row.attribute_flavor || '';
const strength = row.attribute_strength || '';
const humidity = row.attribute_humidity || '';
const version = row.attribute_version || '';
const size = row.attribute_size || row.size || '';
// 将quantity保存到attribute_quantity字段
const quantity = row.attribute_quantity || row.quantity;
// 获取产品类型
const type = row.type || '';
// 生成基础SKU不包含站点前缀
const baseSku = generateSku(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
type,
);
const name = generateName(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
type,
);
// 为所有站点生成带前缀的siteSkus
const siteSkus = generateSiteSkus(baseSku);
// 返回包含新SKU和siteSkus的行数据将SKU直接保存到sku栏
return {
...row,
sku: baseSku, // 直接生成在sku栏
generatedName: name,
// name: name, // 生成的产品名称
siteSkus,
attribute_quantity: quantity, // 确保quantity保存到attribute_quantity
};
});
// Determine which data to use for processing and download
let finalData = dataWithSku;
console.log('generateBundleSkuForSingle', generateBundleSkuForSingle);
// If generateBundleSkuForSingle is enabled, generate bundle products for single products
if (generateBundleSkuForSingle) {
// Filter out single records
const singleRecords = dataWithSku.filter(
(row) => row.type === 'single',
);
// Get quantity values from the config (same source as other attributes like brand)
const quantityValues = config.quantities.map(
(quantity) => quantity.name,
);
// Generate bundle products for each single record and quantity
const generatedBundleRecords = singleRecords.flatMap((singleRecord) => {
return quantityValues.map((quantity) => {
// Extract all necessary attributes from the single record
const brand = singleRecord.attribute_brand || '';
const version = singleRecord.attribute_version || '';
const category = singleRecord.category || '';
const flavor = singleRecord.attribute_flavor || '';
const strength = singleRecord.attribute_strength || '';
const humidity = singleRecord.attribute_humidity || '';
const size = singleRecord.attribute_size || singleRecord.size || '';
// Generate bundle SKU with the quantity
const bundleSku = generateSku(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
'bundle',
);
// Generate bundle name with the quantity
const bundleName = generateName(
brand,
version,
category,
flavor,
strength,
humidity,
size,
quantity,
'bundle',
);
// Generate siteSkus for the bundle
const bundleSiteSkus = generateSiteSkus(bundleSku);
// Create the bundle record
return {
...singleRecord,
type: 'bundle', // Change type to bundle
sku: bundleSku, // Use the new bundle SKU
name: bundleName, // Use the new bundle name
siteSkus: bundleSiteSkus,
attribute_quantity: quantity, // Set the attribute_quantity
component_1_sku: singleRecord.sku, // Set component_1_sku to the single product's sku
component_1_quantity: Number(quantity), // Set component_1_quantity to the same as attribute_quantity
};
});
});
// Combine original dataWithSku with generated bundle records
finalData = [...dataWithSku, ...generatedBundleRecords];
}
// Set the processed data
setProcessedData(finalData);
message.success({
content: 'SKU生成成功!正在自动下载...',
key: 'processing',
});
// 自动下载 the final data (with or without generated bundle products)
downloadData(finalData);
} catch (error) {
message.error({
content: '处理失败,请检查配置或文件.',
key: 'processing',
});
console.error('Processing Error:', error);
} finally {
setIsProcessing(false);
}
};
return (
<PageContainer title="产品SKU批量生成工具">
<Row gutter={[16, 16]}>
{/* 左侧:配置表单 */}
<Col xs={24} md={10}>
<Card title="1. 配置SKU生成规则">
<ProForm
form={form}
initialValues={config}
onFinish={handleProcessData}
submitter={false}
>
<ProFormSelect
name="brands"
label="品牌列表"
mode="tags"
placeholder="请输入品牌,按回车确认"
rules={[{ required: true, message: '至少需要一个品牌' }]}
tooltip="品牌名称会作为SKU的第一个组成部分"
options={config.brands.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="categories"
label="商品分类"
mode="tags"
placeholder="请输入分类,按回车确认"
rules={[{ required: true, message: '至少需要一个分类' }]}
tooltip="分类名称会作为SKU的第二个组成部分"
options={config.categories.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="flavors"
label="口味列表"
mode="tags"
placeholder="请输入口味,按回车确认"
rules={[{ required: true, message: '至少需要一个口味' }]}
tooltip="口味名称会作为SKU的第三个组成部分"
options={config.flavors.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="strengths"
label="强度列表"
mode="tags"
placeholder="请输入强度,按回车确认"
tooltip="强度信息会作为SKU的第四个组成部分"
options={config.strengths.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="humidities"
label="湿度列表"
mode="tags"
placeholder="请输入湿度,按回车确认"
tooltip="湿度信息会作为SKU的第五个组成部分"
options={config.humidities.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="versions"
label="版本列表"
mode="tags"
placeholder="请输入版本,按回车确认"
tooltip="版本信息会作为SKU的第六个组成部分"
options={config.versions.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="sizes"
label="尺寸列表"
mode="tags"
placeholder="请输入尺寸,按回车确认"
tooltip="尺寸信息会作为SKU的第七个组成部分"
options={config.sizes.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
/>
<ProFormSelect
name="quantities"
label="数量列表"
mode="tags"
placeholder="请输入数量,按回车确认"
tooltip="数量信息会作为bundle SKU的组成部分"
options={config.quantities.map((opt) => ({
label: `${opt.name} (${opt.shortName})`,
value: opt.name,
}))}
fieldProps={{ allowClear: true }}
/>
<ProForm.Item
name="generateBundleSkuForSingle"
label="为type=single生成bundle产品数据行"
tooltip="为类型为single的记录生成包含quantity的bundle SKU"
valuePropName="checked"
initialValue={true}
>
<Checkbox
onChange={(e) =>
setGenerateBundleSkuForSingle(e.target.checked)
}
>
single类型生成bundle SKU
</Checkbox>
</ProForm.Item>
</ProForm>
</Card>
{/* 显示所有站点及其shortname */}
<Card title="3. 所有站点信息" style={{ marginTop: '16px' }}>
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{sites.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#fafafa' }}>
<th
style={{
padding: '8px',
textAlign: 'left',
borderBottom: '1px solid #e8e8e8',
}}
>
</th>
<th
style={{
padding: '8px',
textAlign: 'left',
borderBottom: '1px solid #e8e8e8',
}}
>
ShortName
</th>
</tr>
</thead>
<tbody>
{sites.map((site) => (
<tr key={site.id}>
<td
style={{
padding: '8px',
borderBottom: '1px solid #e8e8e8',
}}
>
{site.name}
</td>
<td
style={{
padding: '8px',
borderBottom: '1px solid #e8e8e8',
fontWeight: 'bold',
}}
>
{site.skuPrefix}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p style={{ textAlign: 'center', color: '#999' }}>
</p>
)}
</div>
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
<p>
shortName将作为前缀添加到生成的SKU中
</p>
</div>
</Card>
</Col>
{/* 右侧:文件上传与操作 */}
<Col xs={24} md={14}>
<Card title="2. 上传文件并操作">
<Upload
beforeUpload={handleFileUpload}
maxCount={1}
showUploadList={!!file}
onRemove={() => {
setFile(null);
setCsvData([]);
setProcessedData([]);
}}
>
<Button icon={<UploadOutlined />}> CSV </Button>
</Upload>
<div style={{ marginTop: 16 }}>
<label style={{ display: 'block', marginBottom: 8 }}>
</label>
<Input value={file ? file.name : '暂未选择文件'} readOnly />
</div>
<Button
type="primary"
onClick={handleProcessData}
disabled={
csvData.length === 0 ||
isProcessing ||
selectedSites.length === 0
}
loading={isProcessing}
style={{ marginTop: '20px' }}
>
SKU
</Button>
{/* 显示处理结果摘要 */}
{processedData.length > 0 && (
<div
style={{
marginTop: '20px',
padding: '10px',
backgroundColor: '#f0f9eb',
borderRadius: '4px',
}}
>
<p style={{ margin: 0, color: '#52c41a' }}>
{processedData.length} SKU
</p>
</div>
)}
</Card>
</Col>
</Row>
</PageContainer>
);
};
export default CsvTool;

View File

@ -1,368 +0,0 @@
import React, { useEffect, useState, useMemo } from 'react';
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
import { Card, Collapse, Divider, Image, Select, Space, Typography, message } from 'antd';
import { categorycontrollerGetall } from '@/servers/api/category';
import { productcontrollerGetproductlistgrouped } from '@/servers/api/product';
import { dictcontrollerGetdictitems } from '@/servers/api/dict';
// Define interfaces
interface Category {
id: number;
name: string;
title: string;
attributes: string[]; // List of attribute names for this category
}
interface Attribute {
id: number;
name: string;
title: string;
}
interface AttributeValue {
id: number;
name: string;
title: string;
titleCN?: string;
value?: string;
image?: string;
}
interface Product {
id: number;
sku: string;
name: string;
image?: string;
brandId: number;
brandName: string;
attributes: { [key: string]: number }; // attribute name to attribute value id mapping
price?: number;
}
// Grouped products by attribute value
interface GroupedProducts {
[attributeValueId: string]: Product[];
}
// ProductCard component for displaying single product
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
return (
<Card hoverable style={{ width: 240 }}>
{/* <div style={{ height: 180, overflow: 'hidden', marginBottom: '12px' }}>
<Image
src={product.image || 'https://via.placeholder.com/240x180?text=No+Image'}
alt={product.name}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div> */}
<div>
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: '4px' }}>
{product.sku}
</Typography.Text>
<Typography.Text ellipsis style={{ width: '100%', display: 'block', marginBottom: '8px' }}>
{product.name}
</Typography.Text>
<Typography.Text strong style={{ fontSize: 16, color: '#ff4d4f', display: 'block' }}>
¥{product.price || '--'}
</Typography.Text>
</div>
</Card>
);
};
// ProductGroup component for displaying grouped products
const ProductGroup: React.FC<{
attributeValueId: string;
groupProducts: Product[];
attributeValue: AttributeValue | undefined;
attributeName: string;
}> = ({ attributeValueId, groupProducts, attributeValue }) => {
// State for collapse control
const [isCollapsed, setIsCollapsed] = useState(false);
// Create collapse panel header
const panelHeader = (
<Space>
{attributeValue?.image && (
<Image
src={attributeValue.image}
style={{ width: 24, height: 24, objectFit: 'cover', borderRadius: 4 }}
/>
)}
<Typography.Title level={5} style={{ margin: 0 }}>
<span>
{attributeValue?.titleCN || attributeValue?.title || attributeValue?.name || attributeValueId||'未知'}
( {groupProducts.length} )
</span>
</Typography.Title>
</Space>
);
return (
<Collapse
activeKey={isCollapsed ? [] : [attributeValueId]}
onChange={(key) => setIsCollapsed(Array.isArray(key) && key.length === 0)}
ghost
bordered={false}
items={[
{
key: attributeValueId,
label: panelHeader,
children: (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', paddingTop: '8px' }}>
{groupProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
),
},
]}
/>
);
};
// Main ProductGroupBy component
const ProductGroupBy: React.FC = () => {
// State management
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
// Store selected values for each attribute
const [attributeFilters, setAttributeFilters] = useState<{ [key: string]: number | null }>({});
// Group by attribute
const [groupByAttribute, setGroupByAttribute] = useState<string | null>(null);
// Products
const [products, setProducts] = useState<Product[]>([]);
const [groupedProducts, setGroupedProducts] = useState<GroupedProducts>({});
const [loading, setLoading] = useState(false);
// Extract all unique attributes from categories
const categoryAttributes = useMemo(() => {
if (!selectedCategory) return [];
const categoryItem = categories.find((category: any) => category.name === selectedCategory);
if (!categoryItem) return [];
const attributesList: Attribute[] = categoryItem.attributes.map((attribute: any, index) => ({
...attribute.attributeDict,
id: index + 1,
}));
return attributesList;
}, [selectedCategory]);
// Fetch categories list
const fetchCategories = async () => {
try {
const response = await categorycontrollerGetall();
const rawCategories = Array.isArray(response) ? response : response?.data || [];
setCategories(rawCategories);
// Set default category
if (rawCategories.length > 0) {
const defaultCategory = rawCategories.find((category: any) => category.name === 'nicotine-pouches');
setSelectedCategory(defaultCategory?.name || rawCategories[0].name);
}
} catch (error) {
console.error('Failed to fetch categories:', error);
message.error('获取分类列表失败');
}
};
// Update category attributes when selected category changes
useEffect(() => {
if (!selectedCategory) return;
const category = categories.find(cat => cat.name === selectedCategory);
if (!category) return;
// Get attributes for this category
const attributesForCategory = categoryAttributes.filter(attr =>
attr.name === 'brand' || category.attributes.includes(attr.name)
);
// Reset attribute filters when category changes
const newFilters: { [key: string]: number | null } = {};
attributesForCategory.forEach(attr => {
newFilters[attr.name] = null;
});
setAttributeFilters(newFilters);
// Set default group by attribute
if (attributesForCategory.length > 0) {
setGroupByAttribute(attributesForCategory[0].name);
}
}, [selectedCategory, categories, categoryAttributes]);
// Handle attribute filter change
const handleAttributeFilterChange = (attributeName: string, value: number | null) => {
setAttributeFilters(prev => ({ ...prev, [attributeName]: value }));
};
// Fetch products based on filters
const fetchProducts = async () => {
if (!selectedCategory || !groupByAttribute) return;
setLoading(true);
try {
const params: any = {
category: selectedCategory,
groupBy: groupByAttribute
};
const response = await productcontrollerGetproductlistgrouped(params);
const grouped = response?.data || {};
setGroupedProducts(grouped);
// Flatten grouped products to get all products
const allProducts = Object.values(grouped).flat() as Product[];
setProducts(allProducts);
} catch (error) {
console.error('Failed to fetch grouped products:', error);
message.error('获取分组产品列表失败');
setProducts([]);
setGroupedProducts({});
} finally {
setLoading(false);
}
};
// Initial data fetch
useEffect(() => {
fetchCategories();
}, []);
// Fetch products when filters change
useEffect(() => {
fetchProducts();
}, [selectedCategory, attributeFilters, groupByAttribute]);
// Destructure antd components
const { Title, Text } = Typography;
return (
<PageContainer title="品牌空间">
<div style={{ padding: '16px', background: '#fff' }}>
{/* Filter Section */}
<div style={{ marginBottom: '24px' }}>
<Title level={4} style={{ marginBottom: '16px' }}></Title>
<Space direction="vertical" size="large">
{/* Category Filter */}
<div>
<Text strong></Text>
<Select
placeholder="请选择分类"
style={{ width: 300, marginLeft: '8px' }}
value={selectedCategory}
onChange={setSelectedCategory}
allowClear
showSearch
optionFilterProp="children"
>
{categories.map(category => (
<Option key={category.id} value={category.name}>
{category.title}
</Option>
))}
</Select>
</div>
{/* Attribute Filters */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Space direction="vertical" style={{ marginTop: '8px', width: '100%' }}>
{categoryAttributes.map(attr => (
<div key={attr.id} style={{ display: 'flex', alignItems: 'center' }}>
<Text style={{ width: '100px' }}>{attr.title}</Text>
<ProFormSelect
placeholder={`请选择${attr.title}`}
style={{ width: 300 }}
value={attributeFilters[attr.name] || null}
onChange={value => handleAttributeFilterChange(attr.name, value)}
allowClear
showSearch
optionFilterProp="children"
request={async (params) => {
try {
console.log('params', params,attr);
const response = await dictcontrollerGetdictitems({ dictId: attr.name });
const rawValues = Array.isArray(response) ? response : response?.data?.items || [];
const filteredValues = rawValues.filter((value: any) =>
value.dictId === attr.name || value.dict?.id === attr.name || value.dict?.name === attr.name
);
return {
options: filteredValues.map((value: any) => ({
label: `${value.name}${value.titleCN || value.title}`,
value: value.id
}))
};
} catch (error) {
console.error(`Failed to fetch ${attr.title} values:`, error);
message.error(`获取${attr.title}属性值失败`);
return { options: [] };
}
}}
/>
</div>
))}
</Space>
</div>
)}
{/* Group By Attribute */}
{categoryAttributes.length > 0 && (
<div>
<Text strong></Text>
<Select
placeholder="请选择分组属性"
style={{ width: 300, marginLeft: '8px' }}
value={groupByAttribute}
onChange={setGroupByAttribute}
showSearch
optionFilterProp="children"
>
{categoryAttributes.map(attr => (
<Option key={attr.id} value={attr.name}>
{attr.title}
</Option>
))}
</Select>
</div>
)}
</Space>
</div>
<Divider />
{/* Products Section */}
<div>
<Title level={4} style={{ marginBottom: '16px' }}> ({products.length} )</Title>
{loading ? (
<div style={{ textAlign: 'center', padding: '64px' }}>
<Text>...</Text>
</div>
) : groupByAttribute && Object.keys(groupedProducts).length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{Object.entries(groupedProducts).map(([attrValueId, groupProducts]) => {
return (
<ProductGroup
key={attrValueId}
attributeValueId={attrValueId}
groupProducts={groupProducts}
// attributeValue={}
attributeName={groupByAttribute!}
/>
);
})}
</div>
) : (
<div style={{ textAlign: 'center', padding: '64px', background: '#fafafa', borderRadius: 8 }}>
<Text type="secondary"></Text>
</div>
)}
</div>
</div>
</PageContainer>
);
};
export default ProductGroupBy;

View File

@ -3,7 +3,6 @@ import {
productcontrollerCreateproduct,
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerGetproductlist,
} from '@/servers/api/product';
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
import { templatecontrollerRendertemplate } from '@/servers/api/template';
@ -77,7 +76,7 @@ const CreateForm: React.FC<{
const strengthName: string = String(strengthValues?.[0] || '');
const flavorName: string = String(flavorValues?.[0] || '');
const humidityName: string = String(humidityValues?.[0] || '');
console.log(formValues);
// 调用模板渲染API来生成SKU
const {
data: rendered,
@ -86,25 +85,10 @@ const CreateForm: React.FC<{
} = await templatecontrollerRendertemplate(
{ name: 'product.sku' },
{
category: formValues.category,
attributes: [
{
dict: { name: 'brand' },
shortName: brandName || '',
},
{
dict: { name: 'flavor' },
shortName: flavorName || '',
},
{
dict: { name: 'strength' },
shortName: strengthName || '',
},
{
dict: { name: 'humidity' },
shortName: humidityName ? capitalize(humidityName) : '',
},
],
brand: brandName || '',
strength: strengthName || '',
flavor: flavorName || '',
humidity: humidityName ? capitalize(humidityName) : '',
},
);
if (!success) {
@ -216,24 +200,19 @@ const CreateForm: React.FC<{
}
}}
onFinish={async (values: any) => {
// 根据产品类型决定是否组装 attributes
// 如果产品类型为 bundle则 attributes 为空数组
// 如果产品类型为 single则根据 activeAttributes 动态组装 attributes
const attributes =
values.type === 'bundle'
? []
: activeAttributes.flatMap((attr: any) => {
const dictName = attr.name;
const key = `${dictName}Values`;
const vals = values[key];
if (vals && Array.isArray(vals)) {
return vals.map((v: string) => ({
dictName: dictName,
name: v,
}));
}
return [];
});
// 组装 attributes(根据 activeAttributes 动态组装)
const attributes = activeAttributes.flatMap((attr: any) => {
const dictName = attr.name;
const key = `${dictName}Values`;
const vals = values[key];
if (vals && Array.isArray(vals)) {
return vals.map((v: string) => ({
dictName: dictName,
name: v,
}));
}
return [];
});
const payload: any = {
name: (values as any).name,
@ -267,7 +246,13 @@ const CreateForm: React.FC<{
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
<ProFormSelect
name="siteSkus"
label="站点 SKU 列表"
width="md"
mode="tags"
placeholder="输入站点 SKU,回车添加"
/>
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button>
@ -280,14 +265,6 @@ const CreateForm: React.FC<{
</Tag>
)}
</ProForm.Group>
<ProFormSelect
name="siteSkus"
initialValue={[]}
label="站点 SKU 列表"
width="md"
mode="tags"
placeholder="输入站点 SKU,回车添加"
/>
<ProForm.Group>
<ProFormText
name="name"
@ -336,18 +313,15 @@ const CreateForm: React.FC<{
rules={[{ required: true, message: '请输入子产品SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords
? { sku: keyWords, name: keyWords, type: 'single' }
: { pageSize: 9999, type: 'single' };
const { data } = await productcontrollerGetproductlist(
params as any,
);
? { sku: keyWords, name: keyWords }
: { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}
// 只返回类型为单品的产品
return data.items
.filter((item: any) => item.type === 'single' && item.sku)
.map((item: any) => ({
.filter((item) => item.sku)
.map((item) => ({
label: `${item.sku} - ${item.name}`,
value: item.sku,
}));

View File

@ -3,7 +3,7 @@ import {
productcontrollerGetcategoriesall,
productcontrollerGetcategoryattributes,
productcontrollerGetproductcomponents,
productcontrollerGetproductlist,
productcontrollerGetproductsiteskus,
productcontrollerUpdateproduct,
} from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
@ -35,6 +35,7 @@ const EditForm: React.FC<{
const [stockStatus, setStockStatus] = useState<
'in-stock' | 'out-of-stock' | null
>(null);
const [siteSkuCodes, setSiteSkuCodes] = useState<string[]>([]);
const [sites, setSites] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
@ -98,6 +99,15 @@ const EditForm: React.FC<{
const { data: componentsData } =
await productcontrollerGetproductcomponents({ id: record.id });
setComponents(componentsData || []);
// 获取站点SKU详细信息
const { data: siteSkusData } = await productcontrollerGetproductsiteskus({
id: record.id,
});
// 只提取code字段组成字符串数组
const codes = siteSkusData
? siteSkusData.map((item: any) => item.code)
: [];
setSiteSkuCodes(codes);
})();
}, [record]);
@ -119,10 +129,10 @@ const EditForm: React.FC<{
type: type,
categoryId: (record as any).categoryId || (record as any).category?.id,
// 初始化站点SKU为字符串数组
// 修改后代码:
siteSkus: (record.siteSkus || []).map((code) => ({ code })),
siteSkus: siteSkuCodes,
};
}, [record, components, type]);
}, [record, components, type, siteSkuCodes]);
return (
<DrawerForm<any>
title="编辑"
@ -187,7 +197,7 @@ const EditForm: React.FC<{
attributes,
type: values.type, // 直接使用 type
categoryId: values.categoryId,
siteSkus: values.siteSkus.map((v: { code: string }) => v.code) || [], // 直接传递字符串数组
siteSkus: values.siteSkus || [], // 直接传递字符串数组
// 连带更新 components
components:
values.type === 'bundle'
@ -210,8 +220,6 @@ const EditForm: React.FC<{
return false;
}}
>
{/* {JSON.stringify(record)}
{JSON.stringify(initialValues)} */}
<ProForm.Group>
<ProFormText
name="sku"
@ -307,25 +315,17 @@ const EditForm: React.FC<{
<ProForm.Group>
<ProFormSelect
name="sku"
label="单品SKU"
label="库存SKU"
width="md"
showSearch
debounceTime={300}
placeholder="请输入单品SKU"
rules={[{ required: true, message: '请输入单品SKU' }]}
placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords
? {
where: {
sku: keyWords,
name: keyWords,
type: 'single',
},
}
: { per_page: 9999, where: { type: 'single' } };
const { data } = await productcontrollerGetproductlist(
params,
);
? { sku: keyWords, name: keyWords }
: { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}

View File

@ -1,204 +0,0 @@
import { productcontrollerBatchsynctosite } from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { showBatchOperationResult } from '@/utils/showResult';
import {
ModalForm,
ProFormDependency,
ProFormSelect,
ProFormText,
} from '@ant-design/pro-components';
import { App, Button, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
interface SyncToSiteModalProps {
visible: boolean;
onClose: () => void;
products: API.Product[];
site?: any;
onSuccess: () => void;
}
const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
visible,
onClose,
products,
site,
onSuccess,
}) => {
const { message } = App.useApp();
const [sites, setSites] = useState<any[]>([]);
const formRef = useRef<any>();
// 生成单个产品的站点SKU
const generateSingleSiteSku = async (
currentSite: API.Site,
product: API.Product,
): Promise<string> => {
try {
console.log('site', currentSite);
const { data: renderedSku } = await templatecontrollerRendertemplate(
{ name: 'site.product.sku' },
{ site: currentSite, product },
);
return (
renderedSku || `${currentSite.skuPrefix || ''}${product.sku || ''}`
);
} catch (error) {
return `${currentSite.skuPrefix || ''}${product.sku || ''}`;
}
};
// 生成所有产品的站点SKU并设置到表单
const generateAndSetSiteSkus = async (currentSite: any) => {
const siteSkus: Record<string, string> = {};
for (const product of products) {
const siteSku = await generateSingleSiteSku(currentSite, product);
siteSkus[product.id] = siteSku;
}
// 设置表单值
formRef.current?.setFieldsValue({ siteSkus });
};
useEffect(() => {
if (visible) {
sitecontrollerAll().then((res: any) => {
const siteList = res?.data || [];
setSites(siteList);
// 如果有站点列表默认选择第一个站点或传入的site
const targetSite = site || (siteList.length > 0 ? siteList[0] : null);
if (targetSite) {
// 使用 setTimeout 确保 formRef 已经准备好
setTimeout(() => {
if (formRef.current) {
formRef.current.setFieldsValue({ siteId: targetSite.id });
// 自动生成所有产品的站点 SKU
generateAndSetSiteSkus(targetSite);
}
}, 0);
}
});
}
}, [visible, products, site]);
return (
<ModalForm
title={`同步到站点 (${products.length} 项)`}
open={visible}
onOpenChange={(open) => !open && onClose()}
modalProps={{ destroyOnClose: true }}
formRef={formRef}
onValuesChange={async (changedValues) => {
if ('siteId' in changedValues && changedValues.siteId) {
const siteId = changedValues.siteId;
const currentSite = sites.find((s: any) => s.id === siteId) || {};
// 站点改变时重新生成所有产品的站点SKU
generateAndSetSiteSkus(currentSite);
}
}}
onFinish={async (values) => {
console.log(`values`, values);
if (!values.siteId) return false;
try {
const siteSkusMap = values.siteSkus || {};
const data = products.map((product) => ({
productId: product.id,
siteSku:
siteSkusMap[product.id] || `${values.siteId}-${product.sku}`,
}));
console.log(`data`, data);
const result = await productcontrollerBatchsynctosite({
siteId: values.siteId,
data,
});
showBatchOperationResult(result, '同步到站点');
onSuccess();
return true;
} catch (error: any) {
message.error(error.message || '同步失败');
return false;
}
}}
>
<ProFormSelect
name="siteId"
label="选择站点"
options={sites.map((site) => ({ label: site.name, value: site.id }))}
rules={[{ required: true, message: '请选择站点' }]}
/>
{products.map((row) => (
<ProFormDependency key={row.id} name={['siteId']}>
{({ siteId }) => (
<div style={{ marginBottom: 16 }}>
<div
style={{
display: 'flex',
gap: 8,
alignItems: 'center',
marginBottom: 8,
}}
>
<div style={{ minWidth: 220 }}>SKU: {row.sku || '-'}</div>
<div style={{ minWidth: 150 }}>
SKU:{' '}
{row.siteSkus && row.siteSkus.length > 0
? row.siteSkus.map((siteSku: string, idx: number) => (
<Tag key={idx} color="cyan">
{siteSku}
</Tag>
))
: '-'}
</div>
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<ProFormText
name={['siteSkus', row.id]}
label={`商品 ${row.sku} 站点SKU`}
placeholder="请输入站点SKU"
fieldProps={{
onChange: (e) => {
// 手动输入时更新表单值
const currentValues =
formRef.current?.getFieldValue('siteSkus') || {};
currentValues[row.id] = e.target.value;
formRef.current?.setFieldsValue({
siteSkus: currentValues,
});
},
}}
/>
</div>
<Button
type="primary"
size="small"
onClick={async () => {
if (siteId) {
const currentSite =
sites.find((s: any) => s.id === siteId) || {};
const siteSku = await generateSingleSiteSku(
currentSite,
row,
);
const currentValues =
formRef.current?.getFieldValue('siteSkus') || {};
currentValues[row.id] = siteSku;
formRef.current?.setFieldsValue({
siteSkus: currentValues,
});
}
}}
>
</Button>
</div>
</div>
)}
</ProFormDependency>
))}
</ModalForm>
);
};
export default SyncToSiteModal;

View File

@ -1,53 +0,0 @@
import React from "react";
import { ProTable, ProColumns } from "@ant-design/pro-components";
interface ProductComponentListProps {
record: API.Product;
columns: ProColumns<API.Product>[];
dataSource?: API.Product[];
}
const ProductComponentList: React.FC<ProductComponentListProps> = ({ record, columns, dataSource }) => {
if (record.type !== "bundle" || !record.components || record.components.length === 0) {
return null;
}
const componentSkus = record.components.map(component => component.sku);
const includedProducts = [];
if (dataSource) {
includedProducts = dataSource
.filter(product => product.type === "single" && componentSkus.includes(product.sku));
}
if (includedProducts.length === 0) {
return (
<div style={{ padding: "16px", textAlign: "center", color: "#999" }}>
</div>
);
}
const componentColumns = columns.filter(col =>
[200~cd ../api"option", "siteSkus", "category", "type"].includes(col.dataIndex as string)
);
return (
<div style={{ padding: "8px 16px", backgroundColor: "#fafafa" }}>
<ProTable
dataSource={includedProducts}
columns={componentColumns}
pagination={false}
rowKey="id"
bordered
size="small"
scroll={{ x: "max-content" }}
headerTitle={null}
toolBarRender={false}
/>
</div>
);
};
export default ProductComponentList;

View File

@ -1,11 +1,19 @@
import {
productcontrollerBatchdeleteproduct,
productcontrollerBatchupdateproduct,
productcontrollerBindproductsiteskus,
productcontrollerDeleteproduct,
productcontrollerGetcategoriesall,
productcontrollerGetproductcomponents,
productcontrollerGetproductlist,
productcontrollerUpdatenamecn,
} from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
import {
wpproductcontrollerBatchsynctosite,
wpproductcontrollerSynctoproduct,
} from '@/servers/api/wpProduct';
import {
ActionType,
ModalForm,
@ -20,7 +28,6 @@ import { App, Button, Modal, Popconfirm, Tag, Upload } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import CreateForm from './CreateForm';
import EditForm from './EditForm';
import SyncToSiteModal from './SyncToSiteModal';
const NameCn: React.FC<{
id: number;
@ -66,7 +73,16 @@ const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
);
};
const ComponentsCell: React.FC<{ components?: any[] }> = ({ components }) => {
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
const [components, setComponents] = React.useState<any[]>([]);
React.useEffect(() => {
(async () => {
const { data = [] } = await productcontrollerGetproductcomponents({
id: productId,
});
setComponents(data || []);
})();
}, [productId]);
return (
<div>
{components && components.length ? (
@ -153,30 +169,251 @@ const BatchEditModal: React.FC<{
</ModalForm>
);
};
const ProductList = ({
filter,
columns,
}: {
filter: { skus: string[] };
columns: any[];
}) => {
const SyncToSiteModal: React.FC<{
visible: boolean;
onClose: () => void;
productIds: number[];
productRows: API.Product[];
onSuccess: () => void;
}> = ({ visible, onClose, productIds, productRows, onSuccess }) => {
const { message } = App.useApp();
const [sites, setSites] = useState<any[]>([]);
const formRef = useRef<any>();
useEffect(() => {
if (visible) {
sitecontrollerAll().then((res: any) => {
setSites(res?.data || []);
});
}
}, [visible]);
return (
<ModalForm
title={`同步到站点 (${productIds.length} 项)`}
open={visible}
onOpenChange={(open) => !open && onClose()}
modalProps={{ destroyOnClose: true }}
formRef={formRef}
onValuesChange={(changedValues) => {
if ('siteId' in changedValues && changedValues.siteId) {
const siteId = changedValues.siteId;
const site = sites.find((s: any) => s.id === siteId) || {};
const prefix = site.skuPrefix || '';
const map: Record<string, any> = {};
productRows.forEach((p) => {
map[p.id] = {
code: `${prefix}${p.sku || ''}`,
quantity: undefined,
};
});
formRef.current?.setFieldsValue({ productSiteSkus: map });
}
}}
onFinish={async (values) => {
if (!values.siteId) return false;
try {
await wpproductcontrollerBatchsynctosite(
{ siteId: values.siteId },
{ productIds },
);
const map = values.productSiteSkus || {};
for (const currentProductId of productIds) {
const entry = map?.[currentProductId];
if (entry && entry.code) {
await productcontrollerBindproductsiteskus(
{ id: currentProductId },
{
siteSkus: [
{
siteId: values.siteId,
code: entry.code,
quantity: entry.quantity,
},
],
},
);
}
}
message.success('同步任务已提交');
onSuccess();
return true;
} catch (error: any) {
message.error(error.message || '同步失败');
return false;
}
}}
>
<ProFormSelect
name="siteId"
label="选择站点"
options={sites.map((site) => ({ label: site.name, value: site.id }))}
rules={[{ required: true, message: '请选择站点' }]}
/>
{productRows.map((row) => (
<div
key={row.id}
style={{ display: 'flex', gap: 12, alignItems: 'flex-end' }}
>
<div style={{ minWidth: 220 }}>SKU: {row.sku || '-'}</div>
<ProFormText
name={['productSiteSkus', row.id, 'code']}
label={`商品 ${row.id} 站点SKU`}
placeholder="请输入站点SKU"
/>
<ProFormText
name={['productSiteSkus', row.id, 'quantity']}
label="数量"
placeholder="请输入数量"
/>
</div>
))}
</ModalForm>
);
};
const WpProductInfo: React.FC<{
skus: string[];
record: API.Product;
parentTableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ skus, record, parentTableRef }) => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
return (
<ProTable
request={async (pag) => {
const { data, success } = await productcontrollerGetproductlist({
where: filter,
});
if (!success) return [];
return data || [];
}}
columns={columns}
headerTitle="站点产品信息"
actionRef={actionRef}
search={false}
options={false}
pagination={false}
rowKey="id"
bordered
size="small"
scroll={{ x: 'max-content' }}
headerTitle={null}
toolBarRender={false}
toolBarRender={() => [
<Button
key="refresh"
type="primary"
onClick={() => actionRef.current?.reload()}
>
</Button>,
]}
request={async () => {
// 判断是否存在站点SKU列表
if (!skus || skus.length === 0) return { data: [] };
try {
// 获取所有站点列表用于遍历查询
const { data: siteResponse } = await sitecontrollerAll();
const siteList = siteResponse || [];
// 聚合所有站点的产品数据
const aggregatedProducts: any[] = [];
// 遍历每一个站点
for (const siteItem of siteList) {
// 遍历每一个SKU在当前站点进行搜索
for (const skuCode of skus) {
// 直接调用站点API根据搜索关键字获取产品列表
const response = await siteapicontrollerGetproducts({
siteId: Number(siteItem.id),
per_page: 100,
search: skuCode,
});
const productPage = response as any;
const siteProducts = productPage?.data?.items || [];
// 将站点信息附加到产品数据中便于展示
siteProducts.forEach((p: any) => {
aggregatedProducts.push({
...p,
siteId: siteItem.id,
siteName: siteItem.name,
});
});
}
}
return { data: aggregatedProducts, success: true };
} catch (error: any) {
// 请求失败进行错误提示
message.error(error?.message || '获取站点产品失败');
return { data: [], success: false };
}
}}
columns={[
{
title: '站点',
dataIndex: 'siteName',
},
{
title: 'SKU',
dataIndex: 'sku',
},
{
title: '价格',
dataIndex: 'regular_price',
render: (_, row) => (
<div>
<div>: {row.regular_price}</div>
<div>: {row.sale_price}</div>
</div>
),
},
{
title: '状态',
dataIndex: 'status',
},
{
title: '操作',
valueType: 'option',
render: (_, wpRow) => [
<a
key="syncToSite"
onClick={async () => {
try {
await wpproductcontrollerBatchsynctosite(
{ siteId: wpRow.siteId },
{ productIds: [record.id] },
);
message.success('同步到站点成功');
actionRef.current?.reload();
} catch (e: any) {
message.error(e.message || '同步失败');
}
}}
>
</a>,
<a
key="syncToProduct"
onClick={async () => {
try {
await wpproductcontrollerSynctoproduct({ id: wpRow.id });
message.success('同步进商品成功');
parentTableRef.current?.reload();
} catch (e: any) {
message.error(e.message || '同步失败');
}
}}
>
</a>,
<Popconfirm
key="delete"
title="删除"
description="确认删除?"
onConfirm={async () => {
try {
await request(`/wp_product/${wpRow.id}`, {
method: 'DELETE',
});
message.success('删除成功');
actionRef.current?.reload();
} catch (e: any) {
message.error(e.message || '删除失败');
}
}}
>
<a style={{ color: 'red' }}></a>
</Popconfirm>,
],
},
]}
/>
);
};
@ -186,8 +423,8 @@ const List: React.FC = () => {
// 状态:存储当前选中的行
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
const [syncProducts, setSyncProducts] = useState<API.Product[]>([]);
const [syncModalVisible, setSyncModalVisible] = useState(false);
const [syncProductIds, setSyncProductIds] = useState<number[]>([]);
const { message } = App.useApp();
// 导出产品 CSV(带认证请求)
@ -221,25 +458,18 @@ const List: React.FC = () => {
sorter: true,
},
{
title: '关联商品',
title: '商品SKU',
dataIndex: 'siteSkus',
width: 200,
render: (_, record) => (
<>
{record.siteSkus?.map((siteSku, index) => (
{record.siteSkus?.map((code, index) => (
<Tag key={index} color="cyan">
{siteSku}
{code}
</Tag>
))}
</>
),
},
{
title: '图片',
dataIndex: 'image',
width: 100,
valueType: 'image',
},
{
title: '名称',
dataIndex: 'name',
@ -255,6 +485,13 @@ const List: React.FC = () => {
},
},
{
title: '商品类型',
dataIndex: 'category',
render: (_, record: any) => {
return record.category?.title || record.category?.name || '-';
},
},
{
title: '价格',
dataIndex: 'price',
@ -267,13 +504,6 @@ const List: React.FC = () => {
hideInSearch: true,
sorter: true,
},
{
title: '商品类型',
dataIndex: 'category',
render: (_, record: any) => {
return record.category?.title || record.category?.name || '-';
},
},
{
title: '属性',
dataIndex: 'attributes',
@ -305,7 +535,7 @@ const List: React.FC = () => {
title: '构成',
dataIndex: 'components',
hideInSearch: true,
render: (_, record) => <ComponentsCell components={record.components} />,
render: (_, record) => <ComponentsCell productId={(record as any).id} />,
},
{
@ -338,7 +568,7 @@ const List: React.FC = () => {
<Button
type="link"
onClick={() => {
setSyncProducts([record]);
setSyncProductIds([record.id]);
setSyncModalVisible(true);
}}
>
@ -379,6 +609,55 @@ const List: React.FC = () => {
toolBarRender={() => [
// 新建按钮
<CreateForm tableRef={actionRef} />,
// 批量编辑按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => setBatchEditModalVisible(true)}
>
</Button>,
// 批量同步按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => {
setSyncProductIds(selectedRows.map((row) => row.id));
setSyncModalVisible(true);
}}
>
</Button>,
// 批量删除按钮
<Button
danger
disabled={selectedRows.length <= 0}
onClick={() => {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
onOk: async () => {
try {
const { success, message: errMsg } =
await productcontrollerBatchdeleteproduct({
ids: selectedRows.map((row) => row.id),
});
if (success) {
message.success('批量删除成功');
setSelectedRows([]);
actionRef.current?.reload();
} else {
message.error(errMsg || '删除失败');
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
},
});
}}
>
</Button>,
// 导出 CSV(后端返回 text/csv,直接新窗口下载)
<Button onClick={handleDownloadProductsCSV}>CSV</Button>,
// 导入 CSV(使用 customRequest 以支持 request 拦截器和鉴权)
<Upload
name="file"
@ -454,57 +733,8 @@ const List: React.FC = () => {
}
}}
>
<Button></Button>
<Button>CSV</Button>
</Upload>,
// 批量编辑按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => setBatchEditModalVisible(true)}
>
</Button>,
// 批量同步按钮
<Button
disabled={selectedRows.length <= 0}
onClick={() => {
setSyncProducts(selectedRows);
setSyncModalVisible(true);
}}
>
</Button>,
// 批量删除按钮
<Button
danger
disabled={selectedRows.length <= 0}
onClick={() => {
Modal.confirm({
title: '确认删除',
content: `确定要删除选中的 ${selectedRows.length} 个产品吗?此操作不可恢复。`,
onOk: async () => {
try {
const { success, message: errMsg } =
await productcontrollerBatchdeleteproduct({
ids: selectedRows.map((row) => row.id),
});
if (success) {
message.success('批量删除成功');
setSelectedRows([]);
actionRef.current?.reload();
} else {
message.error(errMsg || '删除失败');
}
} catch (error: any) {
message.error(error.message || '删除失败');
}
},
});
}}
>
</Button>,
// 导出 CSV(后端返回 text/csv,直接新窗口下载)
<Button onClick={handleDownloadProductsCSV}>CSV</Button>,
]}
request={async (params, sort) => {
let sortField = undefined;
@ -515,12 +745,9 @@ const List: React.FC = () => {
sortField = field;
sortOrder = sort[field];
}
const { current, pageSize, ...where } = params;
console.log(`params`, params);
const { data, success } = await productcontrollerGetproductlist({
where,
page: current || 1,
per_page: pageSize || 10,
...params,
sortField,
sortOrder,
} as any);
@ -531,18 +758,17 @@ const List: React.FC = () => {
};
}}
columns={columns}
// expandable={{
// expandedRowRender: (record) => {
// return <ProductList filter={{
// skus: record.components?.map(component => component.sku) || [],
// }}
// columns={columns}
// ></ProductList>
// }
// ,
// rowExpandable: (record) =>
// !!(record.type==='bundle'),
// }}
expandable={{
expandedRowRender: (record) => (
<WpProductInfo
skus={(record.siteSkus as string[]) || []}
record={record}
parentTableRef={actionRef}
/>
),
rowExpandable: (record) =>
!!(record.siteSkus && record.siteSkus.length > 0),
}}
editable={{
type: 'single',
onSave: async (key, record, originRow) => {
@ -552,11 +778,6 @@ const List: React.FC = () => {
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
pagination={{
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
}}
/>
<BatchEditModal
visible={batchEditModalVisible}
@ -571,7 +792,8 @@ const List: React.FC = () => {
<SyncToSiteModal
visible={syncModalVisible}
onClose={() => setSyncModalVisible(false)}
products={syncProducts}
productIds={syncProductIds}
productRows={selectedRows}
onSuccess={() => {
setSyncModalVisible(false);
setSelectedRows([]);

View File

@ -3,7 +3,6 @@ import {
productcontrollerGetcategoryattributes,
productcontrollerGetproductlist,
} from '@/servers/api/product';
import { DownloadOutlined } from '@ant-design/icons';
import {
ActionType,
PageContainer,
@ -113,18 +112,12 @@ 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', {
params: { 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);
@ -202,92 +195,6 @@ const PermutationPage: React.FC = () => {
setCreateModalVisible(true);
};
// 处理导出CSV功能
const handleExport = () => {
try {
// 如果没有数据则提示用户
if (permutations.length === 0) {
message.warning('暂无数据可导出');
return;
}
// 生成CSV表头(包含所有属性列和SKU列)
const headers = [
...attributes.map((attr) => attr.title || attr.name),
'SKU',
'状态',
];
// 生成CSV数据行
const rows = permutations.map((perm) => {
const key = generateKeyFromPermutation(perm);
const product = existingProducts.get(key);
// 获取每个属性值
const attrValues = attributes.map((attr) => {
const value = perm[attr.name]?.name || '';
return value;
});
// 获取SKU和状态
const sku = product?.sku || '';
const status = product ? '已存在' : '未创建';
return [...attrValues, sku, status];
});
// 将表头和数据行合并
const csvContent = [headers, ...rows]
.map((row) =>
// 处理CSV中的特殊字符(逗号、双引号、换行符)
row
.map((cell) => {
const cellStr = String(cell || '');
// 如果包含逗号、双引号或换行符,需要用双引号包裹,并将内部的双引号转义
if (
cellStr.includes(',') ||
cellStr.includes('"') ||
cellStr.includes('\n')
) {
return `"${cellStr.replace(/"/g, '""')}"`;
}
return cellStr;
})
.join(','),
)
.join('\n');
// 添加BOM以支持Excel正确显示中文
const BOM = '\uFEFF';
const blob = new Blob([BOM + csvContent], {
type: 'text/csv;charset=utf-8;',
});
// 创建下载链接并触发下载
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
// 生成文件名(包含当前分类名称和日期)
const category = categories.find((c) => c.id === categoryId);
const categoryName = category?.name || '产品';
const date = new Date().toISOString().slice(0, 10);
link.setAttribute('download', `${categoryName}_排列组合_${date}.csv`);
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
URL.revokeObjectURL(url);
message.success('导出成功');
} catch (error) {
console.error('导出失败:', error);
message.error('导出失败');
}
};
const columns: any[] = [
...attributes.map((attr) => ({
title: attr.title || attr.name,
@ -299,7 +206,7 @@ const PermutationPage: React.FC = () => {
const valB = b[attr.name]?.name || '';
return valA.localeCompare(valB);
},
filters: attributeValues?.[attr.name]?.map?.((v: any) => ({
filters: attributeValues[attr.name]?.map((v: any) => ({
text: v.name,
value: v.name,
})),
@ -400,21 +307,11 @@ const PermutationPage: React.FC = () => {
pagination={{
defaultPageSize: 50,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
}}
scroll={{ x: 'max-content' }}
search={false}
toolBarRender={() => [
<Button
key="export"
type="default"
icon={<DownloadOutlined />}
onClick={handleExport}
>
</Button>,
]}
toolBarRender={false}
/>
)}
</ProCard>

View File

@ -1,461 +0,0 @@
import { productcontrollerGetproductlist } from '@/servers/api/product';
import {
siteapicontrollerGetproducts,
siteapicontrollerUpsertproduct,
} from '@/servers/api/siteApi';
import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { SyncOutlined } from '@ant-design/icons';
import { ModalForm, ProFormText } from '@ant-design/pro-components';
import { Button, message, Spin, Tag } from 'antd';
import React, { useEffect, useState } from 'react';
// 定义站点接口
interface Site {
id: number;
name: string;
skuPrefix?: string;
isDisabled?: boolean;
}
// 定义本地产品接口(与后端 Product 实体匹配)
interface SiteProduct {
id: number;
sku: string;
name: string;
nameCn: string;
shortDescription?: string;
description?: string;
price: number;
promotionPrice: number;
type: string;
categoryId?: number;
category?: any;
attributes?: any[];
components?: any[];
siteSkus: string[];
source: number;
createdAt: Date;
updatedAt: Date;
}
// 定义本地产品完整接口
interface LocalProduct {
id: number;
sku: string;
name: string;
nameCn: string;
shortDescription?: string;
description?: string;
price: number;
promotionPrice: number;
type: string;
categoryId?: number;
category?: any;
attributes?: any[];
components?: any[];
siteSkus: string[];
source: number;
images?: string[];
weight?: number;
dimensions?: any;
}
// 定义站点产品数据接口
interface SiteProductData {
sku: string;
regular_price?: number;
price?: number;
sale_price?: number;
stock_quantity?: number;
stockQuantity?: number;
status?: string;
externalProductId?: string;
name?: string;
description?: string;
images?: string[];
}
interface SiteProductCellProps {
// 产品行数据
product: SiteProduct;
// 站点列数据
site: Site;
// 同步成功后的回调
onSyncSuccess?: () => void;
}
const SiteProductCell: React.FC<SiteProductCellProps> = ({
product,
site,
onSyncSuccess,
}) => {
// 存储该站点对应的产品数据
const [siteProduct, setSiteProduct] = useState<SiteProductData | null>(null);
// 存储本地产品完整数据
const [localProduct, setLocalProduct] = useState<LocalProduct | null>(null);
// 加载状态
const [loading, setLoading] = useState(false);
// 是否已加载过数据
const [loaded, setLoaded] = useState(false);
// 同步中状态
const [syncing, setSyncing] = useState(false);
// 组件挂载时加载数据
useEffect(() => {
loadSiteProduct();
}, [product.id, site.id]);
// 加载站点产品数据
const loadSiteProduct = async () => {
// 如果已经加载过,则不再重复加载
if (loaded) {
return;
}
setLoading(true);
try {
// 首先查找该产品在该站点的实际SKU
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
// 这里使用模板生成的 SKU 作为默认值
let siteProductSku = '';
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
// 如果没有找到实际的siteSku则根据模板或默认规则生成期望的SKU
const expectedSku =
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
// 使用 siteapicontrollerGetproducts 获取该站点的所有产品
const productsRes = await siteapicontrollerGetproducts({
siteId: site.id,
current: 1,
pageSize: 10000,
} as any);
if (productsRes.data?.items) {
// 在该站点的产品数据中查找匹配的产品
let foundProduct = productsRes.data.items.find(
(item: any) => item.sku === expectedSku,
);
// 如果根据实际SKU没找到再尝试用模板生成的SKU查找
if (!foundProduct && siteProductSku) {
foundProduct = productsRes.data.items.find(
(item: any) => item.sku === siteProductSku,
);
}
if (foundProduct) {
setSiteProduct(foundProduct as SiteProductData);
}
}
// 标记为已加载
setLoaded(true);
} catch (error) {
console.error(`加载站点 ${site.name} 的产品数据失败:`, error);
} finally {
setLoading(false);
}
};
// 获取本地产品完整信息
const getLocalProduct = async (): Promise<LocalProduct | null> => {
try {
// 如果已经有本地产品数据,直接返回
if (localProduct) {
return localProduct;
}
// 使用 productcontrollerGetproductlist 获取本地产品完整信息
const res = await productcontrollerGetproductlist({
where: {
id: product.id,
},
} as any);
if (res.success && res.data) {
const productData = res.data as LocalProduct;
setLocalProduct(productData);
return productData;
}
return null;
} catch (error) {
console.error('获取本地产品信息失败:', error);
return null;
}
};
// 渲染站点SKU
const renderSiteSku = async (data: any): Promise<string> => {
try {
// 使用 templatecontrollerRendertemplate API 渲染模板
const res = await templatecontrollerRendertemplate(
{ name: 'siteproduct-sku' } as any,
data,
);
return res?.template || res?.result || '';
} catch (error) {
console.error('渲染SKU模板失败:', error);
return '';
}
};
// 同步产品到站点
const syncProductToSite = async (values: any) => {
try {
setSyncing(true);
const hide = message.loading('正在同步...', 0);
// 获取本地产品完整信息
const productDetail = await getLocalProduct();
if (!productDetail) {
hide();
message.error('获取本地产品信息失败');
return false;
}
// 构造要同步的产品数据
const productData: any = {
sku: values.sku,
name: productDetail.name,
description: productDetail.description || '',
regular_price: productDetail.price,
price: productDetail.price,
stock_quantity: productDetail.stock,
status: 'publish',
};
// 如果有图片,添加图片信息
if (productDetail.images && productDetail.images.length > 0) {
productData.images = productDetail.images;
}
// 如果有重量,添加重量信息
if (productDetail.weight) {
productData.weight = productDetail.weight;
}
// 如果有尺寸,添加尺寸信息
if (productDetail.dimensions) {
productData.dimensions = productDetail.dimensions;
}
// 使用 siteapicontrollerUpsertproduct API 同步产品到站点
const res = await siteapicontrollerUpsertproduct(
{ siteId: site.id } as any,
productData as any,
);
if (!res.success) {
hide();
throw new Error(res.message || '同步失败');
}
// 更新本地状态
if (res.data && typeof res.data === 'object') {
setSiteProduct(res.data as SiteProductData);
}
hide();
message.success('同步成功');
// 触发回调
if (onSyncSuccess) {
onSyncSuccess();
}
return true;
} catch (error: any) {
message.error('同步失败: ' + (error.message || error.toString()));
return false;
} finally {
setSyncing(false);
}
};
// 更新同步产品到站点
const updateSyncProduct = async (values: any) => {
try {
setSyncing(true);
const hide = message.loading('正在更新...', 0);
// 获取本地产品完整信息
const productDetail = await getLocalProduct();
if (!productDetail) {
hide();
message.error('获取本地产品信息失败');
return false;
}
// 构造要更新的产品数据
const productData: any = {
...siteProduct,
sku: values.sku,
name: productDetail.name,
description: productDetail.description || '',
regular_price: productDetail.price,
price: productDetail.price,
stock_quantity: productDetail.stock,
status: 'publish',
};
// 如果有图片,添加图片信息
if (productDetail.images && productDetail.images.length > 0) {
productData.images = productDetail.images;
}
// 如果有重量,添加重量信息
if (productDetail.weight) {
productData.weight = productDetail.weight;
}
// 如果有尺寸,添加尺寸信息
if (productDetail.dimensions) {
productData.dimensions = productDetail.dimensions;
}
// 使用 siteapicontrollerUpsertproduct API 更新产品到站点
const res = await siteapicontrollerUpsertproduct(
{ siteId: site.id } as any,
productData as any,
);
if (!res.success) {
hide();
throw new Error(res.message || '更新失败');
}
// 更新本地状态
if (res.data && typeof res.data === 'object') {
setSiteProduct(res.data as SiteProductData);
}
hide();
message.success('更新成功');
// 触发回调
if (onSyncSuccess) {
onSyncSuccess();
}
return true;
} catch (error: any) {
message.error('更新失败: ' + (error.message || error.toString()));
return false;
} finally {
setSyncing(false);
}
};
// 如果正在加载,显示加载状态
if (loading) {
return (
<div style={{ textAlign: 'center', padding: 10 }}>
<Spin size="small" />
</div>
);
}
// 如果没有找到站点产品,显示同步按钮
if (!siteProduct) {
// 首先查找该产品在该站点的实际SKU
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
// 这里使用模板生成的 SKU 作为默认值
let siteProductSku = '';
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
const defaultSku =
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
return (
<ModalForm
title="同步产品"
trigger={
<Button type="link" icon={<SyncOutlined />}>
</Button>
}
width={400}
onFinish={async (values) => {
return await syncProductToSite(values);
}}
initialValues={{
sku: defaultSku,
}}
>
<ProFormText
name="sku"
label="商店 SKU"
placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]}
/>
</ModalForm>
);
}
// 显示站点产品信息
return (
<div style={{ fontSize: 12 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'start',
}}
>
<div style={{ fontWeight: 'bold' }}>{siteProduct.sku}</div>
<ModalForm
title="更新同步"
trigger={
<Button
type="link"
size="small"
icon={<SyncOutlined spin={false} />}
>
</Button>
}
width={400}
onFinish={async (values) => {
return await updateSyncProduct(values);
}}
initialValues={{
sku: siteProduct.sku,
}}
>
<ProFormText
name="sku"
label="商店 SKU"
placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]}
disabled
/>
<div style={{ marginBottom: 16, color: '#666' }}>
</div>
</ModalForm>
</div>
<div>Price: {siteProduct.regular_price ?? siteProduct.price}</div>
{siteProduct.sale_price && (
<div style={{ color: 'red' }}>Sale: {siteProduct.sale_price}</div>
)}
<div>
Stock: {siteProduct.stock_quantity ?? siteProduct.stockQuantity}
</div>
<div style={{ marginTop: 2 }}>
Status:{' '}
{siteProduct.status === 'publish' ? (
<Tag color="green">Published</Tag>
) : (
<Tag>{siteProduct.status}</Tag>
)}
</div>
</div>
);
};
export default SiteProductCell;

View File

@ -1,52 +1,50 @@
import {
productcontrollerBatchsynctosite,
productcontrollerGetproductlist,
productcontrollerSynctosite,
} from '@/servers/api/product';
import { productcontrollerGetproductlist } from '@/servers/api/product';
import { templatecontrollerGettemplatebyname } from '@/servers/api/template';
import { EditOutlined, SyncOutlined } from '@ant-design/icons';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
Button,
Card,
message,
Modal,
Progress,
Select,
Spin,
Tag,
} from 'antd';
ActionType,
ModalForm,
ProColumns,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, Card, Spin, Tag, message, Select, Progress, Modal } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm';
import SiteProductCell from './SiteProductCell';
// 定义站点接口
interface Site {
id: number;
id: string;
name: string;
skuPrefix?: string;
isDisabled?: boolean;
}
// 定义本地产品接口(与后端 Product 实体匹配)
interface SiteProduct {
id: number;
// 定义WordPress商品接口
interface WpProduct {
id?: number;
externalProductId?: string;
sku: string;
name: string;
nameCn: string;
shortDescription?: string;
description?: string;
price: number;
promotionPrice: number;
type: string;
categoryId?: number;
category?: any;
price: string;
regular_price?: string;
sale_price?: string;
stock_quantity: number;
stockQuantity?: number;
status: string;
attributes?: any[];
components?: any[];
siteSkus: string[];
source: number;
createdAt: Date;
updatedAt: Date;
constitution?: { sku: string; quantity: number }[];
}
// 扩展本地产品接口,包含对应的 WP 产品信息
interface ProductWithWP extends API.Product {
wpProducts: Record<string, WpProduct>;
attributes?: any[];
siteSkus?: Array<{
siteSku: string;
[key: string]: any;
}>;
}
// 定义API响应接口
@ -72,61 +70,121 @@ const getSites = async (): Promise<ApiResponse<Site>> => {
};
};
const getWPProducts = async (): Promise<ApiResponse<WpProduct>> => {
return request('/product/wp-products', {
method: 'GET',
});
};
const ProductSyncPage: React.FC = () => {
const [sites, setSites] = useState<Site[]>([]);
// 存储所有 WP 产品,用于查找匹配。 Key: SKU (包含前缀)
const [wpProductMap, setWpProductMap] = useState<Map<string, WpProduct>>(
new Map(),
);
const [skuTemplate, setSkuTemplate] = useState<string>('');
const [initialLoading, setInitialLoading] = useState(true);
const actionRef = useRef<ActionType>();
const [selectedSiteId, setSelectedSiteId] = useState<string>('');
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 [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedRows, setSelectedRows] = useState<SiteProduct[]>([]);
// 初始化加载站点列表
const [syncResults, setSyncResults] = useState<{ success: number; failed: number; errors: string[] }>({ success: 0, failed: 0, errors: [] });
// 初始化数据:获取站点和所有 WP 产品
useEffect(() => {
const initializeData = async () => {
const fetchData = async () => {
try {
// 获取站点列表
const sitesRes = await getSites();
if (sitesRes.success && sitesRes.data.length > 0) {
setSites(sitesRes.data);
setInitialLoading(true);
// 获取所有站点
const sitesResponse = await getSites();
const rawSiteList = sitesResponse.data || [];
// 过滤掉已禁用的站点
const siteList: Site[] = rawSiteList.filter((site) => !site.isDisabled);
setSites(siteList);
// 获取所有 WordPress 商品
const wpProductsResponse = await getWPProducts();
const wpProductList: WpProduct[] = wpProductsResponse.data || [];
// 构建 WP 产品 MapKey 为 SKU
const map = new Map<string, WpProduct>();
wpProductList.forEach((p) => {
if (p.sku) {
map.set(p.sku, p);
}
});
setWpProductMap(map);
// 获取 SKU 模板
try {
const templateRes = await templatecontrollerGettemplatebyname({
name: 'site.product.sku',
});
if (templateRes && templateRes.value) {
setSkuTemplate(templateRes.value);
}
} catch (e) {
console.log('Template site.product.sku not found, using default.');
}
} catch (error) {
console.error('初始化数据失败:', error);
message.error('初始化数据失败');
message.error('获取基础数据失败,请重试');
console.error('Error fetching data:', error);
} finally {
setInitialLoading(false);
}
};
initializeData();
fetchData();
}, []);
// 同步产品到站点
const syncProductToSite = async (
values: any,
record: SiteProduct,
record: ProductWithWP,
site: Site,
siteProductId?: string,
wpProductId?: string,
) => {
try {
const hide = message.loading('正在同步...', 0);
const data = {
name: record.name,
sku: values.sku,
regular_price: record.price?.toString(),
sale_price: record.promotionPrice?.toString(),
type: record.type === 'bundle' ? 'simple' : record.type,
description: record.description,
status: 'publish',
stock_status: 'instock',
manage_stock: false,
};
// 使用 productcontrollerSynctosite API 同步产品到站点
const res = await productcontrollerSynctosite({
productId: Number(record.id),
siteId: Number(site.id),
} as any);
let res;
if (wpProductId) {
res = await request(`/site-api/${site.id}/products/${wpProductId}`, {
method: 'PUT',
data,
});
} else {
res = await request(`/site-api/${site.id}/products`, {
method: 'POST',
data,
});
}
console.log('res', res);
if (!res.success) {
hide();
throw new Error(res.message || '同步失败');
}
// 更新本地缓存 Map避免刷新
setWpProductMap((prev) => {
const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') {
newMap.set(values.sku, res.data as WpProduct);
}
return newMap;
});
hide();
message.success('同步成功');
@ -134,86 +192,135 @@ const ProductSyncPage: React.FC = () => {
} catch (error: any) {
message.error('同步失败: ' + (error.message || error.toString()));
return false;
} finally {
}
};
// 批量同步产品到指定站点
const batchSyncProducts = async (productsToSync?: SiteProduct[]) => {
const batchSyncProducts = async () => {
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;
}
// 如果没有传入产品列表,则使用选中的产品
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 SiteProduct[];
} catch (error) {
message.error('获取产品列表失败');
return;
}
}
setSyncing(true);
setSyncProgress(0);
setSyncResults({ success: 0, failed: 0, errors: [] });
try {
// 使用 productcontrollerBatchsynctosite API 批量同步
const productIds = products.map((product) => Number(product.id));
// 更新进度为50%,表示正在处理
setSyncProgress(50);
const res = await productcontrollerBatchsynctosite({
productIds: productIds,
siteId: Number(targetSite.id),
// 获取所有产品
const { data, success } = await productcontrollerGetproductlist({
current: 1,
pageSize: 10000, // 获取所有产品
} as any);
if (res.success) {
const syncedCount = res.data?.synced || 0;
const errors = res.data?.errors || [];
if (!success || !data?.items) {
message.error('获取产品列表失败');
return;
}
// 更新进度为100%,表示完成
setSyncProgress(100);
const products = data.items as ProductWithWP[];
const totalProducts = products.length;
let processed = 0;
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
setSyncResults({
success: syncedCount,
failed: errors.length,
errors: errors.map((err: any) => err.error || '未知错误'),
});
// 逐个同步产品
for (const product of products) {
try {
// 获取该产品在目标站点的SKU
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);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
}
}
if (errors.length === 0) {
message.success(`批量同步完成,成功同步 ${syncedCount} 个产品`);
} else {
message.warning(
`批量同步完成,成功 ${syncedCount} 个,失败 ${errors.length}`,
// 如果没有找到实际的siteSku则根据模板生成
const expectedSku = siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site: targetSite, product })
: `${targetSite.skuPrefix || ''}-${product.sku}`
);
// 检查是否已存在
const existingProduct = wpProductMap.get(expectedSku);
// 准备同步数据
const syncData = {
name: product.name,
sku: expectedSku,
regular_price: product.price?.toString(),
sale_price: product.promotionPrice?.toString(),
type: product.type === 'bundle' ? 'simple' : product.type,
description: product.description,
status: 'publish',
stock_status: 'instock',
manage_stock: false,
};
let res;
if (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`, {
method: 'POST',
data: syncData,
});
}
console.log('res', res);
if (res.success) {
successCount++;
// 更新本地缓存
setWpProductMap((prev) => {
const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') {
newMap.set(expectedSku, res.data as WpProduct);
}
return newMap;
});
} else {
failedCount++;
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
}
} catch (error: any) {
failedCount++;
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
}
// 刷新表格
actionRef.current?.reload();
} else {
throw new Error(res.message || '批量同步失败');
processed++;
setSyncProgress(Math.round((processed / totalProducts) * 100));
}
setSyncResults({ success: successCount, failed: failedCount, errors });
if (failedCount === 0) {
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
} else {
message.warning(`批量同步完成,成功 ${successCount} 个,失败 ${failedCount}`);
}
// 刷新表格
actionRef.current?.reload();
} catch (error: any) {
message.error('批量同步失败: ' + (error.message || error.toString()));
} finally {
@ -221,9 +328,27 @@ const ProductSyncPage: React.FC = () => {
}
};
// 简单的模板渲染函数
const renderSku = (template: string, data: any) => {
if (!template) return '';
// 支持 <%= it.path %> (Eta) 和 {{ path }} (Mustache/Handlebars)
return template.replace(
/<%=\s*it\.([\w.]+)\s*%>|\{\{\s*([\w.]+)\s*\}\}/g,
(_, p1, p2) => {
const path = p1 || p2;
const keys = path.split('.');
let value = data;
for (const key of keys) {
value = value?.[key];
}
return value === undefined || value === null ? '' : String(value);
},
);
};
// 生成表格列配置
const generateColumns = (): ProColumns<Site>[] => {
const columns: ProColumns<SiteProduct>[] = [
const generateColumns = (): ProColumns<ProductWithWP>[] => {
const columns: ProColumns<ProductWithWP>[] = [
{
title: 'SKU',
dataIndex: 'sku',
@ -315,21 +440,132 @@ const ProductSyncPage: React.FC = () => {
// 为每个站点生成列
sites.forEach((site: Site) => {
const siteColumn: ProColumns<SiteProduct> = {
const siteColumn: ProColumns<ProductWithWP> = {
title: site.name,
key: `site_${site.id}`,
hideInSearch: true,
width: 220,
render: (_, record) => {
// 首先查找该产品在该站点的实际SKU
let siteProductSku = '';
if (record.siteSkus && record.siteSkus.length > 0) {
// 根据站点名称匹配对应的siteSku
const siteSkuInfo = record.siteSkus.find((sku: any) => {
// 这里假设可以根据站点名称或其他标识来匹配
// 如果需要更精确的匹配逻辑,可以根据实际需求调整
return sku.siteSku && sku.siteSku.includes(site.skuPrefix || site.name);
});
if (siteSkuInfo) {
siteProductSku = siteSkuInfo.siteSku;
}
}
// 如果没有找到实际的siteSku则根据模板或默认规则生成期望的SKU
const expectedSku = siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`
);
// 尝试用确定的SKU获取WP产品
let wpProduct = wpProductMap.get(expectedSku);
// 如果根据实际SKU没找到再尝试用模板生成的SKU查找
if (!wpProduct && siteProductSku && skuTemplate) {
const templateSku = renderSku(skuTemplate, { site, product: record });
wpProduct = wpProductMap.get(templateSku);
}
if (!wpProduct) {
return (
<ModalForm
title="同步产品"
trigger={
<Button type="link" icon={<SyncOutlined />}>
</Button>
}
width={400}
onFinish={async (values) => {
return await syncProductToSite(values, record, site);
}}
initialValues={{
sku: siteProductSku || (
skuTemplate
? renderSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`
),
}}
>
<ProFormText
name="sku"
label="商店 SKU"
placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]}
/>
</ModalForm>
);
}
return (
<SiteProductCell
product={record}
site={site}
onSyncSuccess={() => {
// 同步成功后刷新表格
actionRef.current?.reload();
}}
/>
<div style={{ fontSize: 12 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'start',
}}
>
<div style={{ fontWeight: 'bold' }}>{wpProduct.sku}</div>
<ModalForm
title="更新同步"
trigger={
<Button
type="link"
size="small"
icon={<SyncOutlined spin={false} />}
></Button>
}
width={400}
onFinish={async (values) => {
return await syncProductToSite(
values,
record,
site,
wpProduct.externalProductId,
);
}}
initialValues={{
sku: wpProduct.sku,
}}
>
<ProFormText
name="sku"
label="商店 SKU"
placeholder="请输入商店 SKU"
rules={[{ required: true, message: '请输入 SKU' }]}
disabled
/>
<div style={{ marginBottom: 16, color: '#666' }}>
</div>
</ModalForm>
</div>
<div>Price: {wpProduct.regular_price ?? wpProduct.price}</div>
{wpProduct.sale_price && (
<div style={{ color: 'red' }}>Sale: {wpProduct.sale_price}</div>
)}
<div>
Stock: {wpProduct.stock_quantity ?? wpProduct.stockQuantity}
</div>
<div style={{ marginTop: 2 }}>
Status:{' '}
{wpProduct.status === 'publish' ? (
<Tag color="green">Published</Tag>
) : (
<Tag>{wpProduct.status}</Tag>
)}
</div>
</div>
);
},
};
@ -351,69 +587,58 @@ const ProductSyncPage: React.FC = () => {
}
return (
<Card title="商品同步状态" className="product-sync-card">
<ProTable<SiteProduct>
columns={generateColumns()}
actionRef={actionRef}
rowKey="id"
rowSelection={{
selectedRowKeys,
onChange: (keys, rows) => {
setSelectedRowKeys(keys);
setSelectedRows(rows);
},
}}
toolBarRender={() => [
<Card
title="商品同步状态"
className="product-sync-card"
extra={
<div style={{ display: 'flex', gap: 8 }}>
<Select
key="site-select"
style={{ width: 200 }}
placeholder="选择目标站点"
value={selectedSiteId}
onChange={setSelectedSiteId}
options={sites.map((site) => ({
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);
}}
onClick={() => setBatchSyncModalVisible(true)}
disabled={!selectedSiteId || sites.length === 0}
>
</Button>,
]}
</Button>
</div>
}
>
<ProTable<ProductWithWP>
columns={generateColumns()}
actionRef={actionRef}
rowKey="id"
request={async (params, sort, filter) => {
// 调用本地获取产品列表 API
const response = await productcontrollerGetproductlist({
const { data, success } = await productcontrollerGetproductlist({
...params,
current: params.current,
pageSize: params.pageSize,
// 传递搜索参数
// keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
sku: (params as any).sku,
name: (params as any).name,
} as any);
console.log('result', response);
// 返回给 ProTable
return {
data: response.data?.items || [],
success: response.success,
total: response.data?.total || 0,
data: (data?.items || []) as ProductWithWP[],
success,
total: data?.total || 0,
};
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 'max-content' }}
search={{
@ -425,7 +650,7 @@ const ProductSyncPage: React.FC = () => {
}}
dateFormatter="string"
/>
{/* 批量同步模态框 */}
<Modal
title="批量同步产品"
@ -436,59 +661,45 @@ const ProductSyncPage: React.FC = () => {
maskClosable={!syncing}
>
<div style={{ marginBottom: 16 }}>
<p>
:
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
</p>
{selectedRows.length > 0 ? (
<p>
<strong>{selectedRows.length}</strong>
</p>
) : (
<p></p>
)}
<p><strong>{sites.find(s => s.id === selectedSiteId)?.name}</strong></p>
<p></p>
</div>
{syncing && (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8 }}>:</div>
<div style={{ marginBottom: 8 }}></div>
<Progress percent={syncProgress} status="active" />
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
:{syncResults.success} | :{syncResults.failed}
{syncResults.success} | {syncResults.failed}
</div>
</div>
)}
{syncResults.errors.length > 0 && (
<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) => (
<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>
)}
<div style={{ textAlign: 'right' }}>
<Button
<Button
onClick={() => setBatchSyncModalVisible(false)}
disabled={syncing}
style={{ marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => batchSyncProducts()}
<Button
type="primary"
onClick={batchSyncProducts}
loading={syncing}
disabled={syncing}
>

View File

@ -1,32 +1,15 @@
import { ordercontrollerSyncorders } from '@/servers/api/order';
import { ordercontrollerSyncorder } from '@/servers/api/order';
import {
sitecontrollerCreate,
sitecontrollerDisable,
sitecontrollerList,
sitecontrollerUpdate,
} from '@/servers/api/site';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
import {
ActionType,
DrawerForm,
ProColumns,
ProFormSelect,
ProFormSwitch,
ProTable,
} from '@ant-design/pro-components';
import {
Button,
Form,
message,
notification,
Popconfirm,
Space,
Tag,
} from 'antd';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
import React, { useEffect, useRef, useState } from 'react';
import { wpproductcontrollerSyncproducts } from '@/servers/api/wpProduct';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { Button, message, notification, Popconfirm, Space, Tag } from 'antd';
import React, { useRef, useState } from 'react';
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
// 区域数据项类型
@ -58,25 +41,31 @@ export interface SiteItem {
const SiteList: React.FC = () => {
const actionRef = useRef<ActionType>();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<SiteItem & { areas: string[] } | null>(null);
const [editing, setEditing] = useState<SiteItem | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchEditOpen, setBatchEditOpen] = useState(false);
const [batchEditForm] = Form.useForm();
countries.registerLocale(zhCN);
const handleSync = async (ids: number[]) => {
if (!ids.length) return;
const hide = message.loading('正在同步...', 0);
const stats = {
products: { success: 0, fail: 0 },
orders: { success: 0, fail: 0 },
subscriptions: { success: 0, fail: 0 },
};
try {
for (const id of ids) {
// 同步产品
const prodRes = await wpproductcontrollerSyncproducts({ siteId: id });
if (prodRes.success) {
stats.products.success += 1;
} else {
stats.products.fail += 1;
}
// 同步订单
const orderRes = await ordercontrollerSyncorders({ siteId: id });
const orderRes = await ordercontrollerSyncorder({ siteId: id });
if (orderRes.success) {
stats.orders.success += 1;
} else {
@ -97,6 +86,9 @@ const SiteList: React.FC = () => {
message: '同步完成',
description: (
<div>
<p>
产品: 成功 {stats.products.success}, {stats.products.fail}
</p>
<p>
订单: 成功 {stats.orders.success}, {stats.orders.fail}
</p>
@ -117,68 +109,6 @@ const SiteList: React.FC = () => {
}
};
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
// 处理批量编辑提交
const handleBatchEditFinish = async (values: any) => {
if (!selectedRowKeys.length) return;
const hide = message.loading('正在批量更新...', 0);
try {
// 遍历所有选中的站点 ID
for (const id of selectedRowKeys) {
// 构建更新数据对象,只包含用户填写了值的字段
const updateData: any = {};
// 如果用户选择了区域,则更新区域
if (values.areas && values.areas.length > 0) {
updateData.areas = values.areas;
}
// 如果用户选择了仓库,则更新仓库
if (values.stockPointIds && values.stockPointIds.length > 0) {
updateData.stockPointIds = values.stockPointIds;
}
// 如果用户设置了禁用状态,则更新状态
if (values.isDisabled !== undefined) {
updateData.isDisabled = values.isDisabled;
}
// 如果有需要更新的字段,则调用更新接口
if (Object.keys(updateData).length > 0) {
await sitecontrollerUpdate({ id: String(id) }, updateData);
}
}
hide();
message.success('批量更新成功');
setBatchEditOpen(false);
setSelectedRowKeys([]);
batchEditForm.resetFields();
actionRef.current?.reload();
} catch (error: any) {
hide();
message.error(error.message || '批量更新失败');
}
};
// 当批量编辑弹窗打开时,重置表单
useEffect(() => {
if (batchEditOpen) {
batchEditForm.resetFields();
}
}, [batchEditOpen, batchEditForm]);
// 表格列定义
const columns: ProColumns<SiteItem>[] = [
{
@ -202,51 +132,26 @@ const SiteList: React.FC = () => {
</a>
),
},
{
title: 'webhook地址',
dataIndex: 'webhookUrl',
hideInSearch: true,
},
{
title: 'SKU 前缀',
dataIndex: 'skuPrefix',
width: 160,
hideInSearch: true,
},
{
title: '平台',
dataIndex: 'type',
width: 140,
valueType: 'select',
request: async () => [
{ label: 'WooCommerce', value: 'woocommerce' },
{ label: 'Shopyy', value: 'shopyy' },
],
},
{
// 地区列配置
title: '地区',
dataIndex: 'areas',
hideInSearch: true,
render: (_, row) => {
// 如果没有关联地区,显示"全局"标签
if (!row.areas || row.areas.length === 0) {
return <Tag color="default"></Tag>;
}
// 遍历显示所有关联的地区名称
return (
<Space wrap>
{row.areas.map((area) => (
<Tag color="geekblue" key={area.code}>
{area.name}
</Tag>
))}
</Space>
);
},
},
{
title: '关联仓库',
dataIndex: 'stockPoints',
width: 200,
hideInSearch: true,
render: (_, row) => {
if (!row.stockPoints || row.stockPoints.length === 0) {
@ -292,13 +197,7 @@ const SiteList: React.FC = () => {
<Button
size="small"
onClick={() => {
function normalEditing(row:SiteItem){
return {
...row,
areas: row.areas?.map(area=>area.code) || [],
}
}
setEditing(normalEditing(row));
setEditing(row);
setOpen(true);
}}
>
@ -381,11 +280,6 @@ const SiteList: React.FC = () => {
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
pagination={{
defaultPageSize: 20,
showSizeChanger: true,
showQuickJumper: true,
}}
toolBarRender={() => [
<Button
type="primary"
@ -396,12 +290,6 @@ const SiteList: React.FC = () => {
>
</Button>,
<Button
disabled={!selectedRowKeys.length}
onClick={() => setBatchEditOpen(true)}
>
</Button>,
<Button
disabled={!selectedRowKeys.length}
onClick={() => handleSync(selectedRowKeys as number[])}
@ -423,51 +311,6 @@ const SiteList: React.FC = () => {
isEdit={!!editing}
onFinish={handleFinish}
/>
{/* 批量编辑弹窗 */}
<DrawerForm
title={`批量编辑站点 (${selectedRowKeys.length} 个)`}
form={batchEditForm}
open={batchEditOpen}
onOpenChange={setBatchEditOpen}
onFinish={handleBatchEditFinish}
layout="vertical"
>
<ProFormSelect
name="areas"
label="区域"
mode="multiple"
placeholder="请选择区域(留空表示不修改)"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
/>
<ProFormSelect
name="stockPointIds"
label="关联仓库"
mode="multiple"
placeholder="请选择关联仓库(留空表示不修改)"
request={async () => {
// 从后端接口获取仓库数据
const res = await stockcontrollerGetallstockpoints();
// 使用可选链和空值合并运算符来安全地处理可能未定义的数据
return (
res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ??
[]
);
}}
/>
<ProFormSwitch
name="isDisabled"
label="是否禁用"
fieldProps={{
checkedChildren: '是',
unCheckedChildren: '否',
}}
/>
</DrawerForm>
</>
);
};

View File

@ -1,4 +1,3 @@
import Address from '@/components/Address';
import {
DeleteFilled,
EditOutlined,
@ -188,29 +187,27 @@ const CustomerPage: React.FC = () => {
hideInSearch: true,
render: (_, record) => {
const { billing } = record;
return <Address address={billing} />;
if (!billing) return '-';
return (
<div style={{ fontSize: 12 }}>
<div>
{billing.address_1} {billing.address_2}
</div>
<div>
{billing.city}, {billing.state}, {billing.postcode}
</div>
<div>{billing.country}</div>
<div>{billing.phone}</div>
</div>
);
},
},
{
title: '物流地址',
dataIndex: 'shipping',
hideInSearch: true,
render: (shipping) => {
return <Address address={shipping} />;
},
},
{
title: '创建时间',
title: '注册时间',
dataIndex: 'date_created',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '更新时间',
dataIndex: 'date_modified',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
@ -281,7 +278,7 @@ const CustomerPage: React.FC = () => {
const response = await request(`/site-api/${siteId}/customers`, {
params: {
page: current,
per_page: pageSize,
page_size: pageSize,
where,
...(orderObj ? { order: orderObj } : {}),
...(name || email ? { search: name || email } : {}),
@ -465,11 +462,7 @@ const CustomerPage: React.FC = () => {
<ProTable
rowKey="id"
search={false}
pagination={{
pageSize: 20,
showSizeChanger: true,
showQuickJumper: true,
}}
pagination={{ pageSize: 20 }}
columns={[
{ title: '订单号', dataIndex: 'number', copyable: true },
{

View File

@ -1,3 +1,4 @@
import { areacontrollerGetarealist } from '@/servers/api/area';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import {
DrawerForm,
@ -8,8 +9,6 @@ import {
ProFormTextArea,
} from '@ant-design/pro-components';
import { Form } from 'antd';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
import React, { useEffect } from 'react';
// 定义组件的 props 类型
@ -30,9 +29,6 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
}) => {
const [form] = Form.useForm();
// 初始化中文语言包
countries.registerLocale(zhCN);
// 当 initialValues 或 open 状态变化时, 更新表单的值
useEffect(() => {
// 如果抽屉是打开的
@ -40,11 +36,8 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
// 如果是编辑模式并且有初始值
if (isEdit && initialValues) {
// 编辑模式下, 设置表单值为初始值
const { token, consumerKey, consumerSecret, ...safeInitialValues } =
initialValues;
// 清空敏感字段, 让用户输入最新的数据
form.setFieldsValue({
...safeInitialValues,
...initialValues,
isDisabled: initialValues.isDisabled === 1, // 将后端的 1/0 转换成 true/false
});
} else {
@ -54,17 +47,6 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
}
}, [initialValues, isEdit, open, form]);
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
return (
<DrawerForm
title={isEdit ? '编辑站点' : '新建站点'}
@ -78,7 +60,6 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
}}
layout="vertical"
>
{JSON.stringify(initialValues)}
<ProFormText
name="name"
label="名称"
@ -101,11 +82,6 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
label="网站地址"
placeholder="请输入网站地址"
/>
<ProFormText
name="webhookUrl"
label="Webhook 地址"
placeholder="请输入 Webhook 地址"
/>
<ProFormSelect
name="type"
label="平台"
@ -170,11 +146,15 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
label="区域"
mode="multiple"
placeholder="请选择区域"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
request={async () => {
// 从后端接口获取区域数据
const res = await areacontrollerGetarealist({ pageSize: 1000 });
// areacontrollerGetarealist 直接返回数组, 所以不需要 .data.list
return res.map((area: any) => ({
label: area.name,
value: area.code,
}));
}}
/>
<ProFormSelect
name="stockPointIds"

View File

@ -2,8 +2,7 @@ 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, Col, Menu, Row, Select, Spin, message } from 'antd';
import Sider from 'antd/es/layout/Sider';
import { Button, Card, Col, Menu, Row, Select, Spin, message } from 'antd';
import React, { useEffect, useState } from 'react';
import type { SiteItem } from '../List/index';
import EditSiteForm from './EditSiteForm';
@ -15,7 +14,7 @@ const ShopLayout: React.FC = () => {
const location = useLocation();
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingSite, setEditingSite] = useState<SiteItem & { areas: string[] } | null>(null);
const [editingSite, setEditingSite] = useState<SiteItem | null>(null);
const fetchSites = async () => {
try {
@ -91,11 +90,22 @@ const ShopLayout: React.FC = () => {
};
return (
<PageContainer header={{ title: null, breadcrumb: undefined }}>
<PageContainer
header={{ title: null, breadcrumb: undefined }}
contentStyle={{
padding: 0,
}}
>
<Row gutter={16} style={{ height: 'calc(100vh - 100px)' }}>
<Col span={4} style={{ height: '100%' }}>
<Sider
style={{ background: 'white', height: '100%', overflow: 'hidden', zIndex: 1 }}
<Card
bodyStyle={{
padding: '10px 0',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
style={{ height: '100%', overflow: 'hidden' }}
>
<div style={{ padding: '0 10px 16px' }}>
<div
@ -103,13 +113,12 @@ const ShopLayout: React.FC = () => {
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '4px'
}}
>
<Select
style={{ flex: 1 }}
placeholder="请选择店铺"
options={sites?.map?.((site) => ({
options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
@ -120,18 +129,13 @@ const ShopLayout: React.FC = () => {
/>
<Button
icon={<EditOutlined />}
style={{ marginLeft: 8 }}
onClick={() => {
const currentSite = sites.find(
(site) => site.id === Number(siteId),
);
if (currentSite) {
function normalizeEditing(site: SiteItem) {
return {
...site,
areas: site.areas?.map(area => area.code) || [],
}
}
setEditingSite(normalizeEditing(currentSite));
setEditingSite(currentSite);
setEditModalOpen(true);
} else {
message.warning('请先选择一个店铺');
@ -153,12 +157,10 @@ const ShopLayout: React.FC = () => {
{ key: 'media', label: '媒体管理' },
{ key: 'customers', label: '客户管理' },
{ key: 'reviews', label: '评论管理' },
{ key: 'webhooks', label: 'Webhooks管理' },
{ key: 'links', label: '链接管理' },
]}
/>
</div>
</Sider>
</Card>
</Col>
<Col span={20} style={{ height: '100%', overflowY: 'auto' }}>
{siteId ? <Outlet /> : <div></div>}

View File

@ -1,99 +0,0 @@
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 {
title: string;
url: string;
}
const LinksPage: React.FC = () => {
const { siteId } = useParams<{ siteId: string }>();
const { message: antMessage } = App.useApp();
const [links, setLinks] = useState<LinkItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
// 获取链接列表的函数
const fetchLinks = async () => {
if (!siteId) return;
setLoading(true);
try {
const response = await request(`/site-api/${siteId}/links`);
if (response.success && response.data) {
setLinks(response.data);
} else {
antMessage.error(response.message || '获取链接列表失败');
}
} catch (error) {
antMessage.error('获取链接列表失败');
} finally {
setLoading(false);
}
};
// 页面加载时获取链接列表
useEffect(() => {
fetchLinks();
}, [siteId]);
// 处理链接点击事件,在新标签页打开
const handleLinkClick = (url: string) => {
window.open(url, '_blank', 'noopener,noreferrer');
};
return (
<div>
<PageHeader title="站点链接" breadcrumb={{ items: [] }} />
<Card
title="常用链接"
bordered={false}
extra={
<Button type="primary" onClick={fetchLinks} loading={loading}>
</Button>
}
>
<List
loading={loading}
dataSource={links}
renderItem={(item) => (
<List.Item
key={item.title}
actions={[
<Button
key={`visit-${item.title}`}
type="link"
icon={<LinkOutlined />}
onClick={() => handleLinkClick(item.url)}
target="_blank"
>
访
</Button>,
]}
>
<List.Item.Meta
title={item.title}
description={
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff' }}
>
{item.url}
</a>
}
/>
</List.Item>
)}
/>
</Card>
</div>
);
};
export default LinksPage;

View File

@ -203,7 +203,7 @@ const MediaPage: React.FC = () => {
const response = await request(`/site-api/${siteId}/media`, {
params: {
page: current,
per_page: pageSize,
page_size: pageSize,
...(orderObj ? { order: orderObj } : {}),
},
});

View File

@ -18,7 +18,6 @@ import {
CreateOrder,
EditOrder,
OrderNote,
ShipOrderForm,
} from '../components/Order/Forms';
const OrdersPage: React.FC = () => {
@ -62,14 +61,11 @@ const OrdersPage: React.FC = () => {
return [{ key: 'all', label: `全部(${total})` }, ...tabs];
}, [count]);
const columns: ProColumns<API.UnifiedOrderDTO>[] = [
{
title: '订单ID',
dataIndex: 'id',
},
const columns: ProColumns<API.Order>[] = [
{
title: '订单号',
dataIndex: 'number',
dataIndex: 'id',
hideInSearch: true,
},
{
title: '状态',
@ -77,6 +73,12 @@ const OrdersPage: React.FC = () => {
valueType: 'select',
valueEnum: ORDER_STATUS_ENUM,
},
{
title: '订单日期',
dataIndex: 'date_created',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '金额',
dataIndex: 'total',
@ -87,38 +89,6 @@ const OrdersPage: React.FC = () => {
dataIndex: 'currency',
hideInSearch: true,
},
{
title: '财务状态',
dataIndex: 'financial_status',
},
{
title: '支付方式',
dataIndex: 'payment_method',
},
{
title: '支付时间',
dataIndex: 'date_paid',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '创建时间',
dataIndex: 'date_created',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '更新时间',
dataIndex: 'date_modified',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '客户ID',
dataIndex: 'customer_id',
hideInSearch: true,
},
{
title: '客户邮箱',
dataIndex: 'email',
@ -126,28 +96,6 @@ const OrdersPage: React.FC = () => {
{
title: '客户姓名',
dataIndex: 'customer_name',
},
{
title: '客户IP',
dataIndex: 'customer_ip_address',
},
{
title: '联系电话',
render: (_, record) => record.shipping?.phone || record.billing?.phone,
},
{
title: '设备类型',
dataIndex: 'device_type',
hideInSearch: true,
},
{
title: '来源类型',
dataIndex: 'source_type',
hideInSearch: true,
},
{
title: 'UTM来源',
dataIndex: 'utm_source',
hideInSearch: true,
},
{
@ -163,9 +111,7 @@ const OrdersPage: React.FC = () => {
return (
<div>
{record.line_items.map((item: any) => (
<div
key={item.id}
>{`${item.name}(${item.sku}) x ${item.quantity}`}</div>
<div key={item.id}>{`${item.name} x ${item.quantity}`}</div>
))}
</div>
);
@ -174,6 +120,15 @@ const OrdersPage: React.FC = () => {
return '-';
},
},
{
title: '支付方式',
dataIndex: 'payment_method',
},
{
title: '联系电话',
hideInSearch: true,
render: (_, record) => record.shipping?.phone || record.billing?.phone,
},
{
title: '账单地址',
dataIndex: 'billing_full_address',
@ -190,73 +145,6 @@ const OrdersPage: React.FC = () => {
ellipsis: true,
copyable: true,
},
{
title: '发货状态',
dataIndex: 'fulfillment_status',
// hideInSearch: true,
// render: (_, record) => {
// const fulfillmentStatus = record.fulfillment_status;
// const fulfillmentStatusMap: Record<string, string> = {
// '0': '未发货',
// '1': '部分发货',
// '2': '已发货',
// '3': '已取消',
// '4': '确认发货',
// };
// if (fulfillmentStatus === undefined || fulfillmentStatus === null) {
// return '-';
// }
// return (
// fulfillmentStatusMap[String(fulfillmentStatus)] ||
// String(fulfillmentStatus)
// );
// },
},
{
title: '物流',
dataIndex: 'fulfillments',
hideInSearch: true,
render: (_, record) => {
// 检查是否有物流信息
if (
!record.fulfillments ||
!Array.isArray(record.fulfillments) ||
record.fulfillments.length === 0
) {
return '-';
}
// 遍历物流信息数组, 显示每个物流的提供商和单号
return (
<div>
{record.fulfillments.map((item, index: number) => (
<div
key={index}
style={{ display: 'flex', flexDirection: 'column' }}
>
<span>
{item.shipping_provider
? `快递方式: ${item.shipping_provider}`
: ''}
</span>
{
item.shipping_method
? `发货方式: ${item.shipping_method}`
: ''
}
<span>
{item.tracking_number
? `物流单号: ${item.tracking_number}`
: ''}
</span>
<span>
{item.date_created ? `发货日期: ${item.date_created}` : ''}
</span>
</div>
))}
</div>
);
},
},
{
title: '操作',
dataIndex: 'option',
@ -298,43 +186,6 @@ const OrdersPage: React.FC = () => {
>
<Button type="text" icon={<EllipsisOutlined />} />
</Dropdown>
<ShipOrderForm
orderId={record.id as number}
tableRef={actionRef}
siteId={siteId}
orderItems={(record as any).line_items?.map((item: any) => ({
id: item.id,
name: item.name,
quantity: item.quantity,
sku: item.sku,
}))}
/>
{record.status === 'completed' && (
<Popconfirm
title="确定取消发货?"
description="取消发货后订单状态将恢复为处理中"
onConfirm={async () => {
try {
const res = await request(
`/site-api/${siteId}/orders/${record.id}/cancel-ship`,
{ method: 'POST' },
);
if (res.success) {
message.success('取消发货成功');
actionRef.current?.reload();
} else {
message.error(res.message || '取消发货失败');
}
} catch (e) {
message.error('取消发货失败');
}
}}
>
<Button type="link" danger title="取消发货">
</Button>
</Popconfirm>
)}
<Popconfirm
title="确定删除订单?"
onConfirm={async () => {
@ -380,7 +231,6 @@ const OrdersPage: React.FC = () => {
pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000'],
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10,
}}
toolBarRender={() => [
@ -391,7 +241,6 @@ const OrdersPage: React.FC = () => {
setSelectedRowKeys={setSelectedRowKeys}
siteId={siteId}
/>,
<Button disabled></Button>,
<Button
title="批量删除"
danger
@ -472,7 +321,18 @@ const OrdersPage: React.FC = () => {
</ModalForm>,
]}
request={async (params, sort, filter) => {
const { current, pageSize, date, status, ...rest } = params;
const p: any = params || {};
const current = p.current;
const pageSize = p.pageSize;
const date = p.date;
const status = p.status;
const {
current: _c,
pageSize: _ps,
date: _d,
status: _s,
...rest
} = p;
const where: Record<string, any> = { ...(filter || {}), ...rest };
if (status && status !== 'all') {
where.status = status;
@ -493,9 +353,9 @@ const OrdersPage: React.FC = () => {
const response = await request(`/site-api/${siteId}/orders`, {
params: {
page: current,
per_page: pageSize,
page_size: pageSize,
where,
...(orderObj ? { orderBy: orderObj } : {}),
...(orderObj ? { order: orderObj } : {}),
},
});
@ -547,12 +407,13 @@ const OrdersPage: React.FC = () => {
return { status: key, count: 0 };
}
try {
const res = await request(
`/site-api/${siteId}/orders/count`,
{
params: { ...baseWhere, status: rawStatus },
const res = await request(`/site-api/${siteId}/orders`, {
params: {
page: 1,
per_page: 1,
where: { ...baseWhere, status: rawStatus },
},
);
});
const totalCount = Number(res?.data?.total || 0);
return { status: key, count: totalCount };
} catch (err) {

View File

@ -116,6 +116,7 @@ const ProductsPage: React.FC = () => {
// ID
title: 'ID',
dataIndex: 'id',
hideInSearch: true,
width: 120,
copyable: true,
render: (_, record) => {
@ -155,7 +156,7 @@ const ProductsPage: React.FC = () => {
},
{
// 库存
title: '库存数量',
title: '库存',
dataIndex: 'stock_quantity',
hideInSearch: true,
},
@ -187,9 +188,9 @@ const ProductsPage: React.FC = () => {
<strong>:</strong> {record.erpProduct.category.name}
</div>
)}
<div>
<strong>:</strong> {record.erpProduct.stock_quantity ?? '-'}
</div>
<div>
<strong>:</strong> {record.erpProduct.stock_quantity ?? '-'}
</div>
</div>
);
}
@ -393,7 +394,6 @@ const ProductsPage: React.FC = () => {
pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10,
}}
actionRef={actionRef}
@ -422,7 +422,7 @@ const ProductsPage: React.FC = () => {
params: {
page,
per_page: pageSize,
where,
...where,
...(orderObj
? {
sortField: Object.keys(orderObj)[0],

View File

@ -1,12 +1,4 @@
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;
import React from 'react';
interface ReviewFormProps {
open: boolean;
@ -23,161 +15,19 @@ const ReviewForm: React.FC<ReviewFormProps> = ({
onClose,
onSuccess,
}) => {
const [form] = Form.useForm();
// 当编辑状态改变时,重置表单数据
useEffect(() => {
if (editing) {
form.setFieldsValue({
product_id: editing.product_id,
author: editing.author,
email: editing.email,
content: editing.content,
rating: editing.rating,
status: editing.status,
});
} else {
form.resetFields();
}
}, [editing, form]);
// 处理表单提交
const handleSubmit = async (values: any) => {
try {
let response;
if (editing) {
// 更新评论
response = await siteapicontrollerUpdatereview(
{
siteId,
id: editing.id,
},
{
review: values.content,
rating: values.rating,
status: values.status,
},
);
} else {
// 创建新评论
response = await siteapicontrollerCreatereview(
{
siteId,
},
{
product_id: values.product_id,
review: values.content,
rating: values.rating,
author: values.author,
author_email: values.email,
},
);
}
if (response.success) {
message.success(editing ? '更新成功' : '创建成功');
onSuccess();
onClose();
form.resetFields();
} else {
message.error(response.message || '操作失败');
}
} catch (error) {
console.error('提交评论表单失败:', error);
message.error('提交失败,请重试');
}
};
// // 这是一个临时的占位符组件
// // 你可以在这里实现表单逻辑
if (!open) {
return null;
}
return (
<Modal
title={editing ? '编辑评论' : '新建评论'}
open={open}
onCancel={onClose}
onOk={() => form.submit()}
okText="保存"
cancelText="取消"
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
status: 'approved',
rating: 5,
}}
>
{!editing && (
<>
<Form.Item
name="product_id"
label="产品ID"
rules={[{ required: true, message: '请输入产品ID' }]}
>
<Input placeholder="请输入产品ID" />
</Form.Item>
<Form.Item
name="author"
label="评论者"
rules={[{ required: true, message: '请输入评论者姓名' }]}
>
<Input placeholder="请输入评论者姓名" />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
</>
)}
<Form.Item
name="content"
label="评论内容"
rules={[{ required: true, message: '请输入评论内容' }]}
>
<TextArea
rows={4}
placeholder="请输入评论内容"
maxLength={1000}
showCount
/>
</Form.Item>
<Form.Item
name="rating"
label="评分"
rules={[{ required: true, message: '请选择评分' }]}
>
<InputNumber
min={1}
max={5}
precision={0}
placeholder="评分 (1-5)"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select placeholder="请选择状态">
<Option value="approved"></Option>
<Option value="pending"></Option>
<Option value="spam"></Option>
<Option value="trash"></Option>
</Select>
</Form.Item>
</Form>
</Modal>
<div>
<h2>Review Form</h2>
<p>Site ID: {siteId}</p>
<p>Editing: {editing ? 'Yes' : 'No'}</p>
<button onClick={onClose}>Close</button>
</div>
);
};

View File

@ -85,52 +85,17 @@ const ReviewsPage: React.FC = () => {
columns={columns}
actionRef={actionRef}
request={async (params) => {
try {
const response = await siteapicontrollerGetreviews({
...params,
siteId,
page: params.current,
per_page: params.pageSize,
});
// 确保 response.data 存在
if (!response || !response.data) {
return {
data: [],
success: true,
total: 0,
};
}
// 确保 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,
// 如果 product_id 是对象,转换为字符串
product_id:
typeof item.product_id === 'object'
? JSON.stringify(item.product_id)
: item.product_id,
}));
return {
data: processedItems,
success: true,
total: Number(response.data.total) || 0,
};
} catch (error) {
console.error('获取评论失败:', error);
return {
data: [],
success: true,
total: 0,
};
}
const response = await siteapicontrollerGetreviews({
...params,
siteId,
page: params.current,
per_page: params.pageSize,
});
return {
data: response.data.items,
success: true,
total: response.data.total,
};
}}
rowKey="id"
search={{

View File

@ -1,332 +0,0 @@
import {
siteapicontrollerCreatewebhook,
siteapicontrollerDeletewebhook,
siteapicontrollerGetwebhooks,
siteapicontrollerUpdatewebhook,
} from '@/servers/api/siteApi';
import {
ActionType,
ProCard,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { useParams } from '@umijs/max';
import {
Button,
Form,
Input,
message,
Modal,
Popconfirm,
Select,
Space,
} from 'antd';
import React, { useRef, useState } from 'react';
const WebhooksPage: React.FC = () => {
const params = useParams();
const siteId = Number(params.siteId);
const actionRef = useRef<ActionType>();
// 模态框状态
const [isModalVisible, setIsModalVisible] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [currentWebhook, setCurrentWebhook] =
useState<API.UnifiedWebhookDTO | null>(null);
// 表单实例
const [form] = Form.useForm();
// webhook主题选项
const webhookTopics = [
{ label: '订单创建', value: 'order.created' },
{ label: '订单更新', value: 'order.updated' },
{ label: '订单删除', value: 'order.deleted' },
{ label: '产品创建', value: 'product.created' },
{ label: '产品更新', value: 'product.updated' },
{ label: '产品删除', value: 'product.deleted' },
{ label: '客户创建', value: 'customer.created' },
{ label: '客户更新', value: 'customer.updated' },
{ label: '客户删除', value: 'customer.deleted' },
];
// webhook状态选项
const webhookStatuses = [
{ label: '活跃', value: 'active' },
{ label: '非活跃', value: 'inactive' },
];
// 打开新建模态框
const showCreateModal = () => {
setIsEditMode(false);
setCurrentWebhook(null);
form.resetFields();
setIsModalVisible(true);
};
// 打开编辑模态框
const showEditModal = async (record: API.UnifiedWebhookDTO) => {
setIsEditMode(true);
setCurrentWebhook(record);
try {
// 如果需要获取最新的webhook数据可以取消下面的注释
// const response = await siteapicontrollerGetwebhook({ siteId, id: String(record.id) });
// if (response.success && response.data) {
// form.setFieldsValue(response.data);
// } else {
// form.setFieldsValue(record);
// }
form.setFieldsValue(record);
setIsModalVisible(true);
} catch (error) {
message.error('加载webhook数据失败');
}
};
// 关闭模态框
const handleCancel = () => {
setIsModalVisible(false);
form.resetFields();
};
// 提交表单
const handleSubmit = async () => {
try {
const values = await form.validateFields();
// 准备提交数据
const webhookData = {
...values,
siteId,
};
let response;
if (isEditMode && currentWebhook?.id) {
// 更新webhook
response = await siteapicontrollerUpdatewebhook({
...webhookData,
id: String(currentWebhook.id),
});
} else {
// 创建新webhook
response = await siteapicontrollerCreatewebhook(webhookData);
}
if (response.success) {
message.success(isEditMode ? '更新成功' : '创建成功');
setIsModalVisible(false);
form.resetFields();
actionRef.current?.reload();
} else {
message.error(isEditMode ? '更新失败' : '创建失败');
}
} catch (error: any) {
message.error('表单验证失败:' + error.message);
}
};
const columns: ProColumns<API.UnifiedWebhookDTO>[] = [
{ 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: '状态', 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: '操作',
key: 'action',
width: 150,
render: (_, record) => (
<Space>
<Button
type="link"
style={{ padding: 0 }}
onClick={() => showEditModal(record)}
>
</Button>
<Popconfirm
title="确定删除吗?"
onConfirm={async () => {
if (record.id) {
try {
const response = await siteapicontrollerDeletewebhook({
siteId,
id: String(record.id),
});
if (response.success) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error('删除失败');
}
} catch (error) {
message.error('删除失败');
}
}
}}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<>
<ProCard>
<ProTable<API.UnifiedWebhookDTO>
columns={columns}
actionRef={actionRef}
request={async (params) => {
try {
const response = await siteapicontrollerGetwebhooks({
...params,
siteId,
page: params.current,
per_page: params.pageSize,
});
// 确保 response.data 存在
if (!response || !response.data) {
return {
data: [],
success: true,
total: 0,
};
}
// 确保 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,
}));
return {
data: processedItems,
success: true,
total: Number(response.data.total) || 0,
};
} catch (error) {
console.error('获取webhooks失败:', error);
return {
data: [],
success: true,
total: 0,
};
}
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
headerTitle="Webhooks列表"
toolBarRender={() => [
<Button type="primary" onClick={showCreateModal}>
Webhook
</Button>,
]}
/>
</ProCard>
{/* Webhook编辑/新建模态框 */}
<Modal
title={isEditMode ? '编辑Webhook' : '新建Webhook'}
open={isModalVisible}
onCancel={handleCancel}
footer={[
<Button key="back" onClick={handleCancel}>
</Button>,
<Button key="submit" type="primary" onClick={handleSubmit}>
{isEditMode ? '更新' : '创建'}
</Button>,
]}
>
<Form
form={form}
layout="vertical"
initialValues={{
status: 'active',
}}
>
<Form.Item
name="name"
label="名称"
rules={[
{ required: true, message: '请输入webhook名称' },
{ max: 100, message: '名称不能超过100个字符' },
]}
>
<Input placeholder="请输入webhook名称" />
</Form.Item>
<Form.Item
name="topic"
label="主题"
rules={[{ required: true, message: '请选择webhook主题' }]}
>
<Select
placeholder="请选择webhook主题"
options={webhookTopics}
allowClear
/>
</Form.Item>
<Form.Item
name="delivery_url"
label="回调URL"
rules={[
{ required: true, message: '请输入回调URL' },
{ type: 'url', message: '请输入有效的URL' },
]}
>
<Input placeholder="请输入回调URL如:https://example.com/webhook" />
</Form.Item>
<Form.Item
name="secret"
label="密钥(可选)"
rules={[{ max: 255, message: '密钥不能超过255个字符' }]}
>
<Input placeholder="请输入密钥用于验证webhook请求" />
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择webhook状态' }]}
>
<Select placeholder="请选择webhook状态" options={webhookStatuses} />
</Form.Item>
</Form>
</Modal>
</>
);
};
export default WebhooksPage;

View File

@ -47,133 +47,6 @@ const region = {
YT: 'Yukon',
};
// 定义发货订单表单的数据类型
export interface ShipOrderFormData {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: Array<{
id?: string;
quantity?: number;
}>;
}
// 发货订单表单组件
export const ShipOrderForm: React.FC<{
orderId: number;
tableRef?: React.MutableRefObject<ActionType | undefined>;
siteId?: string;
orderItems?: Array<{
id: string;
name: string;
quantity: number;
sku?: string;
}>;
}> = ({ orderId, tableRef, siteId, orderItems }) => {
const { message } = App.useApp();
const formRef = useRef<ProFormInstance>();
return (
<ModalForm
formRef={formRef}
title="发货订单"
width="600px"
modalProps={{ destroyOnHidden: true }}
trigger={
<Button type="link" title="发货">
</Button>
}
onFinish={async (values: ShipOrderFormData) => {
if (!siteId) {
message.error('缺少站点ID');
return false;
}
try {
const { success, message: errMsg } = await request(
`/site-api/${siteId}/orders/${orderId}/ship`,
{
method: 'POST',
data: values,
},
);
if (success === false) {
throw new Error(errMsg || '发货失败');
}
message.success('发货成功');
tableRef?.current?.reload();
return true;
} catch (error: any) {
message.error(error?.message || '发货失败');
return false;
}
}}
onFinishFailed={() => {
const element = document.querySelector('.ant-form-item-explain-error');
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}}
>
<ProFormText
name="tracking_number"
label="物流单号"
placeholder="请输入物流单号"
rules={[{ required: true, message: '请输入物流单号' }]}
/>
<ProFormText
name="shipping_provider"
label="物流公司"
placeholder="请输入物流公司名称"
rules={[{ required: true, message: '请输入物流公司名称' }]}
/>
<ProFormText
name="shipping_method"
label="发货方式"
placeholder="请输入发货方式"
/>
{orderItems && orderItems.length > 0 && (
<ProFormList
label="发货商品项"
name="items"
tooltip="如果不选择,则默认发货所有商品"
>
<ProForm.Group>
<ProFormSelect
name="id"
label="商品"
placeholder="请选择商品"
options={orderItems.map((item) => ({
label: `${item.name} (SKU: ${item.sku || 'N/A'}) - 可发数量: ${
item.quantity
}`,
value: item.id,
}))}
rules={[{ required: true, message: '请选择商品' }]}
/>
<ProFormDigit
name="quantity"
label="发货数量"
placeholder="请输入发货数量"
rules={[{ required: true, message: '请输入发货数量' }]}
fieldProps={{
precision: 0,
min: 1,
}}
/>
</ProForm.Group>
</ProFormList>
)}
</ModalForm>
);
};
export const OrderNote: React.FC<{
id: number;
descRef?: React.MutableRefObject<ActionType | undefined>;

View File

@ -69,12 +69,12 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
onFinish={handleBind}
>
<div style={{ marginBottom: 16 }}>
<strong>:</strong>
<strong></strong>
<div>SKU: {siteProduct.sku}</div>
<div>: {siteProduct.name}</div>
{siteProduct.erpProduct && (
<div style={{ color: '#ff4d4f' }}>
ERP产品:{siteProduct.erpProduct.sku} -{' '}
ERP产品{siteProduct.erpProduct.sku} -{' '}
{siteProduct.erpProduct.name}
</div>
)}
@ -148,7 +148,7 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}}
toolBarRender={false}
options={false}
@ -164,7 +164,7 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
border: '1px solid #b7eb8f',
}}
>
<strong>:</strong>
<strong></strong>
<div>SKU: {selectedProduct.sku}</div>
<div>: {selectedProduct.name}</div>
{selectedProduct.nameCn && (

View File

@ -613,6 +613,18 @@ export const UpdateVaritation: React.FC<{
);
};
// ... SetComponent, BatchEditProducts, BatchDeleteProducts, ImportCsv ...
// I will keep them but comment out/disable parts that rely on old API if I can't easily fix them all.
// BatchEdit/Delete rely on old API.
// I'll comment out their usage in ProductsPage or just return null here.
// I'll keep them but they might break if used.
// Since I removed them from ProductsPage toolbar (Wait, I kept them in ProductsPage toolbar!), I should update them or remove them.
// I'll update BatchDelete to use new API (loop delete).
// BatchEdit? `wpproductcontrollerBatchUpdateProducts`.
// I don't have batch update in my new API.
// I'll remove BatchEdit from ProductsPage toolbar for now or implement batch update in Controller.
// I'll update BatchDelete.
export const BatchDeleteProducts: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
selectedRowKeys: React.Key[];

View File

@ -19,10 +19,9 @@ import {
} from '@ant-design/pro-components';
import { Button, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import ReactECharts from 'echarts-for-react';
import * as countries from 'i18n-iso-countries';
import { useEffect, useMemo, useRef, useState } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.extend(weekOfYear);
const highlightText = (text: string, keyword: string) => {
@ -39,17 +38,6 @@ const highlightText = (text: string, keyword: string) => {
);
};
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
const ListPage: React.FC = () => {
const [xAxis, setXAxis] = useState([]);
const [series, setSeries] = useState<any[]>([]);
@ -142,7 +130,7 @@ const ListPage: React.FC = () => {
});
if (success) {
const res = data?.sort(() => -1);
const formatMap = {
const formatMap = {
month: 'YYYY-MM',
week: 'YYYY年第WW周',
day: 'YYYY-MM-DD',
@ -150,12 +138,10 @@ const ListPage: React.FC = () => {
const format = formatMap[params.grouping] || 'YYYY-MM-DD';
if (params.grouping === 'week') {
setXAxis(
res?.map((v) => {
const [year, week] = v.order_date.split('-');
return `${year}年第${week}`;
}),
);
setXAxis(res?.map((v) => {
const [year, week] = v.order_date.split('-');
return `${year}年第${week}`;
}));
} else {
setXAxis(res?.map((v) => dayjs(v.order_date).format(format)));
}
@ -613,7 +599,7 @@ const ListPage: React.FC = () => {
name="date"
/>
{/* <ProFormText label="关键词" name="keyword" /> */}
<ProFormSelect
<ProFormSelect
label="统计周期"
name="grouping"
initialValue="day"
@ -634,18 +620,6 @@ const ListPage: React.FC = () => {
}));
}}
/>
<ProFormSelect
name="country"
label="区域"
mode="multiple"
placeholder="请选择区域"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
/>
{/* <ProFormSelect
label="类型"
name="purchaseType"

View File

@ -2,37 +2,25 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
statisticscontrollerGetinativeusersbymonth,
statisticscontrollerGetordersource,
statisticscontrollerGetordersorce,
} from '@/servers/api/statistics';
import {
ActionType,
PageContainer,
ProColumns,
ProForm,
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { Space, Tag } from 'antd';
import dayjs from 'dayjs';
import ReactECharts from 'echarts-for-react';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
import { HistoryOrder } from '../Order';
countries.registerLocale(zhCN);
const ListPage: React.FC = () => {
const [data, setData] = useState({});
const initialValues = {
country: ['CA'],
};
function handleSubmit(values: typeof initialValues) {
statisticscontrollerGetordersource({ params: values }).then(
({ data, success }) => {
if (success) setData(data);
},
);
}
useEffect(() => {
handleSubmit(initialValues);
statisticscontrollerGetordersorce().then(({ data, success }) => {
if (success) setData(data);
});
}, []);
const option = useMemo(() => {
@ -51,17 +39,11 @@ const ListPage: React.FC = () => {
data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
label: {
show: true,
formatter: function (params) {
formatter: function (params) {
if (!params.value) return '';
return (
Math.abs(params.value) +
'\n' +
Math.abs(
data?.inactiveRes?.find(
(item) => item.order_month === params.name,
)?.new_user_total || 0,
)
);
return Math.abs(params.value)
+'\n'
+Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.new_user_total || 0);
},
color: '#000000',
},
@ -77,17 +59,11 @@ const ListPage: React.FC = () => {
data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
label: {
show: true,
formatter: function (params) {
formatter: function (params) {
if (!params.value) return '';
return (
Math.abs(params.value) +
'\n' +
Math.abs(
data?.inactiveRes?.find(
(item) => item.order_month === params.name,
)?.old_user_total || 0,
)
);
return Math.abs(params.value)
+'\n'
+Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.old_user_total || 0);
},
color: '#000000',
},
@ -107,18 +83,11 @@ const ListPage: React.FC = () => {
show: true,
formatter: function (params) {
if (!params.value) return '';
return (
Math.abs(params.value) +
'\n' +
+Math.abs(
data?.res?.find(
(item) =>
item.order_month === params.name &&
item.first_order_month_group === v,
)?.total || 0,
)
);
},
return Math.abs(params.value)
+'\n'+
+Math.abs(data?.res?.find((item) => item.order_month === params.name &&
item.first_order_month_group === v)?.total || 0);
},
color: '#000000',
},
data: xAxisData.map((month) => {
@ -143,6 +112,7 @@ const ListPage: React.FC = () => {
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series',
@ -290,26 +260,8 @@ const ListPage: React.FC = () => {
},
},
];
return (
<PageContainer ghost>
<ProForm
initialValues={initialValues}
layout="inline"
onFinish={handleSubmit}
>
<ProFormSelect
name="country"
label="区域"
mode="multiple"
placeholder="请选择区域"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
/>
</ProForm>
<ReactECharts
option={option}
style={{ height: 1050 }}
@ -326,7 +278,6 @@ const ListPage: React.FC = () => {
},
}}
/>
{tableData?.length ? (
<ProTable
search={false}
@ -343,15 +294,4 @@ const ListPage: React.FC = () => {
);
};
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
export default ListPage;

View File

@ -1,249 +0,0 @@
import { ordercontrollerGetordersales } from '@/servers/api/order';
import { sitecontrollerAll } from '@/servers/api/site';
import {
ActionType,
PageContainer,
ProColumns,
ProFormSwitch,
ProTable,
} from '@ant-design/pro-components';
import { Button } from 'antd';
import dayjs from 'dayjs';
import { saveAs } from 'file-saver';
import { useRef, useState } from 'react';
import * as XLSX from 'xlsx';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const formRef = useRef();
const [total, setTotal] = useState(0);
const [isSource, setIsSource] = useState(false);
const [yooneTotal, setYooneTotal] = useState({});
const columns: ProColumns<API.OrderSaleDTO>[] = [
{
title: '时间段',
dataIndex: 'dateRange',
valueType: 'dateTimeRange',
hideInTable: true,
formItemProps: {
rules: [
{
required: true,
message: '请选择时间段',
},
],
},
},
{
title: '排除套装',
dataIndex: 'exceptPackage',
valueType: 'switch',
hideInTable: true,
},
{
title: '产品名称',
dataIndex: 'sku',
},
{
title: '产品名称',
dataIndex: 'name',
},
{
title: '站点',
dataIndex: 'siteId',
valueType: 'select',
request: async () => {
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
hideInTable: true,
},
// {
// title: '分类',
// dataIndex: 'categoryName',
// hideInSearch: true,
// hideInTable: isSource,
// },
{
title: '数量',
dataIndex: 'totalQuantity',
hideInSearch: true,
},
{
title: '一单订单数',
dataIndex: 'firstOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.firstOrderCount;
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
},
},
{
title: '两单订单数',
dataIndex: 'secondOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.secondOrderCount;
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
},
},
{
title: '三单订单数',
dataIndex: 'thirdOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.thirdOrderCount;
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
},
},
{
title: '三单以上订单数',
dataIndex: 'moreThirdOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.moreThirdOrderCount;
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
},
},
{
title: '订单数',
dataIndex: 'totalOrders',
hideInSearch: true,
},
];
return (
<PageContainer ghost>
<ProTable
headerTitle="查询表格"
actionRef={actionRef}
formRef={formRef}
rowKey="id"
params={{ isSource }}
form={{
// ignoreRules: false,
initialValues: {
dateRange: [dayjs().startOf('month'), dayjs().endOf('month')],
},
}}
request={async ({ dateRange, ...param }) => {
const [startDate, endDate] = dateRange.values();
const { data, success } = await ordercontrollerGetordersales({
startDate,
endDate,
...param,
});
if (success) {
setTotal(data?.totalQuantity || 0);
setYooneTotal({
yoone3Quantity: data?.yoone3Quantity || 0,
yoone6Quantity: data?.yoone6Quantity || 0,
yoone9Quantity: data?.yoone9Quantity || 0,
yoone12Quantity: data?.yoone12Quantity || 0,
yoone12QuantityNew: data?.yoone12QuantityNew || 0,
yoone15Quantity: data?.yoone15Quantity || 0,
yoone18Quantity: data?.yoone18Quantity || 0,
zexQuantity: data?.zexQuantity || 0,
});
return {
total: data?.total || 0,
data: data?.items || [],
};
}
setTotal(0);
setYooneTotal({});
return {
data: [],
};
}}
columns={columns}
dateFormatter="number"
footer={() => `总计: ${total}`}
toolBarRender={() => [
<Button
type="primary"
onClick={async () => {
const { dateRange, param } = formRef.current?.getFieldsValue();
const [startDate, endDate] = dateRange.values();
const { data, success } = await ordercontrollerGetordersales({
startDate: dayjs(startDate).valueOf(),
endDate: dayjs(endDate).valueOf(),
...param,
current: 1,
pageSize: 20000,
});
if (!success) return;
// 表头
const headers = ['产品名', '数量'];
// 数据行
const rows = (data?.items || []).map((item) => {
return [item.name, item.totalQuantity];
});
// 导出
const sheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
const book = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(book, sheet, '销售');
const buffer = XLSX.write(book, {
bookType: 'xlsx',
type: 'array',
});
const blob = new Blob([buffer], {
type: 'application/octet-stream',
});
saveAs(blob, '销售.xlsx');
}}
>
</Button>,
<ProFormSwitch
label="原产品"
fieldProps={{
value: isSource,
onChange: () => setIsSource(!isSource),
}}
/>,
]}
/>
<div
style={{
background: '#fff',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
padding: '10px',
marginTop: '20px',
}}
>
<div>
YOONE:{' '}
{(yooneTotal.yoone3Quantity || 0) +
(yooneTotal.yoone6Quantity || 0) +
(yooneTotal.yoone9Quantity || 0) +
(yooneTotal.yoone12Quantity || 0) +
(yooneTotal.yoone15Quantity || 0) +
(yooneTotal.yoone18Quantity || 0) +
(yooneTotal.zexQuantity || 0)}
</div>
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
<div>
YOONE 12MG白:{' '}
{(yooneTotal.yoone12Quantity || 0) -
(yooneTotal.yoone12QuantityNew || 0)}
</div>
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>
</div>
</PageContainer>
);
};
export default ListPage;

View File

@ -41,10 +41,6 @@ const ListPage: React.FC = () => {
valueType: 'switch',
hideInTable: true,
},
{
title: '产品名称',
dataIndex: 'sku',
},
{
title: '产品名称',
dataIndex: 'name',
@ -77,31 +73,37 @@ const ListPage: React.FC = () => {
title: '一单订单数',
dataIndex: 'firstOrderCount',
hideInSearch: true,
},
{
title: '一单YOONE盒数',
dataIndex: 'firstOrderYOONEBoxCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.firstOrderCount;
return `${record.firstOrderCount}(${record.firstOrderYOONEBoxCount})`;
},
},
{
title: '两单订单数',
dataIndex: 'secondOrderCount',
hideInSearch: true,
},
{
title: '两单YOONE盒数',
dataIndex: 'secondOrderYOONEBoxCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.secondOrderCount;
return `${record.secondOrderCount}(${record.secondOrderYOONEBoxCount})`;
},
},
{
title: '三单订单数',
dataIndex: 'thirdOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.thirdOrderCount;
return `${record.thirdOrderCount}(${record.thirdOrderYOONEBoxCount})`;
},
},
{
title: '三单YOONE盒数',
dataIndex: 'thirdOrderYOONEBoxCount',
title: '三单以上订单数',
dataIndex: 'moreThirdOrderCount',
hideInSearch: true,
render(_, record) {
if (isSource) return record.moreThirdOrderCount;
return `${record.moreThirdOrderCount}(${record.moreThirdOrderYOONEBoxCount})`;
},
},
{
title: '订单数',

View File

@ -15,31 +15,16 @@ import {
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
import * as countries from 'i18n-iso-countries';
import zhCN from 'i18n-iso-countries/langs/zh';
import { useRef } from 'react';
// 初始化中文语言包
countries.registerLocale(zhCN);
// 区域数据项类型
interface AreaItem {
code: string;
name: string;
}
// 获取所有国家/地区的选项
const getCountryOptions = () => {
// 获取所有国家的 ISO 代码
const countryCodes = countries.getAlpha2Codes();
// 将国家代码转换为选项数组
return Object.keys(countryCodes).map((code) => ({
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
value: code,
}));
};
const ListPage: React.FC = () => {
const { message } = App.useApp();
const actionRef = useRef<ActionType>();
@ -205,11 +190,23 @@ const CreateForm: React.FC<{
width="lg"
mode="multiple"
placeholder="留空表示全球"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
request={async () => {
try {
const resp = await request('/area', {
method: 'GET',
params: { pageSize: 1000 },
});
if (resp.success) {
return resp.data.list.map((area: AreaItem) => ({
label: area.name,
value: area.code,
}));
}
return [];
} catch (e) {
return [];
}
}}
/>
</ProForm.Group>
</DrawerForm>
@ -292,11 +289,23 @@ const UpdateForm: React.FC<{
width="lg"
mode="multiple"
placeholder="留空表示全球"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={getCountryOptions()}
request={async () => {
try {
const resp = await request('/area', {
method: 'GET',
params: { pageSize: 1000 },
});
if (resp.success) {
return resp.data.list.map((area: AreaItem) => ({
label: area.name,
value: area.code,
}));
}
return [];
} catch (e) {
return [];
}
}}
/>
</ProForm.Group>
</DrawerForm>

View File

@ -161,10 +161,7 @@ const OrdersPage: React.FC = () => {
rowKey="id"
columns={columns}
request={request}
pagination={{
showSizeChanger: true,
showQuickJumper: true,
}}
pagination={{ showSizeChanger: true }}
search={{
labelWidth: 90,
span: 6,

View File

@ -2,85 +2,133 @@ import {
templatecontrollerCreatetemplate,
templatecontrollerDeletetemplate,
templatecontrollerGettemplatelist,
templatecontrollerRendertemplatedirect,
templatecontrollerRendertemplate,
templatecontrollerUpdatetemplate,
} from '@/servers/api/template';
import { EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { BugOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
ActionType,
DrawerForm,
ModalForm,
PageContainer,
ProColumns,
ProForm,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import Editor from '@monaco-editor/react';
import { App, Button, Card, Popconfirm, Space, Typography } from 'antd';
import { App, Button, Card, Popconfirm, Typography } from 'antd';
import { useEffect, useRef, useState } from 'react';
import ReactJson from 'react-json-view';
// 自定义hook用于处理模板预览逻辑
const useTemplatePreview = () => {
const TestModal: React.FC<{
visible: boolean;
onClose: () => void;
template: API.Template | null;
}> = ({ visible, onClose, template }) => {
const { message } = App.useApp();
const [inputData, setInputData] = useState<Record<string, any>>({});
const [renderedResult, setRenderedResult] = useState<string>('');
const [previewData, setPreviewData] = useState<any>(null);
// 防抖的预览效果
// 当模板改变时,重置数据
useEffect(() => {
if (!previewData || !previewData.value) {
setRenderedResult('请输入模板内容');
return;
if (visible && template) {
// 尝试解析模板中可能的变量作为初始数据(可选优化,这里先置空)
// 或者根据模板类型提供一些默认值
if (template.testData) {
try {
setInputData(JSON.parse(template.testData));
} catch (e) {
console.error('Failed to parse testData:', e);
setInputData({});
}
} else {
setInputData({});
}
setRenderedResult('');
}
}, [visible, template]);
// 监听 inputData 变化并调用渲染 API
useEffect(() => {
if (!visible || !template) return;
const timer = setTimeout(async () => {
let testData = {};
try {
if (previewData.testData) {
testData = JSON.parse(previewData.testData);
}
} catch (e) {
testData = {};
}
try {
// 使用新的直接渲染API传入模板内容和测试数据
const res = await templatecontrollerRendertemplatedirect({
template: previewData.value,
data: testData,
});
const res = await templatecontrollerRendertemplate(
{ name: template.name || '' },
inputData,
);
if (res.success) {
setRenderedResult(res.data as unknown as string);
} else {
setRenderedResult(`错误: ${res.message}`);
setRenderedResult(`Error: ${res.message}`);
}
} catch (error: any) {
setRenderedResult(`错误: ${error.message}`);
setRenderedResult(`Error: ${error.message}`);
}
}, 500); // 防抖 500ms
return () => clearTimeout(timer);
}, [previewData]);
}, [inputData, visible, template]);
// 处理实时预览逻辑
const handlePreview = (_changedValues: any, allValues: any) => {
setPreviewData(allValues);
};
// 手动刷新预览
const refreshPreview = (formValues: any) => {
setPreviewData(formValues);
};
return {
renderedResult,
handlePreview,
refreshPreview,
setPreviewData,
};
return (
<ModalForm
title={`测试模板: ${template?.name || '未知模板'}`}
open={visible}
onOpenChange={(open) => !open && onClose()}
modalProps={{ destroyOnClose: true, onCancel: onClose }}
submitter={false} // 不需要提交按钮
width={800}
>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: 1 }}>
<Typography.Title level={5}> (JSON)</Typography.Title>
<Card bodyStyle={{ padding: 0, height: '300px', overflow: 'auto' }}>
<ReactJson
src={inputData}
onEdit={(edit) =>
setInputData(edit.updated_src as Record<string, any>)
}
onAdd={(add) =>
setInputData(add.updated_src as Record<string, any>)
}
onDelete={(del) =>
setInputData(del.updated_src as Record<string, any>)
}
name={false}
displayDataTypes={false}
/>
</Card>
</div>
<div style={{ flex: 1 }}>
<Typography.Title level={5}></Typography.Title>
<Card
bodyStyle={{
padding: '16px',
height: '300px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
{renderedResult}
</pre>
</Card>
</div>
</div>
</ModalForm>
);
};
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
const [testModalVisible, setTestModalVisible] = useState(false);
const [currentTemplate, setCurrentTemplate] = useState<API.Template | null>(
null,
);
const columns: ProColumns<API.Template>[] = [
{
@ -121,7 +169,17 @@ const List: React.FC = () => {
dataIndex: 'option',
valueType: 'option',
render: (_, record) => (
<Space>
<>
<Button
type="link"
icon={<BugOutlined />}
onClick={() => {
setCurrentTemplate(record);
setTestModalVisible(true);
}}
>
</Button>
<UpdateForm tableRef={actionRef} values={record} />
<Popconfirm
title="删除"
@ -140,7 +198,7 @@ const List: React.FC = () => {
</Button>
</Popconfirm>
</Space>
</>
),
},
];
@ -164,6 +222,11 @@ const List: React.FC = () => {
}}
columns={columns}
/>
<TestModal
visible={testModalVisible}
onClose={() => setTestModalVisible(false)}
template={currentTemplate}
/>
</PageContainer>
);
};
@ -172,14 +235,9 @@ const CreateForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
}> = ({ tableRef }) => {
const { message } = App.useApp();
const [form] = ProForm.useForm();
const { renderedResult, handlePreview, refreshPreview } =
useTemplatePreview();
return (
<DrawerForm<API.CreateTemplateDTO>
title="新建"
form={form}
trigger={
<Button type="primary">
<PlusOutlined />
@ -189,9 +247,7 @@ const CreateForm: React.FC<{
autoFocusFirstInput
drawerProps={{
destroyOnHidden: true,
width: 1200, // 增加抽屉宽度以容纳调试面板
}}
onValuesChange={handlePreview}
onFinish={async (values) => {
try {
await templatecontrollerCreatetemplate(values);
@ -204,101 +260,46 @@ const CreateForm: React.FC<{
}
}}
>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: 1 }}>
<ProFormText
name="name"
label="模板名称"
placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]}
/>
<ProForm.Item
name="value"
label="模板内容"
rules={[{ required: true, message: '请输入模板内容' }]}
>
<Editor
height="400px"
defaultLanguage="html"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</ProForm.Item>
<ProForm.Item
name="testData"
label="测试数据 (JSON)"
rules={[
{
validator: (_: any, value: any) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error('请输入有效的JSON格式'));
}
},
},
]}
>
<Editor
height="200px"
defaultLanguage="json"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
formatOnPaste: true,
formatOnType: true,
}}
/>
</ProForm.Item>
</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>
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => {
// 获取当前表单数据并触发预览
const currentValues = form.getFieldsValue();
refreshPreview(currentValues);
}}
title="手动刷新预览"
/>
</div>
<Card
styles={{
body: {
padding: '16px',
height: '600px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
},
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
{renderedResult || '修改模板或测试数据后将自动预览结果...'}
</pre>
</Card>
</div>
</div>
<ProFormText
name="name"
label="模板名称"
placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]}
/>
<ProForm.Item
name="value"
label="值"
rules={[{ required: true, message: '请输入值' }]}
>
<Editor
height="500px"
defaultLanguage="html"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</ProForm.Item>
<ProFormTextArea
name="testData"
label="测试数据 (JSON)"
placeholder="请输入JSON格式的测试数据"
rules={[
{
validator: (_, value) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error('请输入有效的JSON格式'));
}
},
},
]}
/>
</DrawerForm>
);
};
@ -308,25 +309,9 @@ const UpdateForm: React.FC<{
values: API.Template;
}> = ({ tableRef, values: initialValues }) => {
const { message } = App.useApp();
const [form] = ProForm.useForm();
const { renderedResult, handlePreview, refreshPreview, setPreviewData } =
useTemplatePreview();
// 组件挂载时初始化预览数据
useEffect(() => {
if (initialValues) {
setPreviewData({
name: initialValues.name,
value: initialValues.value,
testData: initialValues.testData,
});
}
}, [initialValues, setPreviewData]);
return (
<DrawerForm<API.UpdateTemplateDTO>
title="编辑"
form={form}
initialValues={initialValues}
trigger={
<Button type="primary">
@ -337,9 +322,7 @@ const UpdateForm: React.FC<{
autoFocusFirstInput
drawerProps={{
destroyOnHidden: true,
width: 1200, // 增加抽屉宽度以容纳调试面板
}}
onValuesChange={handlePreview}
onFinish={async (values) => {
if (!initialValues.id) return false;
try {
@ -356,101 +339,46 @@ const UpdateForm: React.FC<{
}
}}
>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: 1 }}>
<ProFormText
name="name"
label="模板名称"
placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]}
/>
<ProForm.Item
name="value"
label="模板内容"
rules={[{ required: true, message: '请输入模板内容' }]}
>
<Editor
height="400px"
defaultLanguage="html"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</ProForm.Item>
<ProForm.Item
name="testData"
label="测试数据 (JSON)"
rules={[
{
validator: (_: any, value: any) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error('请输入有效的JSON格式'));
}
},
},
]}
>
<Editor
height="200px"
defaultLanguage="json"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
formatOnPaste: true,
formatOnType: true,
}}
/>
</ProForm.Item>
</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>
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => {
// 获取当前表单数据并触发预览
const currentValues = form.getFieldsValue();
refreshPreview(currentValues);
}}
title="手动刷新预览"
/>
</div>
<Card
styles={{
body: {
padding: '16px',
height: '600px',
overflow: 'auto',
backgroundColor: '#f5f5f5',
},
}}
>
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
{renderedResult || '修改模板或测试数据后将自动预览结果...'}
</pre>
</Card>
</div>
</div>
<ProFormText
name="name"
label="模板名称"
placeholder="请输入名称"
rules={[{ required: true, message: '请输入名称' }]}
/>
<ProForm.Item
name="value"
label="值"
rules={[{ required: true, message: '请输入值' }]}
>
<Editor
height="500px"
defaultLanguage="html"
options={{
minimap: { enabled: false },
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</ProForm.Item>
<ProFormTextArea
name="testData"
label="测试数据 (JSON)"
placeholder="请输入JSON格式的测试数据"
rules={[
{
validator: (_, value) => {
if (!value) return Promise.resolve();
try {
JSON.parse(value);
return Promise.resolve();
} catch (e) {
return Promise.reject(new Error('请输入有效的JSON格式'));
}
},
},
]}
/>
</DrawerForm>
);
};

View File

@ -221,8 +221,6 @@ 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: [],
@ -239,37 +237,22 @@ const WpToolPage: React.FC = () => {
useEffect(() => {
const fetchAllConfigs = async () => {
try {
message.loading({
content: '正在加载字典配置...',
key: 'loading-config',
});
// 1. 获取所有字典列表以找到对应的 ID
const dictListResponse = await request('/dict/list');
// 处理后端统一响应格式
const dictList = dictListResponse?.data || dictListResponse || [];
const dictList = await request('/dict/list');
// 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 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);
const dict = dictList.find((d: any) => d.name === dictName);
if (!dict) {
console.warn(`Dictionary ${dictName} not found`);
return [];
}
const res = await request('/dict/items', {
params: { dictId: dict.id },
});
return res.map((item: any) => item.name);
};
// 3. 并行获取所有字典项
const [
brands,
fruitKeys,
@ -281,9 +264,9 @@ const WpToolPage: React.FC = () => {
categoryKeys,
] = await Promise.all([
getItems('brand'),
getItems('fruit'),
getItems('mint'),
getItems('flavor'),
getItems('fruit'), // 假设字典名为 fruit
getItems('mint'), // 假设字典名为 mint
getItems('flavor'), // 假设字典名为 flavor
getItems('strength'),
getItems('size'),
getItems('humidity'),
@ -300,28 +283,11 @@ 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({
content: '获取字典配置失败,请刷新页面重试',
key: 'loading-config',
});
message.error('获取字典配置失败');
}
};

View File

@ -23,13 +23,13 @@ export async function categorycontrollerGetlist(
/** 此处后端没有提供注释 POST /category/ */
export async function categorycontrollerCreate(
body: API.CreateCategoryDTO,
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<any>('/category/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
@ -40,14 +40,14 @@ export async function categorycontrollerCreate(
export async function categorycontrollerUpdate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.categorycontrollerUpdateParams,
body: API.UpdateCategoryDTO,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/category/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,

View File

@ -2,68 +2,6 @@
/* eslint-disable */
import { request } from 'umi';
/** 此处后端没有提供注释 POST /customer/ */
export async function customercontrollerCreatecustomer(
body: API.CreateCustomerDTO,
options?: { [key: string]: any },
) {
return request<API.GetCustomerDTO>('/customer/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /customer/${param0} */
export async function customercontrollerGetcustomerbyid(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.customercontrollerGetcustomerbyidParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /customer/${param0} */
export async function customercontrollerUpdatecustomer(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.customercontrollerUpdatecustomerParams,
body: API.UpdateCustomerDTO,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 DELETE /customer/${param0} */
export async function customercontrollerDeletecustomer(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.customercontrollerDeletecustomerParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<Record<string, any>>(`/customer/${param0}`, {
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/addtag */
export async function customercontrollerAddtag(
body: API.CustomerTagDTO,
@ -79,51 +17,6 @@ export async function customercontrollerAddtag(
});
}
/** 此处后端没有提供注释 PUT /customer/batch */
export async function customercontrollerBatchupdatecustomers(
body: API.BatchUpdateCustomerDTO,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/batch', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/batch */
export async function customercontrollerBatchcreatecustomers(
body: API.BatchCreateCustomerDTO,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 DELETE /customer/batch */
export async function customercontrollerBatchdeletecustomers(
body: API.BatchDeleteCustomerDTO,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/batch', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/deltag */
export async function customercontrollerDeltag(
body: API.CustomerTagDTO,
@ -139,6 +32,21 @@ export async function customercontrollerDeltag(
});
}
/** 此处后端没有提供注释 GET /customer/getcustomerlist */
export async function customercontrollerGetcustomerlist(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.customercontrollerGetcustomerlistParams,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/getcustomerlist', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /customer/gettags */
export async function customercontrollerGettags(options?: {
[key: string]: any;
@ -149,16 +57,6 @@ export async function customercontrollerGettags(options?: {
});
}
/** 此处后端没有提供注释 GET /customer/list */
export async function customercontrollerGetcustomerlist(options?: {
[key: string]: any;
}) {
return request<API.ApiResponse>('/customer/list', {
method: 'GET',
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/setrate */
export async function customercontrollerSetrate(
body: Record<string, any>,
@ -173,28 +71,3 @@ export async function customercontrollerSetrate(
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /customer/statistic/list */
export async function customercontrollerGetcustomerstatisticlist(options?: {
[key: string]: any;
}) {
return request<Record<string, any>>('/customer/statistic/list', {
method: 'GET',
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /customer/sync */
export async function customercontrollerSynccustomers(
body: API.SyncCustomersDTO,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/customer/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

View File

@ -151,27 +151,12 @@ export async function dictcontrollerDeletedictitem(
});
}
/** 此处后端没有提供注释 GET /dict/item/export */
export async function dictcontrollerExportdictitems(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.dictcontrollerExportdictitemsParams,
options?: { [key: string]: any },
) {
return request<any>('/dict/item/export', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /dict/item/import */
export async function dictcontrollerImportdictitems(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<API.ApiResponse>('/dict/item/import', {
return request<any>('/dict/item/import', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',

View File

@ -8,6 +8,7 @@ import * as customer from './customer';
import * as dict from './dict';
import * as locales from './locales';
import * as logistics from './logistics';
import * as media from './media';
import * as order from './order';
import * as product from './product';
import * as site from './site';
@ -18,6 +19,7 @@ import * as subscription from './subscription';
import * as template from './template';
import * as user from './user';
import * as webhook from './webhook';
import * as wpProduct from './wpProduct';
export default {
area,
category,
@ -25,6 +27,7 @@ export default {
dict,
locales,
logistics,
media,
order,
product,
siteApi,
@ -35,4 +38,5 @@ export default {
template,
user,
webhook,
wpProduct,
};

92
src/servers/api/media.ts Normal file
View File

@ -0,0 +1,92 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 此处后端没有提供注释 DELETE /media/${param0} */
export async function mediacontrollerDelete(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.mediacontrollerDeleteParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/media/${param0}`, {
method: 'DELETE',
params: {
...queryParams,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /media/list */
export async function mediacontrollerList(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.mediacontrollerListParams,
options?: { [key: string]: any },
) {
return request<any>('/media/list', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /media/update/${param0} */
export async function mediacontrollerUpdate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.mediacontrollerUpdateParams,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/media/update/${param0}`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /media/upload */
export async function mediacontrollerUpload(
body: {},
files?: File[],
options?: { [key: string]: any },
) {
const formData = new FormData();
if (files) {
files.forEach((f) => formData.append('files', f || ''));
}
Object.keys(body).forEach((ele) => {
const item = (body as any)[ele];
if (item !== undefined && item !== null) {
if (typeof item === 'object' && !(item instanceof File)) {
if (item instanceof Array) {
item.forEach((f) => formData.append(ele, f || ''));
} else {
formData.append(
ele,
new Blob([JSON.stringify(item)], { type: 'application/json' }),
);
}
} else {
formData.append(ele, item);
}
}
});
return request<any>('/media/upload', {
method: 'POST',
data: formData,
requestType: 'form',
...(options || {}),
});
}

View File

@ -59,21 +59,6 @@ export async function ordercontrollerCreatenote(
});
}
/** 此处后端没有提供注释 POST /order/export */
export async function ordercontrollerExportorder(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<any>('/order/export', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /order/getOrderByNumber */
export async function ordercontrollerGetorderbynumber(
body: string,
@ -192,6 +177,20 @@ export async function ordercontrollerCreateorder(
});
}
/** 此处后端没有提供注释 PUT /order/order/export/${param0} */
export async function ordercontrollerExportorder(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.ordercontrollerExportorderParams,
options?: { [key: string]: any },
) {
const { ids: param0, ...queryParams } = params;
return request<any>(`/order/order/export/${param0}`, {
method: 'PUT',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /order/order/pengding/items */
export async function ordercontrollerPengdingitems(
body: Record<string, any>,
@ -240,21 +239,16 @@ export async function ordercontrollerChangestatus(
});
}
/** 此处后端没有提供注释 POST /order/sync/${param0} */
export async function ordercontrollerSyncorders(
/** 此处后端没有提供注释 POST /order/syncOrder/${param0} */
export async function ordercontrollerSyncorder(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.ordercontrollerSyncordersParams,
body: Record<string, any>,
params: API.ordercontrollerSyncorderParams,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/order/sync/${param0}`, {
return request<API.BooleanRes>(`/order/syncOrder/${param0}`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
@ -266,14 +260,11 @@ export async function ordercontrollerSyncorderbyid(
options?: { [key: string]: any },
) {
const { orderId: param0, siteId: param1, ...queryParams } = params;
return request<API.SyncOperationResult>(
`/order/syncOrder/${param1}/order/${param0}`,
{
method: 'POST',
params: { ...queryParams },
...(options || {}),
},
);
return request<API.BooleanRes>(`/order/syncOrder/${param1}/order/${param0}`, {
method: 'POST',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */

View File

@ -125,21 +125,6 @@ export async function productcontrollerBindproductsiteskus(
});
}
/** 此处后端没有提供注释 GET /product/all */
export async function productcontrollerGetallproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetallproductsParams,
options?: { [key: string]: any },
) {
return request<API.ProductListRes>('/product/all', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /product/attribute */
export async function productcontrollerGetattributelist(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -244,36 +229,6 @@ export async function productcontrollerBatchdeleteproduct(
});
}
/** 此处后端没有提供注释 POST /product/batch-sync-from-site */
export async function productcontrollerBatchsyncfromsite(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<API.SyncOperationResultDTO>('/product/batch-sync-from-site', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /product/batch-sync-to-site */
export async function productcontrollerBatchsynctosite(
body: API.BatchSyncProductToSiteDTO,
options?: { [key: string]: any },
) {
return request<API.SyncOperationResultDTO>('/product/batch-sync-to-site', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /product/batch-update */
export async function productcontrollerBatchupdateproduct(
body: API.BatchUpdateProductDTO,
@ -378,13 +333,13 @@ export async function productcontrollerGetcategoriesall(options?: {
/** 此处后端没有提供注释 POST /product/category */
export async function productcontrollerCreatecategory(
body: API.CreateCategoryDTO,
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<any>('/product/category', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
@ -395,14 +350,14 @@ export async function productcontrollerCreatecategory(
export async function productcontrollerUpdatecategory(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerUpdatecategoryParams,
body: API.UpdateCategoryDTO,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<any>(`/product/category/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
@ -554,21 +509,6 @@ export async function productcontrollerCompatflavorsall(options?: {
});
}
/** 此处后端没有提供注释 GET /product/grouped */
export async function productcontrollerGetgroupedproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetgroupedproductsParams,
options?: { [key: string]: any },
) {
return request<any>('/product/grouped', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /product/import */
export async function productcontrollerImportproductscsv(
body: {},
@ -623,21 +563,6 @@ export async function productcontrollerGetproductlist(
});
}
/** 此处后端没有提供注释 GET /product/list/grouped */
export async function productcontrollerGetproductlistgrouped(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerGetproductlistgroupedParams,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/product/list/grouped', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /product/search */
export async function productcontrollerSearchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -835,21 +760,6 @@ export async function productcontrollerCompatstrengthall(options?: {
});
}
/** 此处后端没有提供注释 POST /product/sync-from-site */
export async function productcontrollerSyncproductfromsite(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<API.ProductRes>('/product/sync-from-site', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /product/sync-stock */
export async function productcontrollerSyncstocktoproduct(options?: {
[key: string]: any;
@ -860,17 +770,12 @@ export async function productcontrollerSyncstocktoproduct(options?: {
});
}
/** 此处后端没有提供注释 POST /product/sync-to-site */
export async function productcontrollerSynctosite(
body: API.SyncProductToSiteDTO,
options?: { [key: string]: any },
) {
return request<API.SyncProductToSiteResultDTO>('/product/sync-to-site', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
/** 此处后端没有提供注释 GET /product/wp-products */
export async function productcontrollerGetwpproducts(options?: {
[key: string]: any;
}) {
return request<any>('/product/wp-products', {
method: 'GET',
...(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.SitesResponse>('/site/all', {
return request<API.WpSitesResponse>('/site/all', {
method: 'GET',
...(options || {}),
});
@ -59,16 +59,9 @@ export async function sitecontrollerGet(
}
/** 此处后端没有提供注释 GET /site/list */
export async function sitecontrollerList(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.sitecontrollerListParams,
options?: { [key: string]: any },
) {
export async function sitecontrollerList(options?: { [key: string]: any }) {
return request<any>('/site/list', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

View File

@ -15,6 +15,10 @@ export async function siteapicontrollerGetcustomers(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
},
@ -70,6 +74,10 @@ export async function siteapicontrollerExportcustomers(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -94,20 +102,6 @@ export async function siteapicontrollerImportcustomers(
});
}
/** 此处后端没有提供注释 GET /site-api/${param0}/links */
export async function siteapicontrollerGetlinks(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerGetlinksParams,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<any>(`/site-api/${param0}/links`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /site-api/${param0}/media */
export async function siteapicontrollerGetmedia(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -119,6 +113,10 @@ export async function siteapicontrollerGetmedia(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -198,6 +196,10 @@ export async function siteapicontrollerExportmedia(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -214,6 +216,10 @@ export async function siteapicontrollerGetorders(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -242,56 +248,17 @@ export async function siteapicontrollerCreateorder(
export async function siteapicontrollerBatchorders(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchordersParams,
body: API.BatchOperationDTO,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/orders/batch`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
return request<Record<string, any>>(`/site-api/${param0}/orders/batch`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
);
}
/** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-fulfill */
export async function siteapicontrollerBatchfulfillorders(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchfulfillordersParams,
body: API.BatchFulfillmentsDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(
`/site-api/${param0}/orders/batch-fulfill`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 GET /site-api/${param0}/orders/count */
export async function siteapicontrollerCountorders(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerCountordersParams,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<Record<string, any>>(`/site-api/${param0}/orders/count`, {
method: 'GET',
params: { ...queryParams },
data: body,
...(options || {}),
});
}
@ -307,6 +274,10 @@ export async function siteapicontrollerExportorders(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -344,6 +315,10 @@ export async function siteapicontrollerGetproducts(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
},
@ -373,44 +348,19 @@ export async function siteapicontrollerCreateproduct(
export async function siteapicontrollerBatchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchproductsParams,
body: API.BatchOperationDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/products/batch`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 POST /site-api/${param0}/products/batch-upsert */
export async function siteapicontrollerBatchupsertproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerBatchupsertproductParams,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BatchOperationResultDTO>(
`/site-api/${param0}/products/batch-upsert`,
{
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
...(options || {}),
return request<Record<string, any>>(`/site-api/${param0}/products/batch`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
);
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /site-api/${param0}/products/export */
@ -424,6 +374,10 @@ export async function siteapicontrollerExportproducts(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -440,6 +394,10 @@ export async function siteapicontrollerExportproductsspecial(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
@ -486,25 +444,6 @@ export async function siteapicontrollerImportproductsspecial(
);
}
/** 此处后端没有提供注释 POST /site-api/${param0}/products/upsert */
export async function siteapicontrollerUpsertproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerUpsertproductParams,
body: API.UnifiedProductDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.UnifiedProductDTO>(`/site-api/${param0}/products/upsert`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /site-api/${param0}/reviews */
export async function siteapicontrollerGetreviews(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -518,6 +457,10 @@ export async function siteapicontrollerGetreviews(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
},
@ -556,6 +499,10 @@ export async function siteapicontrollerGetsubscriptions(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
},
@ -573,46 +520,15 @@ export async function siteapicontrollerExportsubscriptions(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /site-api/${param0}/webhooks */
export async function siteapicontrollerGetwebhooks(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerGetwebhooksParams,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.UnifiedPaginationDTO>(`/site-api/${param0}/webhooks`, {
method: 'GET',
params: {
...queryParams,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /site-api/${param0}/webhooks */
export async function siteapicontrollerCreatewebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerCreatewebhookParams,
body: API.CreateWebhookDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.UnifiedWebhookDTO>(`/site-api/${param0}/webhooks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /site-api/${param1}/customers/${param0} */
export async function siteapicontrollerGetcustomer(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -682,6 +598,10 @@ export async function siteapicontrollerGetcustomerorders(
method: 'GET',
params: {
...queryParams,
where: undefined,
...queryParams['where'],
order: undefined,
...queryParams['order'],
},
...(options || {}),
},
@ -768,67 +688,6 @@ export async function siteapicontrollerDeleteorder(
});
}
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/cancel-fulfill */
export async function siteapicontrollerCancelfulfillment(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerCancelfulfillmentParams,
body: API.CancelFulfillmentDTO,
options?: { [key: string]: any },
) {
const { id: param0, siteId: param1, ...queryParams } = params;
return request<Record<string, any>>(
`/site-api/${param1}/orders/${param0}/cancel-fulfill`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 GET /site-api/${param1}/orders/${param0}/fulfillments */
export async function siteapicontrollerGetorderfulfillments(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerGetorderfulfillmentsParams,
options?: { [key: string]: any },
) {
const { orderId: param0, siteId: param1, ...queryParams } = params;
return request<Record<string, any>>(
`/site-api/${param1}/orders/${param0}/fulfillments`,
{
method: 'GET',
params: { ...queryParams },
...(options || {}),
},
);
}
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/fulfillments */
export async function siteapicontrollerCreateorderfulfillment(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerCreateorderfulfillmentParams,
body: API.UnifiedOrderTrackingDTO,
options?: { [key: string]: any },
) {
const { orderId: param0, siteId: param1, ...queryParams } = params;
return request<API.UnifiedOrderTrackingDTO>(
`/site-api/${param1}/orders/${param0}/fulfillments`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 GET /site-api/${param1}/orders/${param0}/notes */
export async function siteapicontrollerGetordernotes(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
@ -960,111 +819,6 @@ export async function siteapicontrollerDeletereview(
});
}
/** 此处后端没有提供注释 GET /site-api/${param1}/webhooks/${param0} */
export async function siteapicontrollerGetwebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerGetwebhookParams,
options?: { [key: string]: any },
) {
const { id: param0, siteId: param1, ...queryParams } = params;
return request<API.UnifiedWebhookDTO>(
`/site-api/${param1}/webhooks/${param0}`,
{
method: 'GET',
params: { ...queryParams },
...(options || {}),
},
);
}
/** 此处后端没有提供注释 PUT /site-api/${param1}/webhooks/${param0} */
export async function siteapicontrollerUpdatewebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerUpdatewebhookParams,
body: API.UpdateWebhookDTO,
options?: { [key: string]: any },
) {
const { id: param0, siteId: param1, ...queryParams } = params;
return request<API.UnifiedWebhookDTO>(
`/site-api/${param1}/webhooks/${param0}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 DELETE /site-api/${param1}/webhooks/${param0} */
export async function siteapicontrollerDeletewebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerDeletewebhookParams,
options?: { [key: string]: any },
) {
const { id: param0, siteId: param1, ...queryParams } = params;
return request<Record<string, any>>(
`/site-api/${param1}/webhooks/${param0}`,
{
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
},
);
}
/** 此处后端没有提供注释 PUT /site-api/${param2}/orders/${param1}/fulfillments/${param0} */
export async function siteapicontrollerUpdateorderfulfillment(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerUpdateorderfulfillmentParams,
body: API.UnifiedOrderTrackingDTO,
options?: { [key: string]: any },
) {
const {
fulfillmentId: param0,
orderId: param1,
siteId: param2,
...queryParams
} = params;
return request<API.UnifiedOrderTrackingDTO>(
`/site-api/${param2}/orders/${param1}/fulfillments/${param0}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 DELETE /site-api/${param2}/orders/${param1}/fulfillments/${param0} */
export async function siteapicontrollerDeleteorderfulfillment(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.siteapicontrollerDeleteorderfulfillmentParams,
options?: { [key: string]: any },
) {
const {
fulfillmentId: param0,
orderId: param1,
siteId: param2,
...queryParams
} = params;
return request<Record<string, any>>(
`/site-api/${param2}/orders/${param1}/fulfillments/${param0}`,
{
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
},
);
}
/** 此处后端没有提供注释 PUT /site-api/${param2}/products/${param1}/variations/${param0} */
export async function siteapicontrollerUpdatevariation(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -78,7 +78,7 @@ export async function statisticscontrollerGetorderbyemail(
}
/** 此处后端没有提供注释 GET /statistics/orderSource */
export async function statisticscontrollerGetordersource(options?: {
export async function statisticscontrollerGetordersorce(options?: {
[key: string]: any;
}) {
return request<any>('/statistics/orderSource', {

View File

@ -84,21 +84,6 @@ export async function templatecontrollerGettemplatelist(options?: {
});
}
/** 此处后端没有提供注释 POST /template/render-direct */
export async function templatecontrollerRendertemplatedirect(
body: API.RenderTemplateDTO,
options?: { [key: string]: any },
) {
return request<Record<string, any>>('/template/render-direct', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /template/render/${param0} */
export async function templatecontrollerRendertemplate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

File diff suppressed because it is too large Load Diff

View File

@ -10,26 +10,6 @@ export async function webhookcontrollerTest(options?: { [key: string]: any }) {
});
}
/** 此处后端没有提供注释 POST /webhook/shoppy */
export async function webhookcontrollerHandleshoppywebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.webhookcontrollerHandleshoppywebhookParams,
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<any>('/webhook/shoppy', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: {
...params,
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /webhook/woocommerce */
export async function webhookcontrollerHandlewoowebhook(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -0,0 +1,269 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 此处后端没有提供注释 DELETE /wp_product/${param0} */
export async function wpproductcontrollerDelete(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerDeleteParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/${param0}`, {
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/batch-sync-to-site/${param0} */
export async function wpproductcontrollerBatchsynctosite(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerBatchsynctositeParams,
body: API.BatchSyncProductsDTO,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/batch-sync-to-site/${param0}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/batch-update */
export async function wpproductcontrollerBatchupdateproducts(
body: API.BatchUpdateProductsDTO,
options?: { [key: string]: any },
) {
return request<API.BooleanRes>('/wp_product/batch-update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/batch-update-tags */
export async function wpproductcontrollerBatchupdatetags(
body: API.BatchUpdateTagsDTO,
options?: { [key: string]: any },
) {
return request<API.BooleanRes>('/wp_product/batch-update-tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/import/${param0} */
export async function wpproductcontrollerImportproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerImportproductsParams,
body: {},
files?: File[],
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
const formData = new FormData();
if (files) {
files.forEach((f) => formData.append('files', f || ''));
}
Object.keys(body).forEach((ele) => {
const item = (body as any)[ele];
if (item !== undefined && item !== null) {
if (typeof item === 'object' && !(item instanceof File)) {
if (item instanceof Array) {
item.forEach((f) => formData.append(ele, f || ''));
} else {
formData.append(
ele,
new Blob([JSON.stringify(item)], { type: 'application/json' }),
);
}
} else {
formData.append(ele, item);
}
}
});
return request<API.BooleanRes>(`/wp_product/import/${param0}`, {
method: 'POST',
params: { ...queryParams },
data: formData,
requestType: 'form',
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /wp_product/list */
export async function wpproductcontrollerGetwpproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerGetwpproductsParams,
options?: { [key: string]: any },
) {
return request<API.WpProductListRes>('/wp_product/list', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /wp_product/search */
export async function wpproductcontrollerSearchproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerSearchproductsParams,
options?: { [key: string]: any },
) {
return request<API.ProductsRes>('/wp_product/search', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/setconstitution */
export async function wpproductcontrollerSetconstitution(
body: Record<string, any>,
options?: { [key: string]: any },
) {
return request<API.BooleanRes>('/wp_product/setconstitution', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/siteId/${param0}/products */
export async function wpproductcontrollerCreateproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerCreateproductParams,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/siteId/${param0}/products`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /wp_product/siteId/${param1}/products/${param0} */
export async function wpproductcontrollerUpdateproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerUpdateproductParams,
body: API.UpdateWpProductDTO,
options?: { [key: string]: any },
) {
const { productId: param0, siteId: param1, ...queryParams } = params;
return request<API.BooleanRes>(
`/wp_product/siteId/${param1}/products/${param0}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 PUT /wp_product/siteId/${param2}/products/${param1}/variations/${param0} */
export async function wpproductcontrollerUpdatevariation(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerUpdatevariationParams,
body: API.UpdateVariationDTO,
options?: { [key: string]: any },
) {
const {
variationId: param0,
productId: param1,
siteId: param2,
...queryParams
} = params;
return request<any>(
`/wp_product/siteId/${param2}/products/${param1}/variations/${param0}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...queryParams },
data: body,
...(options || {}),
},
);
}
/** 此处后端没有提供注释 POST /wp_product/sync-to-product/${param0} */
export async function wpproductcontrollerSynctoproduct(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerSynctoproductParams,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/sync-to-product/${param0}`, {
method: 'POST',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/sync/${param0} */
export async function wpproductcontrollerSyncproducts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerSyncproductsParams,
options?: { [key: string]: any },
) {
const { siteId: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/sync/${param0}`, {
method: 'POST',
params: { ...queryParams },
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /wp_product/updateState/${param0} */
export async function wpproductcontrollerUpdatewpproductstate(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.wpproductcontrollerUpdatewpproductstateParams,
body: Record<string, any>,
options?: { [key: string]: any },
) {
const { id: param0, ...queryParams } = params;
return request<API.BooleanRes>(`/wp_product/updateState/${param0}`, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
params: { ...queryParams },
data: body,
...(options || {}),
});
}

View File

@ -1,63 +0,0 @@
import { message } from 'antd';
/**
*
*/
export interface BatchErrorItem {
identifier: string;
error: string;
}
/**
*
*/
export interface BatchOperationResult {
total: number;
processed: number;
created?: number;
updated?: number;
deleted?: number;
synced?: number;
errors?: BatchErrorItem[];
}
/**
* ()
* @param result
* @param operationType
*/
export function showBatchOperationResult(
result: BatchOperationResult,
operationType: string = '操作',
): string {
// 从 result.data 中获取实际数据(因为后端返回格式为 { success: true, data: {...} })
const data = (result as any).data || result;
const { total, processed, created, updated, deleted, errors } = data;
// 构建结果消息
let messageContent = `${operationType}结果:共 ${total} 条,成功 ${processed}`;
if (created) {
messageContent += `,创建 ${created}`;
}
if (updated) {
messageContent += `,更新 ${updated}`;
}
if (deleted) {
messageContent += `,删除 ${deleted}`;
}
// 处理错误情况
if (errors && errors.length > 0) {
messageContent += `,失败 ${errors.length}`;
// 显示错误详情
const errorDetails = errors
.map((err: BatchErrorItem) => `${err.identifier}: ${err.error}`)
.join('\n');
message.warning(messageContent + '\n\n错误详情:\n' + errorDetails);
} else {
message.success(messageContent);
}
return messageContent;
}