zksu
/
WEB
forked from yoone/WEB
1
0
Fork 0
WEB/src/pages/Product/Sync/index.tsx

505 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { showBatchOperationResult } from '@/utils/showResult';
import {
productcontrollerBatchsynctosite,
productcontrollerGetproductlist,
productcontrollerSynctosite,
} from '@/servers/api/product';
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';
import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm';
import SiteProductCell from './SiteProductCell';
// 定义站点接口
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;
}
// 定义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 ProductSyncPage: React.FC = () => {
const [sites, setSites] = useState<Site[]>([]);
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[]>([]);
// 初始化加载站点列表
useEffect(() => {
const initializeData = async () => {
try {
// 获取站点列表
const sitesRes = await getSites();
if (sitesRes.success && sitesRes.data.length > 0) {
setSites(sitesRes.data);
}
} catch (error) {
console.error('初始化数据失败:', error);
message.error('初始化数据失败');
} finally {
setInitialLoading(false);
}
};
initializeData();
}, []);
const syncProductToSite = async (
values: any,
record: SiteProduct,
site: Site,
siteProductId?: string,
) => {
try {
const hide = message.loading('正在同步...', 0);
// 使用 productcontrollerSynctosite API 同步产品到站点
const res = await productcontrollerSynctosite({
productId: Number(record.id),
siteId: Number(site.id),
} as any);
if (!res.success) {
hide();
throw new Error(res.message || '同步失败');
}
hide();
message.success('同步成功');
return true;
} catch (error: any) {
message.error('同步失败: ' + (error.message || error.toString()));
return false;
}
};
// 批量同步产品到指定站点
const batchSyncProducts = async (productsToSync?: SiteProduct[]) => {
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 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),
} as any);
if (res.success) {
const syncedCount = res.data?.synced || 0;
const errors = res.data?.errors || [];
// 更新进度为100%,表示完成
setSyncProgress(100);
setSyncResults({
success: syncedCount,
failed: errors.length,
errors: errors.map((err: any) => err.error || '未知错误'),
});
if (errors.length === 0) {
message.success(`批量同步完成,成功同步 ${syncedCount} 个产品`);
} else {
message.warning(
`批量同步完成,成功 ${syncedCount} 个,失败 ${errors.length}`,
);
}
// 刷新表格
actionRef.current?.reload();
} else {
throw new Error(res.message || '批量同步失败');
}
} catch (error: any) {
message.error('批量同步失败: ' + (error.message || error.toString()));
} finally {
setSyncing(false);
}
};
// 生成表格列配置
const generateColumns = (): ProColumns<Site>[] => {
const columns: ProColumns<SiteProduct>[] = [
{
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<SiteProduct> = {
title: site.name,
key: `site_${site.id}`,
hideInSearch: true,
width: 220,
render: (_, record) => {
return (
<SiteProductCell
product={record}
site={site}
onSyncSuccess={() => {
// 同步成功后刷新表格
actionRef.current?.reload();
}}
/>
);
},
};
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<SiteProduct>
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 response = 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);
console.log('result', response);
// 返回给 ProTable
return {
data: response.data?.items || [],
success: response.success,
total: response.data?.total || 0,
};
}}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: 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;