Compare commits

..

5 Commits

Author SHA1 Message Date
tikkhun bb907fa47c feat(订单): 添加订单导出功能并更新相关类型定义
添加订单导出API接口及前端实现
调整订单列表页面的IP字段显示
更新订单相关类型定义,包括物流信息和状态字段
2026-01-06 10:33:44 +08:00
tikkhun ce12e55adc refactor(订单列表): 更新物流信息显示字段和样式
将 shipmentList 字段改为 fulfillments 以匹配后端数据
调整物流信息显示样式,移除状态显示并优化布局
2026-01-05 22:39:30 +08:00
tikkhun e498793678 feat: 为多个页面表格添加快速跳转功能
在多个页面的表格分页器中添加 showQuickJumper 属性,允许用户快速跳转到指定页码
2026-01-05 16:37:53 +08:00
tikkhun 2467a0c3b2 refactor: 提取站点相关逻辑到自定义hook并优化类型定义
将多处重复的站点数据获取和处理的逻辑提取到自定义hook useSites中
修改origin_id类型为string以匹配实际使用场景
移除调试用的console.log语句
为用户名字段添加可复制功能
2026-01-05 16:31:08 +08:00
tikkhun 5471f7a038 fix: 修复账单地址显示问题并优化表格列配置
修复账单地址显示为仅state的问题,改为显示完整地址信息
优化多个页面的表格列配置和请求参数处理
移除Product列表页的展开行功能
添加批量操作按钮和错误处理逻辑
2026-01-05 15:36:35 +08:00
18 changed files with 204 additions and 857 deletions

92
src/hooks/useSites.ts Normal file
View File

@ -0,0 +1,92 @@
import { useState, useEffect } from 'react';
import { sitecontrollerAll } from '@/servers/api/site';
// 站点数据的类型定义
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

@ -241,7 +241,7 @@ const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
pagination={{ pagination={{
pageSize: 10, pageSize: 10,
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => `${total}`, showQuickJumper: true,
}} }}
scroll={{ x: 800 }} scroll={{ x: 800 }}
/> />

View File

