diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..aa81e64
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+# 构建阶段
+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;"]
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..7e658e6
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,33 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/src/components/SyncResultMessage.tsx b/src/components/SyncResultMessage.tsx
new file mode 100644
index 0000000..31ffd48
--- /dev/null
+++ b/src/components/SyncResultMessage.tsx
@@ -0,0 +1,91 @@
+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: (
+
+
{resultMessage}
+
+ 失败详情:
+ {errors
+ .slice(0, 3)
+ .map((err: any) => `${err.identifier}: ${err.error}`)
+ .join(', ')}
+ {errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`}
+
+
+ ),
+ duration: 8,
+ key: 'sync-result',
+ });
+ } else {
+ // 完全成功
+ message.success({
+ content: resultMessage,
+ duration: 4,
+ key: 'sync-result',
+ });
+ }
+};
+
+// 同步结果显示组件
+const SyncResultMessage: React.FC = ({
+ data,
+ entityType = '订单',
+}) => {
+ // 当组件挂载时显示结果
+ React.useEffect(() => {
+ if (data) {
+ showSyncResult(data, entityType);
+ }
+ }, [data, entityType]);
+
+ // 这个组件不渲染任何内容,只用于显示消息
+ return null;
+};
+
+export default SyncResultMessage;
diff --git a/src/pages/Customer/List/HistoryOrders.tsx b/src/pages/Customer/List/HistoryOrders.tsx
index 006e0e1..5c05da9 100644
--- a/src/pages/Customer/List/HistoryOrders.tsx
+++ b/src/pages/Customer/List/HistoryOrders.tsx
@@ -1,5 +1,4 @@
import { ordercontrollerGetorders } from '@/servers/api/order';
-import { siteapicontrollerGetorders } from '@/servers/api/siteApi';
import {
App,
Col,
@@ -89,8 +88,6 @@ const HistoryOrders: React.FC = ({ customer, siteId }) => {
// 获取客户订单数据
const fetchOrders = async () => {
-
-
setLoading(true);
try {
const response = await ordercontrollerGetorders({
diff --git a/src/pages/Customer/List/index.tsx b/src/pages/Customer/List/index.tsx
index 2280537..e993091 100644
--- a/src/pages/Customer/List/index.tsx
+++ b/src/pages/Customer/List/index.tsx
@@ -12,10 +12,12 @@ import {
ModalForm,
PageContainer,
ProColumns,
+ ProFormDateTimeRangePicker,
ProFormSelect,
+ ProFormText,
ProTable,
} from '@ant-design/pro-components';
-import { App, Avatar, Button, Rate, Space, Tag, Tooltip } from 'antd';
+import { App, Avatar, Button, Form, Rate, Space, Tag, Tooltip } from 'antd';
import { useEffect, useRef, useState } from 'react';
import HistoryOrders from './HistoryOrders';
@@ -113,7 +115,6 @@ const CustomerList: React.FC = () => {
const actionRef = useRef();
const { message } = App.useApp();
const [syncModalVisible, setSyncModalVisible] = useState(false);
- const [syncLoading, setSyncLoading] = useState(false);
const [sites, setSites] = useState([]); // 添加站点数据状态
// 获取站点数据
@@ -135,7 +136,6 @@ const CustomerList: React.FC = () => {
return siteId;
}
const site = sites.find((s) => s.id === siteId);
- console.log(`site`, site);
return site ? site.name : String(siteId);
};
@@ -144,11 +144,14 @@ const CustomerList: React.FC = () => {
fetchSites();
}, []);
- const columns: ProColumns[] = [
+ const columns: ProColumns[] = [
{
title: 'ID',
dataIndex: 'id',
- hideInSearch: true,
+ },
+ {
+ title: '原始 ID',
+ dataIndex: 'origin_id',
},
{
title: '站点',
@@ -169,8 +172,9 @@ const CustomerList: React.FC = () => {
return [];
}
},
- render: (siteId: any) => {
- return {getSiteName(siteId) || '-'};
+ render(_, record) {
+ // console.log(`siteId`, record.site_id);
+ return {getSiteName(record.site_id) || '-'};
},
},
{
@@ -254,7 +258,7 @@ const CustomerList: React.FC = () => {
message.error(e?.message || '设置评分失败');
}
}}
- value={record.raw?.rate || 0}
+ value={record.rate || 0}
allowHalf
/>
);
@@ -265,7 +269,7 @@ const CustomerList: React.FC = () => {
dataIndex: 'tags',
hideInSearch: true,
render: (_, record) => {
- const tags = record.raw?.tags || [];
+ const tags = record?.tags || [];
return (
{tags.map((tag: string) => {
@@ -327,7 +331,7 @@ const CustomerList: React.FC = () => {
tableRef={actionRef}
/>
{/* 订单 */}
-
+
);
},
@@ -342,20 +346,59 @@ const CustomerList: React.FC = () => {
actionRef={actionRef}
rowKey="id"
request={async (params, sorter) => {
+ // 获取排序字段和排序方向
const key = Object.keys(sorter)[0];
- const { data, success } = await customercontrollerGetcustomerlist({
- ...params,
- current: params.current?.toString(),
- pageSize: params.pageSize?.toString(),
- ...(key
- ? { sorterKey: key, sorterValue: sorter[key] as string }
- : {}),
- });
+ // 构建过滤条件对象
+ const where: any = {};
+
+ // 添加邮箱过滤
+ if (params.email) {
+ where.email = params.email;
+ }
+
+ // 添加站点ID过滤
+ if (params.site_id) {
+ where.site_id = params.site_id;
+ }
+
+ // 添加用户名过滤
+ if (params.username) {
+ where.username = params.username;
+ }
+
+ // 添加电话过滤
+ if (params.phone) {
+ where.phone = params.phone;
+ }
+
+ // 构建查询参数
+ const queryParams: any = {
+ page: params.current || 1,
+ per_page: params.pageSize || 20,
+ };
+
+ // 添加搜索关键词
+ if (params.fullname) {
+ queryParams.search = params.fullname;
+ }
+
+ // 添加过滤条件(只有在有过滤条件时才添加)
+ if (Object.keys(where).length > 0) {
+ queryParams.where = where;
+ }
+
+ // 添加排序
+ if (key) {
+ queryParams.orderBy = { [key]: sorter[key] as 'asc' | 'desc' };
+ }
+
+ const result = await customercontrollerGetcustomerlist(queryParams);
+ console.log(queryParams, result);
return {
- total: data?.total || 0,
- data: data?.items || [],
- success,
+ total: result?.data?.total || 0,
+ data: result?.data?.items || [],
+ success: true,
};
}}
columns={columns}
@@ -469,6 +512,7 @@ const SyncCustomersModal: React.FC<{
const { message } = App.useApp();
const [sites, setSites] = useState([]);
const [loading, setLoading] = useState(false);
+ const [form] = Form.useForm(); // 添加表单实例
// 获取站点列表
useEffect(() => {
@@ -487,15 +531,56 @@ const SyncCustomersModal: React.FC<{
}
}, [visible]);
- const handleSync = async (values: { siteId: number }) => {
+ // 定义同步参数类型
+ 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) {
@@ -569,6 +654,7 @@ const SyncCustomersModal: React.FC<{
confirmLoading: loading,
}}
onFinish={handleSync}
+ form={form}
>
+
+
+ {
+ return {
+ dateRange: value,
+ };
+ }}
+ fieldProps={{
+ showTime: false,
+ style: { width: '100%' },
+ }}
+ />
+
);
};
diff --git a/src/pages/Customer/Statistic/index.tsx b/src/pages/Customer/Statistic/index.tsx
index 7bd1877..0f758b3 100644
--- a/src/pages/Customer/Statistic/index.tsx
+++ b/src/pages/Customer/Statistic/index.tsx
@@ -129,22 +129,16 @@ const ListPage: React.FC = () => {
},
},
{
- title: 'phone',
+ title: '联系电话',
dataIndex: 'phone',
hideInSearch: true,
render: (_, record) => record?.billing.phone || record?.shipping.phone,
},
{
- title: 'state',
- dataIndex: 'state',
+ title: '账单地址',
+ dataIndex: 'billing',
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',
diff --git a/src/pages/Dict/List/index.tsx b/src/pages/Dict/List/index.tsx
index 54e96f9..858580a 100644
--- a/src/pages/Dict/List/index.tsx
+++ b/src/pages/Dict/List/index.tsx
@@ -17,6 +17,8 @@ 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;
@@ -36,13 +38,10 @@ const DictPage: React.FC = () => {
const [isEditDictModalVisible, setIsEditDictModalVisible] = useState(false);
const [editDictData, setEditDictData] = useState(null);
- // 右侧字典项列表的状态
- const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] =
- useState(false);
- const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] =
- useState(false);
- const [editDictItemData, setEditDictItemData] = useState(null);
- const [dictItemForm] = Form.useForm();
+ // 字典项模态框状态(由 DictItemModal 组件管理)
+ const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
+ const [isEditDictItem, setIsEditDictItem] = useState(false);
+ const [editingDictItemData, setEditingDictItemData] = useState(null);
const actionRef = useRef();
// 获取字典列表
@@ -144,15 +143,16 @@ const DictPage: React.FC = () => {
// 添加字典项
const handleAddDictItem = () => {
- dictItemForm.resetFields();
- setIsAddDictItemModalVisible(true);
+ setIsEditDictItem(false);
+ setEditingDictItemData(null);
+ setIsDictItemModalVisible(true);
};
// 编辑字典项
const handleEditDictItem = (record: any) => {
- setEditDictItemData(record);
- dictItemForm.setFieldsValue(record);
- setIsEditDictItemModalVisible(true);
+ setIsEditDictItem(true);
+ setEditingDictItemData(record);
+ setIsDictItemModalVisible(true);
};
// 删除字典项
@@ -173,53 +173,42 @@ const DictPage: React.FC = () => {
}
};
- // 添加字典项表单提交
- const handleAddDictItemFormSubmit = async (values: any) => {
+ // 处理字典项模态框提交(添加或编辑)
+ const handleDictItemModalOk = async (values: any) => {
try {
- const result = await dictApi.dictcontrollerCreatedictitem({
- ...values,
- dictId: selectedDict.id,
- });
-
- if (!result.success) {
- throw new Error(result.message || '添加失败');
+ 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('添加成功');
}
-
- message.success('添加成功');
- setIsAddDictItemModalVisible(false);
+ setIsDictItemModalVisible(false);
// 强制刷新字典项列表
setTimeout(() => {
actionRef.current?.reload();
}, 100);
} catch (error: any) {
- message.error(`添加失败:${error.message || '未知错误'}`);
- }
- };
-
- // 编辑字典项表单提交
- const handleEditDictItemFormSubmit = async (values: any) => {
- if (!editDictItemData) return;
- try {
- const result = await dictApi.dictcontrollerUpdatedictitem(
- { id: editDictItemData.id },
- values,
+ message.error(
+ `${isEditDictItem ? '更新' : '添加'}失败:${
+ error.message || '未知错误'
+ }`,
);
-
- if (!result.success) {
- throw new Error(result.message || '更新失败');
- }
-
- message.success('更新成功');
- setIsEditDictItemModalVisible(false);
- setEditDictItemData(null);
-
- // 强制刷新字典项列表
- setTimeout(() => {
- actionRef.current?.reload();
- }, 100);
- } catch (error: any) {
- message.error(`更新失败:${error.message || '未知错误'}`);
}
};
@@ -337,13 +326,7 @@ const DictPage: React.FC = () => {
key: 'shortName',
copyable: true,
},
- {
- title: '图片',
- dataIndex: 'image',
- key: 'image',
- valueType: 'image',
- width: 80,
- },
+
{
title: '标题',
dataIndex: 'title',
@@ -356,6 +339,13 @@ const DictPage: React.FC = () => {
key: 'titleCN',
copyable: true,
},
+ {
+ title: '图片',
+ dataIndex: 'image',
+ key: 'image',
+ valueType: 'image',
+ width: 80,
+ },
{
title: '操作',
key: 'action',
@@ -406,7 +396,18 @@ const DictPage: React.FC = () => {
{
+ const { file, onSuccess, onError } = options;
+ try {
+ const result = await dictApi.dictcontrollerImportdicts({}, [
+ file as File,
+ ]);
+ onSuccess?.(result);
+ } catch (error) {
+ onError?.(error as Error);
+ }
+ }}
showUploadList={false}
onChange={(info) => {
if (info.file.status === 'done') {
@@ -495,151 +496,46 @@ const DictPage: React.FC = () => {
size="small"
key={selectedDict?.id}
toolBarRender={() => [
- ,
- {
- const { file, onSuccess, onError } = options;
- try {
- const result =
- await dictApi.dictcontrollerImportdictitems(
- { dictId: selectedDict?.id },
- [file as File],
- );
- onSuccess?.(result);
- } catch (error) {
- onError?.(error as Error);
- }
+ {
+ // 创建 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;
}}
- showUploadList={false}
- disabled={!selectedDict}
- onChange={(info) => {
- console.log(`info`, info);
- if (info.file.status === 'done') {
- message.success(`${info.file.name} 文件上传成功`);
- // 重新加载字典项列表
- setTimeout(() => {
- actionRef.current?.reload();
- }, 100);
- // 重新加载字典列表以更新字典项数量
- fetchDicts();
- } else if (info.file.status === 'error') {
- message.error(`${info.file.name} 文件上传失败`);
- }
- }}
- key="import"
- >
- }>
- 导入字典项
-
- ,
- ,
+ onExport={handleExportDictItems}
+ onAdd={handleAddDictItem}
+ onRefreshDicts={fetchDicts}
+ />,
]}
/>
- {/* 添加字典项 Modal */}
- dictItemForm.submit()}
- onCancel={() => setIsAddDictItemModalVisible(false)}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 编辑字典项 Modal */}
- dictItemForm.submit()}
+ {/* 字典项 Modal(添加或编辑) */}
+ {
- setIsEditDictItemModalVisible(false);
- setEditDictItemData(null);
+ setIsDictItemModalVisible(false);
+ setEditingDictItemData(null);
}}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ onOk={handleDictItemModalOk}
+ />
{/* 添加字典 Modal */}
;
+ // 是否显示导出按钮(某些页面可能不需要导出功能)
+ showExport?: boolean;
+ // 导入字典项的回调函数(如果不提供,则使用默认的导入逻辑)
+ onImport?: (file: File, dictId: number) => Promise;
+ // 导出字典项的回调函数
+ onExport?: () => Promise;
+ // 添加字典项的回调函数
+ onAdd?: () => void;
+ // 刷新字典列表的回调函数(导入成功后可能需要刷新左侧字典列表)
+ onRefreshDicts?: () => void;
+}
+
+// 字典项操作组合组件(包含添加、导入、导出按钮)
+const DictItemActions: React.FC = ({
+ selectedDict,
+ actionRef,
+ showExport = true,
+ onImport,
+ onExport,
+ onAdd,
+ onRefreshDicts,
+}) => {
+ return (
+
+ {/* 添加字典项按钮 */}
+ {onAdd && }
+
+ {/* 导入字典项按钮 */}
+
+
+ {/* 导出字典项按钮 */}
+ {showExport && (
+
+ )}
+
+ );
+};
+
+export default DictItemActions;
diff --git a/src/pages/Dict/components/DictItemAddButton.tsx b/src/pages/Dict/components/DictItemAddButton.tsx
new file mode 100644
index 0000000..7411299
--- /dev/null
+++ b/src/pages/Dict/components/DictItemAddButton.tsx
@@ -0,0 +1,24 @@
+import { Button } from 'antd';
+import React from 'react';
+
+// 字典项添加按钮组件的属性接口
+interface DictItemAddButtonProps {
+ // 是否禁用按钮
+ disabled?: boolean;
+ // 点击按钮时的回调函数
+ onClick: () => void;
+}
+
+// 字典项添加按钮组件
+const DictItemAddButton: React.FC = ({
+ disabled = false,
+ onClick,
+}) => {
+ return (
+
+ );
+};
+
+export default DictItemAddButton;
diff --git a/src/pages/Dict/components/DictItemExportButton.tsx b/src/pages/Dict/components/DictItemExportButton.tsx
new file mode 100644
index 0000000..04e5b2a
--- /dev/null
+++ b/src/pages/Dict/components/DictItemExportButton.tsx
@@ -0,0 +1,53 @@
+import { DownloadOutlined } from '@ant-design/icons';
+import { Button, message } from 'antd';
+import React from 'react';
+
+// 字典项导出按钮组件的属性接口
+interface DictItemExportButtonProps {
+ // 当前选中的字典
+ selectedDict?: any;
+ // 是否禁用按钮
+ disabled?: boolean;
+ // 自定义导出函数
+ onExport?: () => Promise;
+}
+
+// 字典项导出按钮组件
+const DictItemExportButton: React.FC = ({
+ 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 (
+ }
+ onClick={handleExport}
+ disabled={disabled || !selectedDict}
+ >
+ 导出
+
+ );
+};
+
+export default DictItemExportButton;
diff --git a/src/pages/Dict/components/DictItemImportButton.tsx b/src/pages/Dict/components/DictItemImportButton.tsx
new file mode 100644
index 0000000..1b41e5b
--- /dev/null
+++ b/src/pages/Dict/components/DictItemImportButton.tsx
@@ -0,0 +1,124 @@
+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;
+ // 是否禁用按钮
+ disabled?: boolean;
+ // 自定义导入函数,返回 Promise(如果不提供,则使用默认的导入逻辑)
+ onImport?: (file: File, dictId: number) => Promise;
+ // 导入成功后刷新字典列表的回调函数
+ onRefreshDicts?: () => void;
+}
+
+// 字典项导入按钮组件
+const DictItemImportButton: React.FC = ({
+ 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,
+ });
+ }
+
+ // 检查返回结果是否包含 success 字段
+ if (result && result.success !== undefined && !result.success) {
+ throw new Error(result.message || '导入失败');
+ }
+ onSuccess?.(result);
+
+ // 显示导入结果详情
+ 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 (
+
+ }
+ disabled={disabled || !selectedDict}
+ >
+ 导入字典项
+
+
+ );
+};
+
+export default DictItemImportButton;
diff --git a/src/pages/Dict/components/DictItemModal.tsx b/src/pages/Dict/components/DictItemModal.tsx
new file mode 100644
index 0000000..0db2eef
--- /dev/null
+++ b/src/pages/Dict/components/DictItemModal.tsx
@@ -0,0 +1,96 @@
+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;
+}
+
+const DictItemModal: React.FC = ({
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DictItemModal;
diff --git a/src/pages/Order/List/index.tsx b/src/pages/Order/List/index.tsx
index 187f1cb..f4dc9ce 100644
--- a/src/pages/Order/List/index.tsx
+++ b/src/pages/Order/List/index.tsx
@@ -2,6 +2,7 @@ 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 {
@@ -21,6 +22,7 @@ import {
ordercontrollerGetorders,
ordercontrollerRefundorder,
ordercontrollerSyncorderbyid,
+ ordercontrollerSyncorders,
ordercontrollerUpdateorderitems,
} from '@/servers/api/order';
import { productcontrollerSearchproducts } from '@/servers/api/product';
@@ -73,7 +75,6 @@ import {
Tag,
} from 'antd';
import React, { useMemo, useRef, useState } from 'react';
-import { request, useParams } from '@umijs/max';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
const ListPage: React.FC = () => {
@@ -199,7 +200,7 @@ const ListPage: React.FC = () => {
dataIndex: 'keyword',
hideInTable: true,
},
- {
+ {
title: '订单ID',
dataIndex: 'externalOrderId',
},
@@ -250,6 +251,7 @@ const ListPage: React.FC = () => {
{
title: '状态',
dataIndex: 'orderStatus',
+ hideInSearch: true,
valueType: 'select',
valueEnum: ORDER_STATUS_ENUM,
},
@@ -339,15 +341,18 @@ const ListPage: React.FC = () => {
message.error('站点ID或外部订单ID不存在');
return;
}
- const { success, message: errMsg } =
- await ordercontrollerSyncorderbyid({
- siteId: record.siteId,
- orderId: record.externalOrderId,
- });
+ const {
+ success,
+ message: errMsg,
+ data,
+ } = await ordercontrollerSyncorderbyid({
+ siteId: record.siteId,
+ orderId: record.externalOrderId,
+ });
if (!success) {
throw new Error(errMsg);
}
- message.success('同步成功');
+ showSyncResult(data as SyncResultData, '订单');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
@@ -467,16 +472,20 @@ const ListPage: React.FC = () => {
defaultPageSize: 10,
}}
toolBarRender={() => [
- ,
+ // ,
{
try {
- const { success, message: errMsg } =
- await ordercontrollerSyncorderbyid(values);
+ const {
+ success,
+ message: errMsg,
+ data,
+ } = await ordercontrollerSyncorders(values);
if (!success) {
throw new Error(errMsg);
}
- message.success('同步成功');
+ // 使用 showSyncResult 函数显示详细的同步结果
+ showSyncResult(data as SyncResultData, '订单');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
@@ -491,29 +500,19 @@ const ListPage: React.FC = () => {
// >
// 批量导出
// ,
- {
- console.log(selectedRowKeys);
try {
- const res = await request('/order/order/export', {
- method: 'GET',
- params: {
+ const { success, message: errMsg } =
+ await ordercontrollerExportorder({
ids: selectedRowKeys,
- }
- });
- 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 = 'customers.csv';
- a.click();
- URL.revokeObjectURL(url);
- } else {
- message.error(res.message || '导出失败');
+ });
+ if (!success) {
+ throw new Error(errMsg);
}
+ message.success('导出成功');
actionRef.current?.reload();
setSelectedRowKeys([]);
} catch (error: any) {
@@ -521,10 +520,14 @@ const ListPage: React.FC = () => {
}
}}
>
-
+ ,
]}
request={async ({ date, ...param }: any) => {
if (param.status === 'all') {
@@ -622,15 +625,18 @@ const Detail: React.FC<{
message.error('站点ID或外部订单ID不存在');
return;
}
- const { success, message: errMsg } =
- await ordercontrollerSyncorderbyid({
- siteId: record.siteId,
- orderId: record.externalOrderId,
- });
+ const {
+ success,
+ message: errMsg,
+ data,
+ } = await ordercontrollerSyncorderbyid({
+ siteId: record.siteId,
+ orderId: record.externalOrderId,
+ });
if (!success) {
throw new Error(errMsg);
}
- message.success('同步成功');
+ showSyncResult(data as SyncResultData, '订单');
tableRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
@@ -2122,12 +2128,12 @@ const SalesChange: React.FC<{
params={{}}
request={async ({ keyWords }) => {
try {
- const { data } = await wpproductcontrollerSearchproducts({
+ const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return data?.map((item) => {
return {
- label: `${item.name}`,
+ label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
});
diff --git a/src/pages/Product/Attribute/index.tsx b/src/pages/Product/Attribute/index.tsx
index f7cce34..5fad811 100644
--- a/src/pages/Product/Attribute/index.tsx
+++ b/src/pages/Product/Attribute/index.tsx
@@ -1,22 +1,14 @@
-import { UploadOutlined } from '@ant-design/icons';
+import * as dictApi from '@/servers/api/dict';
import {
ActionType,
PageContainer,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
-import {
- Button,
- Form,
- Input,
- Layout,
- Modal,
- Space,
- Table,
- Upload,
- message,
-} from 'antd';
+import { Button, Input, Layout, Space, Table, 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;
@@ -32,10 +24,84 @@ const AttributePage: React.FC = () => {
// 右侧字典项 ProTable 的引用
const actionRef = useRef();
- // 字典项新增/编辑模态框控制
+ // 字典项模态框状态(由 DictItemModal 组件管理)
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
- const [editingDictItem, setEditingDictItem] = useState(null);
- const [dictItemForm] = Form.useForm();
+ const [isEditDictItem, setIsEditDictItem] = useState(false);
+ const [editingDictItemData, setEditingDictItemData] = useState(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 fetchDicts = async (title?: string) => {
setLoadingDicts(true);
@@ -65,24 +131,24 @@ const AttributePage: React.FC = () => {
// 打开添加字典项模态框
const handleAddDictItem = () => {
- setEditingDictItem(null);
- dictItemForm.resetFields();
+ setIsEditDictItem(false);
+ setEditingDictItemData(null);
setIsDictItemModalVisible(true);
};
// 打开编辑字典项模态框
const handleEditDictItem = (item: any) => {
- setEditingDictItem(item);
- dictItemForm.setFieldsValue(item);
+ setIsEditDictItem(true);
+ setEditingDictItemData(item);
setIsDictItemModalVisible(true);
};
// 字典项表单提交(新增或编辑)
const handleDictItemFormSubmit = async (values: any) => {
try {
- if (editingDictItem) {
+ if (isEditDictItem && editingDictItemData) {
// 条件判断,存在编辑项则执行更新
- await request(`/dict/item/${editingDictItem.id}`, {
+ await request(`/dict/item/${editingDictItemData.id}`, {
method: 'PUT',
data: values,
});
@@ -98,7 +164,7 @@ const AttributePage: React.FC = () => {
setIsDictItemModalVisible(false);
actionRef.current?.reload(); // 刷新 ProTable
} catch (error) {
- message.error(editingDictItem ? '更新失败' : '添加失败');
+ message.error(isEditDictItem ? '更新失败' : '添加失败');
}
};
@@ -296,85 +362,45 @@ const AttributePage: React.FC = () => {
size="small"
key={selectedDict?.id}
headerTitle={
-
-
- 添加字典项
-
- {
- // 条件判断,上传状态处理
- if (info.file.status === 'done') {
- message.success(`${info.file.name} 文件上传成功`);
- actionRef.current?.reload();
- } else if (info.file.status === 'error') {
- message.error(`${info.file.name} 文件上传失败`);
- }
- }}
- >
- }
- disabled={!selectedDict}
- >
- 导入字典项
-
-
-
+ {
+ // 创建 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}
+ />
}
/>
- dictItemForm.submit()}
- onCancel={() => setIsDictItemModalVisible(false)}
- destroyOnClose
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {/* 字典项 Modal(添加或编辑) */}
+ {
+ setIsDictItemModalVisible(false);
+ setEditingDictItemData(null);
+ }}
+ onOk={handleDictItemFormSubmit}
+ />
);
};
diff --git a/src/pages/Product/List/CreateForm.tsx b/src/pages/Product/List/CreateForm.tsx
index 270a70f..2243a7f 100644
--- a/src/pages/Product/List/CreateForm.tsx
+++ b/src/pages/Product/List/CreateForm.tsx
@@ -137,8 +137,8 @@ const CreateForm: React.FC<{
humidityName === 'dry'
? 'Dry'
: humidityName === 'moisture'
- ? 'Moisture'
- : capitalize(humidityName),
+ ? 'Moisture'
+ : capitalize(humidityName),
},
);
if (!success) {
@@ -246,13 +246,7 @@ const CreateForm: React.FC<{
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
-
+
自动生成
@@ -265,6 +259,14 @@ const CreateForm: React.FC<{
)}
+
void;
+ products: API.Product[];
+ site?: any;
+ onSuccess: () => void;
+}
+
+const SyncToSiteModal: React.FC = ({
+ visible,
+ onClose,
+ products,
+ site,
+ onSuccess,
+}) => {
+ const { message } = App.useApp();
+ const [sites, setSites] = useState([]);
+ const formRef = useRef();
+
+ // 生成单个产品的站点SKU
+ const generateSingleSiteSku = async (
+ currentSite: API.Site,
+ product: API.Product,
+ ): Promise => {
+ 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 = {};
+ 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 (
+ !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) => {
+ 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}`,
+ }));
+
+ const result = await productcontrollerBatchsynctosite({
+ siteId: values.siteId,
+ data,
+ });
+
+ showBatchOperationResult(result, '同步到站点');
+ onSuccess();
+ return true;
+ } catch (error: any) {
+ message.error(error.message || '同步失败');
+ return false;
+ }
+ }}
+ >
+ ({ label: site.name, value: site.id }))}
+ rules={[{ required: true, message: '请选择站点' }]}
+ />
+ {products.map((row) => (
+
+ {({ siteId }) => (
+
+
+
原始SKU: {row.sku || '-'}
+
+ 商品SKU:{' '}
+ {row.siteSkus && row.siteSkus.length > 0
+ ? row.siteSkus.map((siteSku: string, idx: number) => (
+
+ {siteSku}
+
+ ))
+ : '-'}
+
+
+
+
+
{
+ // 手动输入时更新表单值
+ const currentValues = formRef.current?.getFieldValue('siteSkus') || {};
+ currentValues[row.id] = e.target.value;
+ formRef.current?.setFieldsValue({ siteSkus: currentValues });
+ },
+ }}
+ />
+
+
{
+ 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 });
+ }
+ }}
+ >
+ 自动生成
+
+
+
+ )}
+
+ ))}
+
+ );
+};
+
+export default SyncToSiteModal;
diff --git a/src/pages/Product/List/index.tsx b/src/pages/Product/List/index.tsx
index 3f3bd9f..2d8e9c2 100644
--- a/src/pages/Product/List/index.tsx
+++ b/src/pages/Product/List/index.tsx
@@ -1,15 +1,12 @@
import {
productcontrollerBatchdeleteproduct,
productcontrollerBatchupdateproduct,
- productcontrollerBindproductsiteskus,
productcontrollerDeleteproduct,
productcontrollerGetcategoriesall,
productcontrollerGetproductcomponents,
productcontrollerGetproductlist,
- productcontrollerUpdatenamecn,
+ productcontrollerUpdatenamecn
} from '@/servers/api/product';
-import { sitecontrollerAll } from '@/servers/api/site';
-import { siteapicontrollerGetproducts } from '@/servers/api/siteApi';
import {
ActionType,
ModalForm,
@@ -17,13 +14,14 @@ import {
ProColumns,
ProFormSelect,
ProFormText,
- ProTable,
+ ProTable
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
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;
@@ -166,261 +164,14 @@ const BatchEditModal: React.FC<{
);
};
-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([]);
- const formRef = useRef();
-
- useEffect(() => {
- if (visible) {
- sitecontrollerAll().then((res: any) => {
- setSites(res?.data || []);
- });
- }
- }, [visible]);
-
- return (
- !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 = {};
- 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;
- }
- }}
- >
- ({ label: site.name, value: site.id }))}
- rules={[{ required: true, message: '请选择站点' }]}
- />
- {productRows.map((row) => (
-
-
原始SKU: {row.sku || '-'}
-
-
-
- ))}
-
- );
-};
-
-const WpProductInfo: React.FC<{
- skus: string[];
- record: API.Product;
- parentTableRef: React.MutableRefObject;
-}> = ({ skus, record, parentTableRef }) => {
- const actionRef = useRef();
- const { message } = App.useApp();
-
- return (
- [
- actionRef.current?.reload()}
- >
- 刷新
- ,
- ]}
- 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) => (
-
-
常规: {row.regular_price}
-
促销: {row.sale_price}
-
- ),
- },
- {
- title: '状态',
- dataIndex: 'status',
- },
- {
- title: '操作',
- valueType: 'option',
- render: (_, wpRow) => [
- {
- try {
- await wpproductcontrollerBatchsynctosite(
- { siteId: wpRow.siteId },
- { productIds: [record.id] },
- );
- message.success('同步到站点成功');
- actionRef.current?.reload();
- } catch (e: any) {
- message.error(e.message || '同步失败');
- }
- }}
- >
- 同步到站点
- ,
- {
- try {
- await wpproductcontrollerSynctoproduct({ id: wpRow.id });
- message.success('同步进商品成功');
- parentTableRef.current?.reload();
- } catch (e: any) {
- message.error(e.message || '同步失败');
- }
- }}
- >
- 同步进商品
- ,
- {
- try {
- await request(`/wp_product/${wpRow.id}`, {
- method: 'DELETE',
- });
- message.success('删除成功');
- actionRef.current?.reload();
- } catch (e: any) {
- message.error(e.message || '删除失败');
- }
- }}
- >
- 删除
- ,
- ],
- },
- ]}
- />
- );
-};
const List: React.FC = () => {
const actionRef = useRef();
// 状态:存储当前选中的行
const [selectedRows, setSelectedRows] = React.useState([]);
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
+ const [syncProducts, setSyncProducts] = useState([]);
const [syncModalVisible, setSyncModalVisible] = useState(false);
- const [syncProductIds, setSyncProductIds] = useState([]);
const { message } = App.useApp();
// 导出产品 CSV(带认证请求)
@@ -460,7 +211,7 @@ const List: React.FC = () => {
<>
{record.siteSkus?.map((siteSku, index) => (
- {siteSku.siteSku}
+ {siteSku}
))}
>
@@ -564,7 +315,7 @@ const List: React.FC = () => {
{
- setSyncProductIds([record.id]);
+ setSyncProducts([record]);
setSyncModalVisible(true);
}}
>
@@ -693,7 +444,7 @@ const List: React.FC = () => {
{
- setSyncProductIds(selectedRows.map((row) => row.id));
+ setSyncProducts(selectedRows);
setSyncModalVisible(true);
}}
>
@@ -756,7 +507,7 @@ const List: React.FC = () => {
columns={columns}
expandable={{
expandedRowRender: (record) => (
- {
setSyncModalVisible(false)}
- productIds={syncProductIds}
- productRows={selectedRows}
+ products={syncProducts}
onSuccess={() => {
setSyncModalVisible(false);
setSelectedRows([]);
diff --git a/src/pages/Product/Permutation/index.tsx b/src/pages/Product/Permutation/index.tsx
index 9f655b9..8ef0e13 100644
--- a/src/pages/Product/Permutation/index.tsx
+++ b/src/pages/Product/Permutation/index.tsx
@@ -3,6 +3,7 @@ import {
productcontrollerGetcategoryattributes,
productcontrollerGetproductlist,
} from '@/servers/api/product';
+import { DownloadOutlined } from '@ant-design/icons';
import {
ActionType,
PageContainer,
@@ -201,6 +202,92 @@ 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,
@@ -317,7 +404,16 @@ const PermutationPage: React.FC = () => {
}}
scroll={{ x: 'max-content' }}
search={false}
- toolBarRender={false}
+ toolBarRender={() => [
+ }
+ onClick={handleExport}
+ >
+ 导出列表
+ ,
+ ]}
/>
)}
diff --git a/src/pages/Product/Sync/SiteProductCell.tsx b/src/pages/Product/Sync/SiteProductCell.tsx
new file mode 100644
index 0000000..ec14968
--- /dev/null
+++ b/src/pages/Product/Sync/SiteProductCell.tsx
@@ -0,0 +1,461 @@
+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 = ({
+ product,
+ site,
+ onSyncSuccess,
+}) => {
+ // 存储该站点对应的产品数据
+ const [siteProduct, setSiteProduct] = useState(null);
+ // 存储本地产品完整数据
+ const [localProduct, setLocalProduct] = useState(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 => {
+ 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 => {
+ 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 (
+
+
+
+ );
+ }
+
+ // 如果没有找到站点产品,显示同步按钮
+ if (!siteProduct) {
+ // 首先查找该产品在该站点的实际SKU
+ // 注意:siteSkus 现在是字符串数组,无法直接匹配站点
+ // 这里使用模板生成的 SKU 作为默认值
+ let siteProductSku = '';
+ // 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
+
+ const defaultSku =
+ siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
+
+ return (
+ }>
+ 同步到站点
+
+ }
+ width={400}
+ onFinish={async (values) => {
+ return await syncProductToSite(values);
+ }}
+ initialValues={{
+ sku: defaultSku,
+ }}
+ >
+
+
+ );
+ }
+
+ // 显示站点产品信息
+ return (
+
+
+
{siteProduct.sku}
+
}
+ >
+ 更新
+
+ }
+ width={400}
+ onFinish={async (values) => {
+ return await updateSyncProduct(values);
+ }}
+ initialValues={{
+ sku: siteProduct.sku,
+ }}
+ >
+
+
+ 确定要将本地产品数据更新到站点吗?
+
+
+
+
Price: {siteProduct.regular_price ?? siteProduct.price}
+ {siteProduct.sale_price && (
+
Sale: {siteProduct.sale_price}
+ )}
+
+ Stock: {siteProduct.stock_quantity ?? siteProduct.stockQuantity}
+
+
+ Status:{' '}
+ {siteProduct.status === 'publish' ? (
+ Published
+ ) : (
+ {siteProduct.status}
+ )}
+
+
+ );
+};
+
+export default SiteProductCell;
diff --git a/src/pages/Product/Sync/index.tsx b/src/pages/Product/Sync/index.tsx
index bd6c9ae..6afb087 100644
--- a/src/pages/Product/Sync/index.tsx
+++ b/src/pages/Product/Sync/index.tsx
@@ -1,13 +1,11 @@
-import { productcontrollerGetproductlist } from '@/servers/api/product';
-import { templatecontrollerGettemplatebyname } from '@/servers/api/template';
-import { EditOutlined, SyncOutlined } from '@ant-design/icons';
+import { showBatchOperationResult } from '@/utils/showResult';
import {
- ActionType,
- ModalForm,
- ProColumns,
- ProFormText,
- ProTable,
-} from '@ant-design/pro-components';
+ 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,
@@ -21,39 +19,35 @@ import {
} from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import EditForm from '../List/EditForm';
+import SiteProductCell from './SiteProductCell';
// 定义站点接口
interface Site {
- id: string;
+ id: number;
name: string;
skuPrefix?: string;
isDisabled?: boolean;
}
-// 定义WordPress商品接口
-interface WpProduct {
- id?: number;
- externalProductId?: string;
+// 定义本地产品接口(与后端 Product 实体匹配)
+interface SiteProduct {
+ id: number;
sku: string;
name: string;
- price: string;
- regular_price?: string;
- sale_price?: string;
- stock_quantity: number;
- stockQuantity?: number;
- status: string;
+ nameCn: string;
+ shortDescription?: string;
+ description?: string;
+ price: number;
+ promotionPrice: number;
+ type: string;
+ categoryId?: number;
+ category?: any;
attributes?: any[];
- constitution?: { sku: string; quantity: number }[];
-}
-
-// 扩展本地产品接口,包含对应的 WP 产品信息
-interface ProductWithWP extends API.Product {
- wpProducts: Record;
- attributes?: any[];
- siteSkus?: Array<{
- siteSku: string;
- [key: string]: any;
- }>;
+ components?: any[];
+ siteSkus: string[];
+ source: number;
+ createdAt: Date;
+ updatedAt: Date;
}
// 定义API响应接口
@@ -79,19 +73,9 @@ const getSites = async (): Promise> => {
};
};
-const getWPProducts = async (): Promise> => {
- return request('/product/wp-products', {
- method: 'GET',
- });
-};
-
const ProductSyncPage: React.FC = () => {
const [sites, setSites] = useState([]);
- // 存储所有 WP 产品,用于查找匹配。 Key: SKU (包含前缀)
- const [wpProductMap, setWpProductMap] = useState