@ -115,34 +115,7 @@ const CustomerList: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const { message } = App.useApp(); const { message } = App.useApp();
const [syncModalVisible, setSyncModalVisible] = useState(false); const [syncModalVisible, setSyncModalVisible] = useState(false);
const [sites, setSites] = useState<any[]>([]); // 添加站点数据状态
// 获取站点数据
const fetchSites = async () => {
try {
const { data, success } = await sitecontrollerAll();
if (success) {
setSites(data || []);
}
} catch (error) {
console.error('获取站点数据失败:', error);
}
};
// 根据站点ID获取站点名称
const getSiteName = (siteId: number | undefined | null) => {
if (!siteId) return '-';
if (typeof siteId === 'string') {
return siteId;
}
const site = sites.find((s) => s.id === siteId);
return site ? site.name : String(siteId);
};
// 组件加载时获取站点数据
useEffect(() => {
fetchSites();
}, []);
const columns: ProColumns<API.GetCustomerDTO>[] = [ const columns: ProColumns<API.GetCustomerDTO>[] = [
{ {
@ -205,7 +178,7 @@ const CustomerList: React.FC = () => {
{ {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
hideInSearch: true, copyable: true,
sorter: true, sorter: true,
}, },
{ {
@ -223,6 +196,7 @@ const CustomerList: React.FC = () => {
title: '账单地址', title: '账单地址',
dataIndex: 'billing', dataIndex: 'billing',
hideInSearch: true, hideInSearch: true,
width: 200, width: 200,
render: (billing) => <AddressCell address={billing} title="账单地址" />, render: (billing) => <AddressCell address={billing} title="账单地址" />,
}, },
@ -236,7 +210,6 @@ const CustomerList: React.FC = () => {
{ {
title: '评分', title: '评分',
dataIndex: 'rate', dataIndex: 'rate',
width: 120,
hideInSearch: true, hideInSearch: true,
sorter: true, sorter: true,
render: (_, record) => { render: (_, record) => {
@ -343,6 +316,7 @@ const CustomerList: React.FC = () => {
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
headerTitle="查询表格" headerTitle="查询表格"
actionRef={actionRef} actionRef={actionRef}
columns={columns}
rowKey="id" rowKey="id"
request={async (params, sorter,filter) => { request={async (params, sorter,filter) => {
console.log('custoemr request',params, sorter,filter) console.log('custoemr request',params, sorter,filter)
@ -370,7 +344,7 @@ const CustomerList: React.FC = () => {
success: true, success: true,
}; };
}} }}
columns={columns}
search={{ search={{
labelWidth: 'auto', labelWidth: 'auto',
span: 6, span: 6,
@ -378,8 +352,7 @@ const CustomerList: React.FC = () => {
pagination={{ pagination={{
pageSize: 20, pageSize: 20,
showSizeChanger: true, showSizeChanger: true,
showTotal: (total, range) => showQuickJumper: true,
`${range[0]}-${range[1]} 条/总共 ${total}`,
}} }}
toolBarRender={() => [ toolBarRender={() => [
<Button <Button

View File

@ -137,7 +137,7 @@ const ListPage: React.FC = () => {
{ {
title: '账单地址', title: '账单地址',
dataIndex: 'billing', dataIndex: 'billing',
render: (_, record) => record?.billing.state || record?.shipping.state, render: (_, record) => JSON.stringify(record?.billing || record?.shipping),
}, },
{ {
title: '标签', title: '标签',

View File

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

View File

@ -189,11 +189,20 @@ const ListPage: React.FC = () => {
dataIndex: 'siteId', dataIndex: 'siteId',
valueType: 'select', valueType: 'select',
request: async () => { request: async () => {
const { data = [] } = await sitecontrollerAll(); try {
return data.map((item) => ({ const result = await sitecontrollerAll();
label: item.name, const {success, data}= result
value: item.id, if (success && data) {
return data.map((site: any) => ({
label: site.name,
value: site.id,
})); }));
}
return [];
} catch (error) {
console.error('获取站点列表失败:', error);
return [];
}
}, },
}, },
{ {
@ -258,17 +267,17 @@ const ListPage: React.FC = () => {
}, },
{ {
title: '物流', title: '物流',
dataIndex: 'shipmentList', dataIndex: 'fulfillments',
hideInSearch: true, hideInSearch: true,
render: (_, record) => { render: (_, record) => {
return ( return (
<div> <div>
{(record as any)?.shipmentList?.map((item: any) => { {(record as any)?.fulfillments?.map((item: any) => {
if (!item) return; if (!item) return;
return ( return (
<div> <div style={{ display:"flex", alignItems:"center" }}>
{item.tracking_provider}:{item.primary_tracking_number} ( {item.tracking_provider}
{formatShipmentState(item.state)}) {item.tracking_number}
</div> </div>
); );
})} })}
@ -279,7 +288,6 @@ const ListPage: React.FC = () => {
{ {
title: 'IP', title: 'IP',
dataIndex: 'customer_ip_address', dataIndex: 'customer_ip_address',
hideInSearch: true,
}, },
{ {
title: '设备', title: '设备',
@ -470,6 +478,7 @@ const ListPage: React.FC = () => {
pagination={{ pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000'], pageSizeOptions: ['10', '20', '50', '100', '1000'],
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10, defaultPageSize: 10,
}} }}
toolBarRender={() => [ toolBarRender={() => [
@ -494,31 +503,24 @@ const ListPage: React.FC = () => {
}} }}
tableRef={actionRef} tableRef={actionRef}
/>, />,
// <Button
// type="primary"
// disabled={selectedRowKeys.length === 0}
// onClick={handleBatchExport}
// >
// 批量导出
// </Button>,
<Popconfirm <Popconfirm
title="批量导出" title="批量导出"
description="确认导出选中的订单吗?" description="确认导出选中的订单吗?"
onConfirm={async () => { onConfirm={async () => {
console.log(selectedRowKeys); console.log(selectedRowKeys);
try { try {
const res = await request('/order/order/export', { const res = await request('/order/export', {
method: 'GET', method: 'POST',
params: { data: {
ids: selectedRowKeys, ids: selectedRowKeys,
} }
}); });
if (res?.success && res?.data?.csv) { if (res?.success && res.data) {
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([res.data], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'customers.csv'; a.download = 'orders.csv';
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} else { } else {

View File

@ -492,9 +492,12 @@ const List: React.FC = () => {
sortField = field; sortField = field;
sortOrder = sort[field]; sortOrder = sort[field];
} }
const {current,pageSize,...where} = params
console.log(`params`,params)
const { data, success } = await productcontrollerGetproductlist({ const { data, success } = await productcontrollerGetproductlist({
...params, where,
page: current || 1,
per_page: pageSize || 10,
sortField, sortField,
sortOrder, sortOrder,
} as any); } as any);
@ -505,17 +508,17 @@ const List: React.FC = () => {
}; };
}} }}
columns={columns} columns={columns}
expandable={{ // expandable={{
expandedRowRender: (record) => ( // expandedRowRender: (record) => (
<SiteProductInfo // <SiteProductInfo
skus={(record.siteSkus as string[]) || []} // skus={(record.siteSkus as string[]) || []}
record={record} // record={record}
parentTableRef={actionRef} // parentTableRef={actionRef}
/> // />
), // ),
rowExpandable: (record) => // rowExpandable: (record) =>
!!(record.siteSkus && record.siteSkus.length > 0), // !!(record.siteSkus && record.siteSkus.length > 0),
}} // }}
editable={{ editable={{
type: 'single', type: 'single',
onSave: async (key, record, originRow) => { onSave: async (key, record, originRow) => {

View File

@ -400,6 +400,7 @@ const PermutationPage: React.FC = () => {
pagination={{ pagination={{
defaultPageSize: 50, defaultPageSize: 50,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'], pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
}} }}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}

View File

@ -414,6 +414,7 @@ const ProductSyncPage: React.FC = () => {
pagination={{ pagination={{
pageSize: 10, pageSize: 10,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true,
}} }}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
search={{ search={{

View File

@ -1,761 +0,0 @@
import { productcontrollerGetproductlist } from '@/servers/api/product';
import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
import { templatecontrollerGettemplatebyname } from '@/servers/api/template';
import { EditOutlined, SyncOutlined } from '@ant-design/icons';
import {
ActionType,
ModalForm,
ProColumns,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
Button,
Card,
message,
Modal,
Progress,
Select,
Spin,
Tag,
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm';
// 定义站点接口
interface Site {
id: string;
name: string;
skuPrefix?: string;
isDisabled?: boolean;
}
// 定义站点商品接口
interface SiteProduct {
id: string;
sku: string;
name: string;
price: number;
stock: number;
categories: string[];
}
// 定义API响应接口
interface ApiResponse<T> {
data: T[];
success: boolean;
message?: string;
}
// 模拟API请求函数
const getSites = async (): Promise<ApiResponse<Site>> => {
const res = await request('/site/list', {
method: 'GET',
params: {
current: 1,
pageSize: 1000,
},
});
return {
data: res.data?.items || [],
success: res.success,
message: res.message,
};
};
const getsiteProduct = async (): Promise<ApiResponse<SiteProduct>> => {
return request('/product/wp-products', {
method: 'GET',
});
};
const ProductSyncPage: React.FC = () => {
const [sites, setSites] = useState<Site[]>([]);
// 存储所有 WP 产品,用于查找匹配。 Key: SKU (包含前缀)
const [siteProductMap, setSiteProductMap] = useState<Map<string, SiteProduct>>(
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[]>([]);
// 初始化数据:获取站点和所有 WP 产品
useEffect(() => {
const fetchData = async () => {
try {
setInitialLoading(true);
// 获取所有站点
const sitesResponse = await getSites();
const rawSiteList = sitesResponse.data || [];
// 过滤掉已禁用的站点
const siteList: Site[] = rawSiteList.filter((site) => !site.isDisabled);
setSites(siteList);
// 获取所有 WordPress 商品
const siteProductsResponse = await siteapicontrollerGetproducts({
siteId: selectedSiteId,
});
const siteProductList: SiteProduct[] = siteProductsResponse.data || [];
// 构建 WP 产品 MapKey 为 SKU
const map = new Map<string, SiteProduct>();
siteProductList.forEach((p) => {
if (p.sku) {
map.set(p.sku, p);
}
});
setSiteProductMap(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) {
message.error('获取基础数据失败,请重试');
console.error('Error fetching data:', error);
} finally {
setInitialLoading(false);
}
};
fetchData();
}, []);
// 同步产品到站点
const syncProductToSite = async (
values: any,
record: ProductWithWP,
site: Site,
siteProductId?: 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,
};
let res;
if (siteProductId) {
res = await request(`/site-api/${site.id}/products/${siteProductId}`, {
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避免刷新
setSiteProductMap((prev) => {
const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') {
newMap.set(values.sku, res.data as SiteProduct);
}
return newMap;
});
hide();
message.success('同步成功');
return true;
} catch (error: any) {
message.error('同步失败: ' + (error.message || error.toString()));
return false;
} finally {
}
};
// 批量同步产品到指定站点
const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => {
if (!selectedSiteId) {
message.error('请选择要同步到的站点');
return;
}
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 ProductWithWP[];
} catch (error) {
message.error('获取产品列表失败');
return;
}
}
setSyncing(true);
setSyncProgress(0);
setSyncResults({ success: 0, failed: 0, errors: [] });
const totalProducts = products.length;
let processed = 0;
let successCount = 0;
let failedCount = 0;
const errors: string[] = [];
try {
// 逐个同步产品
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;
}
}
// 如果没有找到实际的siteSku则根据模板生成
const expectedSku =
siteProductSku ||
(skuTemplate
? renderSiteSku(skuTemplate, { site: targetSite, product })
: `${targetSite.skuPrefix || ''}-${product.sku}`);
// 检查是否已存在
const existingProduct = siteProductMap.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++;
// 更新本地缓存
setSiteProductMap((prev) => {
const newMap = new Map(prev);
if (res.data && typeof res.data === 'object') {
newMap.set(expectedSku, res.data as SiteProduct);
}
return newMap;
});
} else {
failedCount++;
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
}
} catch (error: any) {
failedCount++;
errors.push(`产品 ${product.sku}: ${error.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 {
setSyncing(false);
}
};
// 简单的模板渲染函数
const renderSiteSku = (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<ProductWithWP>[] => {
const columns: ProColumns<ProductWithWP>[] = [
{
title: 'SKU',
dataIndex: 'sku',
key: 'sku',
width: 150,
fixed: 'left',
copyable: true,
},
{
title: '商品信息',
key: 'profile',
width: 300,
fixed: 'left',
render: (_, record) => (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
}}
>
<div style={{ fontWeight: 'bold', fontSize: 14 }}>
{record.name}
</div>
<EditForm
record={record}
tableRef={actionRef}
trigger={
<EditOutlined
style={{
cursor: 'pointer',
fontSize: 16,
color: '#1890ff',
}}
/>
}
/>
</div>
<div style={{ fontSize: 12, color: '#666' }}>
<span style={{ marginRight: 8 }}>: {record.price}</span>
{record.promotionPrice && (
<span style={{ color: 'red' }}>
: {record.promotionPrice}
</span>
)}
</div>
{/* 属性 */}
<div style={{ marginTop: 4 }}>
{record.attributes?.map((attr: any, idx: number) => (
<Tag
key={idx}
style={{ fontSize: 10, marginRight: 4, marginBottom: 2 }}
>
{attr.dict?.name || attr.name}: {attr.name}
</Tag>
))}
</div>
{/* 组成 (如果是 Bundle) */}
{record.type === 'bundle' &&
record.components &&
record.components.length > 0 && (
<div
style={{
marginTop: 8,
fontSize: 12,
background: '#f5f5f5',
padding: 4,
borderRadius: 4,
}}
>
<div style={{ fontWeight: 'bold', marginBottom: 2 }}>
Components:
</div>
{record.components.map((comp: any, idx: number) => (
<div key={idx}>
{comp.sku} × {comp.quantity}
</div>
))}
</div>
)}
</div>
),
},
];
// 为每个站点生成列
sites.forEach((site: Site) => {
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
? renderSiteSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`);
// 尝试用确定的SKU获取WP产品
let siteProduct = siteProductMap.get(expectedSku);
// 如果根据实际SKU没找到再尝试用模板生成的SKU查找
if (!siteProduct && siteProductSku && skuTemplate) {
const templateSku = renderSiteSku(skuTemplate, {
site,
product: record,
});
siteProduct = siteProductMap.get(templateSku);
}
if (!siteProduct) {
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
? renderSiteSku(skuTemplate, { site, product: record })
: `${site.skuPrefix || ''}-${record.sku}`),
}}
>
<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 syncProductToSite(
values,
record,
site,
siteProduct.externalProductId,
);
}}
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>
);
},
};
columns.push(siteColumn);
});
return columns;
};
if (initialLoading) {
return (
<Card title="商品同步状态" className="product-sync-card">
<Spin
size="large"
style={{ display: 'flex', justifyContent: 'center', padding: 40 }}
/>
</Card>
);
}
return (
<Card title="商品同步状态" className="product-sync-card">
<ProTable<ProductWithWP>
columns={generateColumns()}
actionRef={actionRef}
rowKey="id"
rowSelection={{
selectedRowKeys,
onChange: (keys, rows) => {
setSelectedRowKeys(keys);
setSelectedRows(rows);
},
}}
toolBarRender={() => [
<Select
key="site-select"
style={{ width: 200 }}
placeholder="选择目标站点"
value={selectedSiteId}
onChange={setSelectedSiteId}
options={sites.map((site) => ({
label: site.name,
value: site.id,
}))}
/>,
<Button
key="batch-sync"
type="primary"
icon={<SyncOutlined />}
onClick={() => {
if (!selectedSiteId) {
message.warning('请先选择目标站点');
return;
}
setBatchSyncModalVisible(true);
}}
disabled={!selectedSiteId || sites.length === 0}
>
</Button>,
]}
request={async (params, sort, filter) => {
// 调用本地获取产品列表 API
const { data, success } = await productcontrollerGetproductlist({
...params,
current: params.current,
pageSize: params.pageSize,
// 传递搜索参数
keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
sku: (params as any).sku,
name: (params as any).name,
} as any);
// 返回给 ProTable
return {
data: (data?.items || []) as ProductWithWP[],
success,
total: data?.total || 0,
};
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
}}
scroll={{ x: 'max-content' }}
search={{
labelWidth: 'auto',
}}
options={{
density: true,
fullScreen: true,
}}
dateFormatter="string"
/>
{/* 批量同步模态框 */}
<Modal
title="批量同步产品"
open={batchSyncModalVisible}
onCancel={() => !syncing && setBatchSyncModalVisible(false)}
footer={null}
closable={!syncing}
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>
)}
</div>
{syncing && (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8 }}></div>
<Progress percent={syncProgress} status="active" />
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
{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>
{syncResults.errors.slice(0, 10).map((error, index) => (
<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>
)}
<div style={{ textAlign: 'right' }}>
<Button
onClick={() => setBatchSyncModalVisible(false)}
disabled={syncing}
style={{ marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => batchSyncProducts()}
loading={syncing}
disabled={syncing}
>
{syncing ? '同步中...' : '开始同步'}
</Button>
</div>
</Modal>
</Card>
);
};
export default ProductSyncPage;

View File

@ -359,7 +359,6 @@ const SiteList: React.FC = () => {
defaultPageSize: 20, defaultPageSize: 20,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (total) => `${total}`,
}} }}
toolBarRender={() => [ toolBarRender={() => [
<Button <Button

View File

@ -465,7 +465,7 @@ const CustomerPage: React.FC = () => {
<ProTable <ProTable
rowKey="id" rowKey="id"
search={false} search={false}
pagination={{ pageSize: 20 }} pagination={{ pageSize: 20 ,showSizeChanger: true, showQuickJumper: true,}}
columns={[ columns={[
{ title: '订单号', dataIndex: 'number', copyable: true }, { title: '订单号', dataIndex: 'number', copyable: true },
{ {

View File

@ -62,11 +62,10 @@ const OrdersPage: React.FC = () => {
return [{ key: 'all', label: `全部(${total})` }, ...tabs]; return [{ key: 'all', label: `全部(${total})` }, ...tabs];
}, [count]); }, [count]);
const columns: ProColumns<API.Order>[] = [ const columns: ProColumns<API.UnifiedOrderDTO>[] = [
{ {
title: '订单号', title: '订单号',
dataIndex: 'id', dataIndex: 'id',
hideInSearch: true,
}, },
{ {
title: '状态', title: '状态',
@ -97,7 +96,6 @@ const OrdersPage: React.FC = () => {
{ {
title: '客户姓名', title: '客户姓名',
dataIndex: 'customer_name', dataIndex: 'customer_name',
hideInSearch: true,
}, },
{ {
title: '商品', title: '商品',
@ -129,7 +127,6 @@ const OrdersPage: React.FC = () => {
}, },
{ {
title: '联系电话', title: '联系电话',
hideInSearch: true,
render: (_, record) => record.shipping?.phone || record.billing?.phone, render: (_, record) => record.shipping?.phone || record.billing?.phone,
}, },
{ {
@ -309,6 +306,7 @@ const OrdersPage: React.FC = () => {
pagination={{ pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000'], pageSizeOptions: ['10', '20', '50', '100', '1000'],
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10, defaultPageSize: 10,
}} }}
toolBarRender={() => [ toolBarRender={() => [
@ -319,6 +317,8 @@ const OrdersPage: React.FC = () => {
setSelectedRowKeys={setSelectedRowKeys} setSelectedRowKeys={setSelectedRowKeys}
siteId={siteId} siteId={siteId}
/>, />,
<Button disabled></Button>
,
<Button <Button
title="批量删除" title="批量删除"
danger danger

View File

@ -394,6 +394,7 @@ const ProductsPage: React.FC = () => {
pagination={{ pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'], pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10, defaultPageSize: 10,
}} }}
actionRef={actionRef} actionRef={actionRef}

View File

@ -148,7 +148,7 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
pagination={{ pagination={{
pageSize: 10, pageSize: 10,
showSizeChanger: true, showSizeChanger: true,
showTotal: (total) => `${total}`, showQuickJumper: true,
}} }}
toolBarRender={false} toolBarRender={false}
options={false} options={false}

View File

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

View File

@ -59,6 +59,21 @@ 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 */ /** 此处后端没有提供注释 POST /order/getOrderByNumber */
export async function ordercontrollerGetorderbynumber( export async function ordercontrollerGetorderbynumber(
body: string, body: string,
@ -251,11 +266,14 @@ export async function ordercontrollerSyncorderbyid(
options?: { [key: string]: any }, options?: { [key: string]: any },
) { ) {
const { orderId: param0, siteId: param1, ...queryParams } = params; const { orderId: param0, siteId: param1, ...queryParams } = params;
return request<API.BooleanRes>(`/order/syncOrder/${param1}/order/${param0}`, { return request<API.SyncOperationResult>(
`/order/syncOrder/${param1}/order/${param0}`,
{
method: 'POST', method: 'POST',
params: { ...queryParams }, params: { ...queryParams },
...(options || {}), ...(options || {}),
}); },
);
} }
/** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */ /** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */

View File

@ -213,7 +213,7 @@ declare namespace API {
/** 站点ID */ /** 站点ID */
site_id?: number; site_id?: number;
/** 原始ID */ /** 原始ID */
origin_id?: number; origin_id?: string;
/** 邮箱 */ /** 邮箱 */
email?: string; email?: string;
/** 名字 */ /** 名字 */
@ -456,6 +456,10 @@ declare namespace API {
shipping_provider?: string; shipping_provider?: string;
/** 发货方式 */ /** 发货方式 */
shipping_method?: string; shipping_method?: string;
/** 状态 */
status?: string;
/** 创建时间 */
date_created?: string;
/** 发货商品项 */ /** 发货商品项 */
items?: FulfillmentItemDTO[]; items?: FulfillmentItemDTO[];
}; };
@ -2406,6 +2410,8 @@ declare namespace API {
params?: UnifiedSearchParamsDTO; params?: UnifiedSearchParamsDTO;
}; };
type SyncOperationResult = {};
type SyncOperationResultDTO = { type SyncOperationResultDTO = {
/** 总处理数量 */ /** 总处理数量 */
total?: number; total?: number;
@ -2641,6 +2647,8 @@ declare namespace API {
number?: string; number?: string;
/** 订单状态 */ /** 订单状态 */
status?: string; status?: string;
/** 财务状态 */
financial_status?: string;
/** 货币 */ /** 货币 */
currency?: string; currency?: string;
/** 货币符号 */ /** 货币符号 */
@ -2683,8 +2691,6 @@ declare namespace API {
fee_lines?: UnifiedFeeLineDTO[]; fee_lines?: UnifiedFeeLineDTO[];
/** 优惠券项 */ /** 优惠券项 */
coupon_lines?: UnifiedCouponLineDTO[]; coupon_lines?: UnifiedCouponLineDTO[];
/** 物流追踪信息 */
tracking?: UnifiedOrderTrackingDTO[];
/** 支付时间 */ /** 支付时间 */
date_paid?: string; date_paid?: string;
/** 客户IP地址 */ /** 客户IP地址 */
@ -2695,6 +2701,10 @@ declare namespace API {
device_type?: string; device_type?: string;
/** 来源类型 */ /** 来源类型 */
source_type?: string; source_type?: string;
/** 订单状态 */
fulfillment_status?: number;
/** 物流信息 */
fulfillments?: FulfillmentDTO[];
}; };
type UnifiedOrderLineItemDTO = { type UnifiedOrderLineItemDTO = {
@ -2739,14 +2749,16 @@ declare namespace API {
type UnifiedOrderTrackingDTO = { type UnifiedOrderTrackingDTO = {
/** 订单ID */ /** 订单ID */
order_id?: string; order_id?: string;
/** 快递公司 */ /** 物流单号 */
tracking_provider?: string;
/** 运单跟踪号 */
tracking_number?: string; tracking_number?: string;
/** 发货日期 */ /** 物流公司 */
date_shipped?: string; shipping_provider?: string;
/** 发货状态 */ /** 发货方式 */
status_shipped?: string; shipping_method?: string;
/** 状态 */
status?: string;
/** 创建时间 */
date_created?: string;
}; };
type UnifiedPaginationDTO = { type UnifiedPaginationDTO = {