forked from yoone/WEB
Compare commits
13 Commits
32e2759151
...
d372f72935
| Author | SHA1 | Date |
|---|---|---|
|
|
d372f72935 | |
|
|
7247015e4c | |
|
|
6677114c46 | |
|
|
4fd9cfee75 | |
|
|
82c1bfa711 | |
|
|
bb907fa47c | |
|
|
ce12e55adc | |
|
|
e498793678 | |
|
|
2467a0c3b2 | |
|
|
5471f7a038 | |
|
|
e23b0b3e39 | |
|
|
6d97ecbcc7 | |
|
|
6f35c2aee5 |
|
|
@ -170,6 +170,11 @@ export default defineConfig({
|
|||
path: '/product/sync',
|
||||
component: './Product/Sync',
|
||||
},
|
||||
{
|
||||
name: '产品CSV 工具',
|
||||
path: '/product/csvtool',
|
||||
component: './Product/CsvTool',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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: (
|
||||
<div>
|
||||
<div>{resultMessage}</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
|
||||
失败详情:
|
||||
{errors
|
||||
.slice(0, 3)
|
||||
.map((err: any) => `${err.identifier}: ${err.error}`)
|
||||
.join(', ')}
|
||||
{errors.length > 3 && ` 等 ${errors.length - 3} 个错误...`}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
duration: 8,
|
||||
key: 'sync-result',
|
||||
});
|
||||
} else {
|
||||
// 完全成功
|
||||
message.success({
|
||||
content: resultMessage,
|
||||
duration: 4,
|
||||
key: 'sync-result',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 同步结果显示组件
|
||||
const SyncResultMessage: React.FC<SyncResultMessageProps> = ({
|
||||
data,
|
||||
entityType = '订单',
|
||||
}) => {
|
||||
// 当组件挂载时显示结果
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
showSyncResult(data, entityType);
|
||||
}
|
||||
}, [data, entityType]);
|
||||
|
||||
// 这个组件不渲染任何内容,只用于显示消息
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SyncResultMessage;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { ordercontrollerGetorders } from '@/servers/api/order';
|
||||
import { siteapicontrollerGetorders } from '@/servers/api/siteApi';
|
||||
import {
|
||||
App,
|
||||
Col,
|
||||
|
|
@ -61,7 +60,7 @@ const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
|
|||
const items = order.line_items || order.items || [];
|
||||
if (Array.isArray(items)) {
|
||||
items.forEach((item: any) => {
|
||||
// 检查商品名称或SKU是否包含yoone(不区分大小写)
|
||||
// 检查商品名称或SKU是否包含yoone(不区分大小写)
|
||||
const itemName = (item.name || '').toLowerCase();
|
||||
const sku = (item.sku || '').toLowerCase();
|
||||
|
||||
|
|
@ -89,12 +88,12 @@ const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
|
|||
|
||||
// 获取客户订单数据
|
||||
const fetchOrders = async () => {
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await ordercontrollerGetorders({
|
||||
customer_email: customer.email,
|
||||
where: {
|
||||
customer_email: customer.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (response) {
|
||||
|
|
@ -242,7 +241,7 @@ const HistoryOrders: React.FC<HistoryOrdersProps> = ({ customer, siteId }) => {
|
|||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,42 +115,17 @@ const CustomerList: React.FC = () => {
|
|||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const [sites, setSites] = useState<any[]>([]); // 添加站点数据状态
|
||||
|
||||
// 获取站点数据
|
||||
const fetchSites = async () => {
|
||||
try {
|
||||
const { data, success } = await sitecontrollerAll();
|
||||
if (success) {
|
||||
setSites(data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取站点数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据站点ID获取站点名称
|
||||
const getSiteName = (siteId: number | undefined | null) => {
|
||||
if (!siteId) return '-';
|
||||
if (typeof siteId === 'string') {
|
||||
return siteId;
|
||||
}
|
||||
const site = sites.find((s) => s.id === siteId);
|
||||
console.log(`site`, site);
|
||||
return site ? site.name : String(siteId);
|
||||
};
|
||||
|
||||
// 组件加载时获取站点数据
|
||||
useEffect(() => {
|
||||
fetchSites();
|
||||
}, []);
|
||||
|
||||
const columns: ProColumns<API.UnifiedCustomerDTO>[] = [
|
||||
const columns: ProColumns<API.GetCustomerDTO>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '原始 ID',
|
||||
dataIndex: 'origin_id',
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '站点',
|
||||
|
|
@ -169,9 +146,6 @@ const CustomerList: React.FC = () => {
|
|||
return [];
|
||||
}
|
||||
},
|
||||
render: (siteId: any) => {
|
||||
return <span>{getSiteName(siteId) || '-'}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '头像',
|
||||
|
|
@ -203,24 +177,26 @@ const CustomerList: React.FC = () => {
|
|||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
hideInSearch: true,
|
||||
dataIndex: 'username',
|
||||
copyable: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
copyable: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
hideInSearch: true,
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
title: '账单地址',
|
||||
dataIndex: 'billing',
|
||||
hideInSearch: true,
|
||||
|
||||
width: 200,
|
||||
render: (billing) => <AddressCell address={billing} title="账单地址" />,
|
||||
},
|
||||
|
|
@ -234,8 +210,8 @@ const CustomerList: React.FC = () => {
|
|||
{
|
||||
title: '评分',
|
||||
dataIndex: 'rate',
|
||||
width: 120,
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Rate
|
||||
|
|
@ -254,7 +230,7 @@ const CustomerList: React.FC = () => {
|
|||
message.error(e?.message || '设置评分失败');
|
||||
}
|
||||
}}
|
||||
value={record.raw?.rate || 0}
|
||||
value={record.rate || 0}
|
||||
allowHalf
|
||||
/>
|
||||
);
|
||||
|
|
@ -265,7 +241,7 @@ const CustomerList: React.FC = () => {
|
|||
dataIndex: 'tags',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
const tags = record.raw?.tags || [];
|
||||
const tags = record?.tags || [];
|
||||
return (
|
||||
<Space size={[0, 8]} wrap>
|
||||
{tags.map((tag: string) => {
|
||||
|
|
@ -327,7 +303,7 @@ const CustomerList: React.FC = () => {
|
|||
tableRef={actionRef}
|
||||
/>
|
||||
{/* 订单 */}
|
||||
<HistoryOrders customer={record} siteId={record.raw?.site_id} />
|
||||
<HistoryOrders customer={record} siteId={record.site_id} />
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
|
|
@ -340,25 +316,35 @@ const CustomerList: React.FC = () => {
|
|||
scroll={{ x: 'max-content' }}
|
||||
headerTitle="查询表格"
|
||||
actionRef={actionRef}
|
||||
columns={columns}
|
||||
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 }
|
||||
: {}),
|
||||
});
|
||||
request={async (params, sorter,filter) => {
|
||||
console.log('custoemr request',params, sorter,filter)
|
||||
const { current, pageSize, ...restParams } = params;
|
||||
const orderBy:any = {}
|
||||
Object.entries(sorter).forEach(([key, value]) => {
|
||||
orderBy[key] = value === 'ascend' ? 'asc' : 'desc';
|
||||
})
|
||||
// 构建查询参数
|
||||
const queryParams: any = {
|
||||
page: current || 1,
|
||||
per_page: pageSize || 20,
|
||||
where: {
|
||||
...filter,
|
||||
...restParams
|
||||
},
|
||||
orderBy
|
||||
};
|
||||
|
||||
const result = await customercontrollerGetcustomerlist({params: queryParams});
|
||||
console.log(queryParams, result);
|
||||
return {
|
||||
total: data?.total || 0,
|
||||
data: data?.items || [],
|
||||
success,
|
||||
total: result?.data?.total || 0,
|
||||
data: result?.data?.items || [],
|
||||
success: true,
|
||||
};
|
||||
}}
|
||||
columns={columns}
|
||||
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
span: 6,
|
||||
|
|
@ -366,8 +352,7 @@ const CustomerList: React.FC = () => {
|
|||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
|
|
@ -469,6 +454,7 @@ const SyncCustomersModal: React.FC<{
|
|||
const { message } = App.useApp();
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm(); // 添加表单实例
|
||||
|
||||
// 获取站点列表
|
||||
useEffect(() => {
|
||||
|
|
@ -487,15 +473,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) {
|
||||
|
|
@ -509,7 +536,7 @@ const SyncCustomersModal: React.FC<{
|
|||
errors = [],
|
||||
} = result;
|
||||
|
||||
let resultMessage = `同步完成!共处理 ${total} 个客户:`;
|
||||
let resultMessage = `同步完成!共处理 ${total} 个客户:`;
|
||||
if (created > 0) resultMessage += ` 新建 ${created} 个`;
|
||||
if (updated > 0) resultMessage += ` 更新 ${updated} 个`;
|
||||
if (synced > 0) resultMessage += ` 同步成功 ${synced} 个`;
|
||||
|
|
@ -522,7 +549,7 @@ const SyncCustomersModal: React.FC<{
|
|||
<div>
|
||||
<div>{resultMessage}</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#faad14' }}>
|
||||
失败详情:
|
||||
失败详情:
|
||||
{errors
|
||||
.slice(0, 3)
|
||||
.map((err: any) => err.email || err.error)
|
||||
|
|
@ -569,6 +596,7 @@ const SyncCustomersModal: React.FC<{
|
|||
confirmLoading: loading,
|
||||
}}
|
||||
onFinish={handleSync}
|
||||
form={form}
|
||||
>
|
||||
<ProFormSelect
|
||||
name="siteId"
|
||||
|
|
@ -583,6 +611,59 @@ const SyncCustomersModal: React.FC<{
|
|||
loading: loading,
|
||||
}}
|
||||
/>
|
||||
<ProFormText
|
||||
name="search"
|
||||
label="搜索关键词"
|
||||
placeholder="输入邮箱、姓名或用户名进行搜索"
|
||||
tooltip="支持搜索邮箱、姓名、用户名等字段"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="role"
|
||||
label="客户角色"
|
||||
placeholder="选择客户角色进行过滤"
|
||||
options={[
|
||||
{ label: '所有角色', value: '' },
|
||||
{ label: '管理员', value: 'administrator' },
|
||||
{ label: '编辑', value: 'editor' },
|
||||
{ label: '作者', value: 'author' },
|
||||
{ label: '订阅者', value: 'subscriber' },
|
||||
{ label: '客户', value: 'customer' },
|
||||
]}
|
||||
fieldProps={{
|
||||
allowClear: true,
|
||||
}}
|
||||
/>
|
||||
<ProFormDateTimeRangePicker
|
||||
name="dateRange"
|
||||
label="注册日期范围"
|
||||
placeholder={['开始日期', '结束日期']}
|
||||
transform={(value) => {
|
||||
return {
|
||||
dateRange: value,
|
||||
};
|
||||
}}
|
||||
fieldProps={{
|
||||
showTime: false,
|
||||
style: { width: '100%' },
|
||||
}}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="orderBy"
|
||||
label="排序方式"
|
||||
placeholder="选择排序方式"
|
||||
options={[
|
||||
{ label: '默认排序', value: '' },
|
||||
{ label: '注册时间(升序)', value: 'date_created:asc' },
|
||||
{ label: '注册时间(降序)', value: 'date_created:desc' },
|
||||
{ label: '邮箱(升序)', value: 'email:asc' },
|
||||
{ label: '邮箱(降序)', value: 'email:desc' },
|
||||
{ label: '姓名(升序)', value: 'first_name:asc' },
|
||||
{ label: '姓名(降序)', value: 'first_name:desc' },
|
||||
]}
|
||||
fieldProps={{
|
||||
allowClear: true,
|
||||
}}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -129,21 +129,15 @@ const ListPage: React.FC = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
title: 'phone',
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.phone || record?.shipping.phone,
|
||||
},
|
||||
{
|
||||
title: 'state',
|
||||
dataIndex: 'state',
|
||||
render: (_, record) => record?.billing.state || record?.shipping.state,
|
||||
},
|
||||
{
|
||||
title: 'city',
|
||||
dataIndex: 'city',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record?.billing.city || record?.shipping.city,
|
||||
title: '账单地址',
|
||||
dataIndex: 'billing',
|
||||
render: (_, record) => JSON.stringify(record?.billing || record?.shipping),
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
|
||||
// 右侧字典项列表的状态
|
||||
const [isAddDictItemModalVisible, setIsAddDictItemModalVisible] =
|
||||
useState(false);
|
||||
const [isEditDictItemModalVisible, setIsEditDictItemModalVisible] =
|
||||
useState(false);
|
||||
const [editDictItemData, setEditDictItemData] = useState<any>(null);
|
||||
const [dictItemForm] = Form.useForm();
|
||||
// 字典项模态框状态(由 DictItemModal 组件管理)
|
||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
||||
const [isEditDictItem, setIsEditDictItem] = useState(false);
|
||||
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
||||
// 获取字典列表
|
||||
|
|
@ -138,21 +137,22 @@ const DictPage: React.FC = () => {
|
|||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
message.error('下载字典模板失败:' + (error.message || '未知错误'));
|
||||
message.error('下载字典模板失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 添加字典项
|
||||
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 || '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -286,7 +275,7 @@ const DictPage: React.FC = () => {
|
|||
|
||||
message.success(`成功导出 ${response.length} 条数据`);
|
||||
} catch (error: any) {
|
||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
||||
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 = () => {
|
|||
</Button>
|
||||
<Upload
|
||||
name="file"
|
||||
action="/api/dict/import"
|
||||
action={undefined}
|
||||
customRequest={async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
try {
|
||||
const result = await dictApi.dictcontrollerImportdicts({}, [
|
||||
file as File,
|
||||
]);
|
||||
onSuccess?.(result);
|
||||
} catch (error) {
|
||||
onError?.(error as Error);
|
||||
}
|
||||
}}
|
||||
showUploadList={false}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === 'done') {
|
||||
|
|
@ -476,7 +477,7 @@ const DictPage: React.FC = () => {
|
|||
};
|
||||
}
|
||||
|
||||
// 兼容旧的响应格式(直接返回数组)
|
||||
// 兼容旧的响应格式(直接返回数组)
|
||||
return {
|
||||
data: res || [],
|
||||
success: true,
|
||||
|
|
@ -495,151 +496,46 @@ const DictPage: React.FC = () => {
|
|||
size="small"
|
||||
key={selectedDict?.id}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleAddDictItem}
|
||||
disabled={!selectedDict}
|
||||
size="small"
|
||||
key="add"
|
||||
>
|
||||
添加字典项
|
||||
</Button>,
|
||||
<Upload
|
||||
name="file"
|
||||
action={undefined}
|
||||
customRequest={async (options) => {
|
||||
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);
|
||||
}
|
||||
<DictItemActions
|
||||
key="dictItemActions"
|
||||
selectedDict={selectedDict}
|
||||
actionRef={actionRef}
|
||||
showExport={true}
|
||||
onImport={async (file: File, dictId: number) => {
|
||||
// 创建 FormData 对象
|
||||
const formData = new FormData();
|
||||
// 添加文件到 FormData
|
||||
formData.append('file', file);
|
||||
// 添加字典 ID 到 FormData
|
||||
formData.append('dictId', String(dictId));
|
||||
// 调用导入字典项的 API,直接返回解析后的 JSON 对象
|
||||
const result = await dictApi.dictcontrollerImportdictitems(
|
||||
formData,
|
||||
);
|
||||
return result;
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<Button size="small" icon={<UploadOutlined />}>
|
||||
导入字典项
|
||||
</Button>
|
||||
</Upload>,
|
||||
<Button
|
||||
onClick={handleExportDictItems}
|
||||
disabled={!selectedDict}
|
||||
size="small"
|
||||
key="export"
|
||||
>
|
||||
导出数据
|
||||
</Button>,
|
||||
onExport={handleExportDictItems}
|
||||
onAdd={handleAddDictItem}
|
||||
onRefreshDicts={fetchDicts}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
{/* 添加字典项 Modal */}
|
||||
<Modal
|
||||
title={`添加字典项 - ${selectedDict?.title || '未选择字典'}`}
|
||||
open={isAddDictItemModalVisible}
|
||||
onOk={() => dictItemForm.submit()}
|
||||
onCancel={() => setIsAddDictItemModalVisible(false)}
|
||||
>
|
||||
<Form
|
||||
form={dictItemForm}
|
||||
layout="vertical"
|
||||
onFinish={handleAddDictItemFormSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="名称 (e.g., zyn)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入标题' }]}
|
||||
>
|
||||
<Input placeholder="标题 (e.g., ZYN)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="中文标题" name="titleCN">
|
||||
<Input placeholder="中文标题 (e.g., 品牌)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称 (可选)" name="shortName">
|
||||
<Input placeholder="简称 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片 (可选)" name="image">
|
||||
<Input placeholder="图片链接 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="值 (可选)" name="value">
|
||||
<Input placeholder="值 (可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑字典项 Modal */}
|
||||
<Modal
|
||||
title="编辑字典项"
|
||||
open={isEditDictItemModalVisible}
|
||||
onOk={() => dictItemForm.submit()}
|
||||
{/* 字典项 Modal(添加或编辑) */}
|
||||
<DictItemModal
|
||||
visible={isDictItemModalVisible}
|
||||
isEdit={isEditDictItem}
|
||||
editingData={editingDictItemData}
|
||||
selectedDict={selectedDict}
|
||||
onCancel={() => {
|
||||
setIsEditDictItemModalVisible(false);
|
||||
setEditDictItemData(null);
|
||||
setIsDictItemModalVisible(false);
|
||||
setEditingDictItemData(null);
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
form={dictItemForm}
|
||||
layout="vertical"
|
||||
onFinish={handleEditDictItemFormSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="名称 (e.g., zyn)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入标题' }]}
|
||||
>
|
||||
<Input placeholder="标题 (e.g., ZYN)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="中文标题" name="titleCN">
|
||||
<Input placeholder="中文标题 (e.g., 品牌)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称 (可选)" name="shortName">
|
||||
<Input placeholder="简称 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片 (可选)" name="image">
|
||||
<Input placeholder="图片链接 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="值 (可选)" name="value">
|
||||
<Input placeholder="值 (可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
onOk={handleDictItemModalOk}
|
||||
/>
|
||||
|
||||
{/* 添加字典 Modal */}
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { ActionType } from '@ant-design/pro-components';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import DictItemAddButton from './DictItemAddButton';
|
||||
import DictItemExportButton from './DictItemExportButton';
|
||||
import DictItemImportButton from './DictItemImportButton';
|
||||
|
||||
// 字典项操作组合组件的属性接口
|
||||
interface DictItemActionsProps {
|
||||
// 当前选中的字典
|
||||
selectedDict?: any;
|
||||
// ProTable 的 actionRef,用于刷新列表
|
||||
actionRef?: React.MutableRefObject<ActionType | undefined>;
|
||||
// 是否显示导出按钮(某些页面可能不需要导出功能)
|
||||
showExport?: boolean;
|
||||
// 导入字典项的回调函数(如果不提供,则使用默认的导入逻辑)
|
||||
onImport?: (file: File, dictId: number) => Promise<any>;
|
||||
// 导出字典项的回调函数
|
||||
onExport?: () => Promise<void>;
|
||||
// 添加字典项的回调函数
|
||||
onAdd?: () => void;
|
||||
// 刷新字典列表的回调函数(导入成功后可能需要刷新左侧字典列表)
|
||||
onRefreshDicts?: () => void;
|
||||
}
|
||||
|
||||
// 字典项操作组合组件(包含添加、导入、导出按钮)
|
||||
const DictItemActions: React.FC<DictItemActionsProps> = ({
|
||||
selectedDict,
|
||||
actionRef,
|
||||
showExport = true,
|
||||
onImport,
|
||||
onExport,
|
||||
onAdd,
|
||||
onRefreshDicts,
|
||||
}) => {
|
||||
return (
|
||||
<Space>
|
||||
{/* 添加字典项按钮 */}
|
||||
{onAdd && <DictItemAddButton disabled={!selectedDict} onClick={onAdd} />}
|
||||
|
||||
{/* 导入字典项按钮 */}
|
||||
<DictItemImportButton
|
||||
selectedDict={selectedDict}
|
||||
actionRef={actionRef}
|
||||
onImport={onImport}
|
||||
onRefreshDicts={onRefreshDicts}
|
||||
/>
|
||||
|
||||
{/* 导出字典项按钮 */}
|
||||
{showExport && (
|
||||
<DictItemExportButton selectedDict={selectedDict} onExport={onExport} />
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictItemActions;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
// 字典项添加按钮组件的属性接口
|
||||
interface DictItemAddButtonProps {
|
||||
// 是否禁用按钮
|
||||
disabled?: boolean;
|
||||
// 点击按钮时的回调函数
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
// 字典项添加按钮组件
|
||||
const DictItemAddButton: React.FC<DictItemAddButtonProps> = ({
|
||||
disabled = false,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<Button type="primary" size="small" onClick={onClick} disabled={disabled}>
|
||||
添加字典项
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictItemAddButton;
|
||||
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
// 字典项导出按钮组件
|
||||
const DictItemExportButton: React.FC<DictItemExportButtonProps> = ({
|
||||
selectedDict,
|
||||
disabled = false,
|
||||
onExport,
|
||||
}) => {
|
||||
// 处理导出操作
|
||||
const handleExport = async () => {
|
||||
if (!selectedDict) {
|
||||
message.warning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果提供了自定义导出函数,则使用自定义函数
|
||||
if (onExport) {
|
||||
await onExport();
|
||||
} else {
|
||||
// 如果没有提供自定义导出函数,这里可以添加默认逻辑
|
||||
message.warning('未提供导出函数');
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
disabled={disabled || !selectedDict}
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictItemExportButton;
|
||||
|
|
@ -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<ActionType | undefined>;
|
||||
// 是否禁用按钮
|
||||
disabled?: boolean;
|
||||
// 自定义导入函数,返回 Promise(如果不提供,则使用默认的导入逻辑)
|
||||
onImport?: (file: File, dictId: number) => Promise<any>;
|
||||
// 导入成功后刷新字典列表的回调函数
|
||||
onRefreshDicts?: () => void;
|
||||
}
|
||||
|
||||
// 字典项导入按钮组件
|
||||
const DictItemImportButton: React.FC<DictItemImportButtonProps> = ({
|
||||
selectedDict,
|
||||
actionRef,
|
||||
disabled = false,
|
||||
onImport,
|
||||
onRefreshDicts,
|
||||
}) => {
|
||||
// 处理导入文件上传
|
||||
const handleImportUpload = async (options: any) => {
|
||||
console.log(options);
|
||||
const { file, onSuccess, onError } = options;
|
||||
try {
|
||||
// 条件判断,确保已选择字典
|
||||
if (!selectedDict?.id) {
|
||||
throw new Error('请先选择字典');
|
||||
}
|
||||
|
||||
let result: any;
|
||||
// 如果提供了自定义导入函数,则使用自定义函数
|
||||
if (onImport) {
|
||||
result = await onImport(file as File, selectedDict.id);
|
||||
} else {
|
||||
// 使用默认的导入逻辑,将 dictId 传入到 body 中
|
||||
const formData = new FormData();
|
||||
formData.append('file', file as File);
|
||||
formData.append('dictId', String(selectedDict.id));
|
||||
|
||||
result = await request('/api/dict/item/import', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查返回结果是否包含 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 (
|
||||
<Upload
|
||||
name="file"
|
||||
action={undefined}
|
||||
customRequest={handleImportUpload}
|
||||
showUploadList={false}
|
||||
disabled={disabled || !selectedDict}
|
||||
onChange={handleUploadChange}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UploadOutlined />}
|
||||
disabled={disabled || !selectedDict}
|
||||
>
|
||||
导入字典项
|
||||
</Button>
|
||||
</Upload>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictItemImportButton;
|
||||
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
const DictItemModal: React.FC<DictItemModalProps> = ({
|
||||
visible,
|
||||
isEdit,
|
||||
editingData,
|
||||
selectedDict,
|
||||
onCancel,
|
||||
onOk,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 当模态框打开或编辑数据变化时,重置或设置表单值
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
if (isEdit && editingData) {
|
||||
// 编辑模式,设置表单值为编辑数据
|
||||
form.setFieldsValue(editingData);
|
||||
} else {
|
||||
// 新增模式,重置表单
|
||||
form.resetFields();
|
||||
}
|
||||
}
|
||||
}, [visible, isEdit, editingData, form]);
|
||||
|
||||
// 表单提交处理
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await onOk(values);
|
||||
} catch (error) {
|
||||
// 表单验证失败,不关闭模态框
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
isEdit
|
||||
? '编辑字典项'
|
||||
: `添加字典项 - ${selectedDict?.title || '未选择字典'}`
|
||||
}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="名称 (e.g., zyn)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入标题' }]}
|
||||
>
|
||||
<Input placeholder="标题 (e.g., ZYN)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="中文标题" name="titleCN">
|
||||
<Input placeholder="中文标题 (e.g., 品牌)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称 (可选)" name="shortName">
|
||||
<Input placeholder="简称 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片 (可选)" name="image">
|
||||
<Input placeholder="图片链接 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="值 (可选)" name="value">
|
||||
<Input placeholder="值 (可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictItemModal;
|
||||
|
|
@ -151,7 +151,10 @@ const OrderItemsPage: React.FC = () => {
|
|||
}
|
||||
columns={columns}
|
||||
request={request}
|
||||
pagination={{ showSizeChanger: true }}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
search={{ labelWidth: 90, span: 6 }}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,8 +75,8 @@ import {
|
|||
Tag,
|
||||
} from 'antd';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { request, useParams } from '@umijs/max';
|
||||
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
|
@ -187,11 +189,20 @@ const ListPage: React.FC = () => {
|
|||
dataIndex: 'siteId',
|
||||
valueType: 'select',
|
||||
request: async () => {
|
||||
const { data = [] } = await sitecontrollerAll();
|
||||
return data.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}));
|
||||
try {
|
||||
const result = await sitecontrollerAll();
|
||||
const {success, data}= result
|
||||
if (success && data) {
|
||||
return data.map((site: any) => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('获取站点列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -199,16 +210,22 @@ const ListPage: React.FC = () => {
|
|||
dataIndex: 'keyword',
|
||||
hideInTable: true,
|
||||
},
|
||||
{
|
||||
{
|
||||
title: '订单ID',
|
||||
dataIndex: 'externalOrderId',
|
||||
},
|
||||
{
|
||||
title: '订单日期',
|
||||
title: '订单创建日期',
|
||||
dataIndex: 'date_created',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '付款日期',
|
||||
dataIndex: 'date_paid',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'total',
|
||||
|
|
@ -250,22 +267,23 @@ const ListPage: React.FC = () => {
|
|||
{
|
||||
title: '状态',
|
||||
dataIndex: 'orderStatus',
|
||||
hideInSearch: true,
|
||||
valueType: 'select',
|
||||
valueEnum: ORDER_STATUS_ENUM,
|
||||
},
|
||||
{
|
||||
title: '物流',
|
||||
dataIndex: 'shipmentList',
|
||||
dataIndex: 'fulfillments',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<div>
|
||||
{(record as any)?.shipmentList?.map((item: any) => {
|
||||
{(record as any)?.fulfillments?.map((item: any) => {
|
||||
if (!item) return;
|
||||
return (
|
||||
<div>
|
||||
{item.tracking_provider}:{item.primary_tracking_number} (
|
||||
{formatShipmentState(item.state)})
|
||||
<div style={{ display:"flex", alignItems:"center",'flexDirection':'column' }}>
|
||||
<span>物流供应商: {item.shipping_provider}</span>
|
||||
<span>物流单号: {item.tracking_number}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -276,7 +294,6 @@ const ListPage: React.FC = () => {
|
|||
{
|
||||
title: 'IP',
|
||||
dataIndex: 'customer_ip_address',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '设备',
|
||||
|
|
@ -339,15 +356,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 || '同步失败');
|
||||
|
|
@ -464,19 +484,24 @@ const ListPage: React.FC = () => {
|
|||
pagination={{
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000'],
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<CreateOrder tableRef={actionRef} />,
|
||||
// <CreateOrder tableRef={actionRef} />,
|
||||
<SyncForm
|
||||
onFinish={async (values: any) => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
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 || '同步失败');
|
||||
|
|
@ -484,31 +509,24 @@ const ListPage: React.FC = () => {
|
|||
}}
|
||||
tableRef={actionRef}
|
||||
/>,
|
||||
// <Button
|
||||
// type="primary"
|
||||
// disabled={selectedRowKeys.length === 0}
|
||||
// onClick={handleBatchExport}
|
||||
// >
|
||||
// 批量导出
|
||||
// </Button>,
|
||||
<Popconfirm
|
||||
<Popconfirm
|
||||
title="批量导出"
|
||||
description="确认导出选中的订单吗?"
|
||||
onConfirm={async () => {
|
||||
console.log(selectedRowKeys);
|
||||
try {
|
||||
const res = await request('/order/order/export', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
const res = await request('/order/export', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
ids: selectedRowKeys,
|
||||
}
|
||||
});
|
||||
if (res?.success && res?.data?.csv) {
|
||||
const blob = new Blob([res.data.csv], { type: 'text/csv;charset=utf-8;' });
|
||||
if (res?.success && res.data) {
|
||||
const blob = new Blob([res.data], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'customers.csv';
|
||||
a.download = 'orders.csv';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
|
|
@ -521,10 +539,13 @@ const ListPage: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
>
|
||||
批量导出
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
request={async ({ date, ...param }: any) => {
|
||||
if (param.status === 'all') {
|
||||
|
|
@ -613,33 +634,36 @@ const Detail: React.FC<{
|
|||
)
|
||||
? []
|
||||
: [
|
||||
<Divider type="vertical" />,
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (!record.siteId || !record.externalOrderId) {
|
||||
message.error('站点ID或外部订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerSyncorderbyid({
|
||||
siteId: record.siteId,
|
||||
orderId: record.externalOrderId,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
message.success('同步成功');
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '同步失败');
|
||||
<Divider type="vertical" />,
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (!record.siteId || !record.externalOrderId) {
|
||||
message.error('站点ID或外部订单ID不存在');
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
同步订单
|
||||
</Button>,
|
||||
]),
|
||||
const {
|
||||
success,
|
||||
message: errMsg,
|
||||
data,
|
||||
} = await ordercontrollerSyncorderbyid({
|
||||
siteId: record.siteId,
|
||||
orderId: record.externalOrderId,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
showSyncResult(data as SyncResultData, '订单');
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '同步失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
同步订单
|
||||
</Button>,
|
||||
]),
|
||||
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
|
||||
// ? [
|
||||
// <Divider type="vertical" />,
|
||||
|
|
@ -658,152 +682,152 @@ const Detail: React.FC<{
|
|||
'pending_refund',
|
||||
].includes(record.orderStatus)
|
||||
? [
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至售后"
|
||||
description="确认转至售后?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerChangestatus(
|
||||
{
|
||||
id: record.id,
|
||||
},
|
||||
{
|
||||
status: 'after_sale_pending',
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至售后"
|
||||
description="确认转至售后?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至售后
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerChangestatus(
|
||||
{
|
||||
id: record.id,
|
||||
},
|
||||
{
|
||||
status: 'after_sale_pending',
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至售后
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]
|
||||
: []),
|
||||
...(record.orderStatus === 'after_sale_pending'
|
||||
? [
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至取消"
|
||||
description="确认转至取消?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerCancelorder({
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至取消"
|
||||
description="确认转至取消?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerCancelorder({
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至取消
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至退款"
|
||||
description="确认转至退款?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerRefundorder({
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至退款
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至完成"
|
||||
description="确认转至完成?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerCompletedorder({
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至完成
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至待补发"
|
||||
description="确认转至待补发?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerChangestatus(
|
||||
{
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
},
|
||||
{
|
||||
status: 'pending_reshipment',
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至取消
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至退款"
|
||||
description="确认转至退款?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerRefundorder({
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至退款
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至完成"
|
||||
description="确认转至完成?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
if (!record.id) {
|
||||
message.error('订单ID不存在');
|
||||
return;
|
||||
}
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerCompletedorder({
|
||||
id: record.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至完成
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Divider type="vertical" />,
|
||||
<Popconfirm
|
||||
title="转至待补发"
|
||||
description="确认转至待补发?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await ordercontrollerChangestatus(
|
||||
{
|
||||
id: record.id,
|
||||
},
|
||||
{
|
||||
status: 'pending_reshipment',
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至待补发
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]
|
||||
tableRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button type="primary" ghost>
|
||||
转至待补发
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
|
|
@ -1065,31 +1089,31 @@ const Detail: React.FC<{
|
|||
}
|
||||
actions={
|
||||
v.state === 'waiting-for-scheduling' ||
|
||||
v.state === 'waiting-for-transit'
|
||||
v.state === 'waiting-for-transit'
|
||||
? [
|
||||
<Popconfirm
|
||||
title="取消运单"
|
||||
description="确认取消运单?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await logisticscontrollerDelshipment({
|
||||
id: v.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
tableRef.current?.reload();
|
||||
initRequest();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
<Popconfirm
|
||||
title="取消运单"
|
||||
description="确认取消运单?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
const { success, message: errMsg } =
|
||||
await logisticscontrollerDelshipment({
|
||||
id: v.id,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteFilled />
|
||||
取消运单
|
||||
</Popconfirm>,
|
||||
]
|
||||
tableRef.current?.reload();
|
||||
initRequest();
|
||||
} catch (error: any) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteFilled />
|
||||
取消运单
|
||||
</Popconfirm>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
|
|
@ -1477,16 +1501,16 @@ const Shipping: React.FC<{
|
|||
<ProFormList
|
||||
label="发货产品"
|
||||
name="sales"
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// message: '至少需要一个商品',
|
||||
// validator: (_, value) =>
|
||||
// value && value.length > 0
|
||||
// ? Promise.resolve()
|
||||
// : Promise.reject('至少需要一个商品'),
|
||||
// },
|
||||
// ]}
|
||||
// rules={[
|
||||
// {
|
||||
// required: true,
|
||||
// message: '至少需要一个商品',
|
||||
// validator: (_, value) =>
|
||||
// value && value.length > 0
|
||||
// ? Promise.resolve()
|
||||
// : Promise.reject('至少需要一个商品'),
|
||||
// },
|
||||
// ]}
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
|
|
@ -1911,7 +1935,7 @@ const Shipping: React.FC<{
|
|||
name="description"
|
||||
placeholder="请输入描述"
|
||||
width="lg"
|
||||
// rules={[{ required: true, message: '请输入描述' }]}
|
||||
// rules={[{ required: true, message: '请输入描述' }]}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
|
|
@ -2122,12 +2146,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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<ActionType>();
|
||||
|
||||
// 字典项新增/编辑模态框控制
|
||||
// 字典项模态框状态(由 DictItemModal 组件管理)
|
||||
const [isDictItemModalVisible, setIsDictItemModalVisible] = useState(false);
|
||||
const [editingDictItem, setEditingDictItem] = useState<any>(null);
|
||||
const [dictItemForm] = Form.useForm();
|
||||
const [isEditDictItem, setIsEditDictItem] = useState(false);
|
||||
const [editingDictItemData, setEditingDictItemData] = useState<any>(null);
|
||||
|
||||
// 导出字典项数据
|
||||
const handleExportDictItems = async () => {
|
||||
// 条件判断,确保已选择字典
|
||||
if (!selectedDict) {
|
||||
message.warning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前字典的所有数据
|
||||
const response = await request('/dict/items', {
|
||||
params: {
|
||||
dictId: selectedDict.id,
|
||||
},
|
||||
});
|
||||
|
||||
// 确保返回的是数组
|
||||
const data = Array.isArray(response) ? response : response?.data || [];
|
||||
|
||||
// 条件判断,检查是否有数据可导出
|
||||
if (data.length === 0) {
|
||||
message.warning('当前字典没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
// 将数据转换为CSV格式
|
||||
const headers = [
|
||||
'name',
|
||||
'title',
|
||||
'titleCN',
|
||||
'value',
|
||||
'sort',
|
||||
'image',
|
||||
'shortName',
|
||||
];
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...data.map((item: any) =>
|
||||
headers
|
||||
.map((header) => {
|
||||
const value = item[header] || '';
|
||||
// 条件判断,如果值包含逗号或引号,需要转义
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
(value.includes(',') || value.includes('"'))
|
||||
) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.join(','),
|
||||
),
|
||||
].join('\n');
|
||||
|
||||
// 创建blob并下载
|
||||
const blob = new Blob(['\ufeff' + csvContent], {
|
||||
// 添加BOM以支持中文
|
||||
type: 'text/csv;charset=utf-8',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${selectedDict.name}_dict_items.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.success(`成功导出 ${data.length} 条数据`);
|
||||
} catch (error: any) {
|
||||
message.error('导出字典项失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
const 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={
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={handleAddDictItem}
|
||||
disabled={!selectedDict}
|
||||
>
|
||||
添加字典项
|
||||
</Button>
|
||||
<Upload
|
||||
name="file"
|
||||
action={`/dict/item/import`}
|
||||
data={{ dictId: selectedDict?.id }}
|
||||
showUploadList={false}
|
||||
disabled={!selectedDict}
|
||||
onChange={(info) => {
|
||||
// 条件判断,上传状态处理
|
||||
if (info.file.status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功`);
|
||||
actionRef.current?.reload();
|
||||
} else if (info.file.status === 'error') {
|
||||
message.error(`${info.file.name} 文件上传失败`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UploadOutlined />}
|
||||
disabled={!selectedDict}
|
||||
>
|
||||
导入字典项
|
||||
</Button>
|
||||
</Upload>
|
||||
</Space>
|
||||
<DictItemActions
|
||||
selectedDict={selectedDict}
|
||||
actionRef={actionRef}
|
||||
showExport={true}
|
||||
onImport={async (file: File, dictId: number) => {
|
||||
// 创建 FormData 对象
|
||||
const formData = new FormData();
|
||||
// 添加文件到 FormData
|
||||
formData.append('file', file);
|
||||
// 添加字典 ID 到 FormData
|
||||
formData.append('dictId', String(dictId));
|
||||
// 调用导入字典项的 API
|
||||
const response = await dictApi.dictcontrollerImportdictitems(
|
||||
formData,
|
||||
);
|
||||
// 返回 JSON 响应
|
||||
return await response.json();
|
||||
}}
|
||||
onExport={handleExportDictItems}
|
||||
onAdd={handleAddDictItem}
|
||||
onRefreshDicts={fetchDicts}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
||||
<Modal
|
||||
title={editingDictItem ? '编辑字典项' : '添加字典项'}
|
||||
open={isDictItemModalVisible}
|
||||
onOk={() => dictItemForm.submit()}
|
||||
onCancel={() => setIsDictItemModalVisible(false)}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={dictItemForm}
|
||||
layout="vertical"
|
||||
onFinish={handleDictItemFormSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input size="small" placeholder="名称 (e.g., zyn)" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入标题' }]}
|
||||
>
|
||||
<Input size="small" placeholder="标题 (e.g., ZYN)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="中文标题" name="titleCN">
|
||||
<Input size="small" placeholder="中文标题 (e.g., 品牌)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称 (可选)" name="shortName">
|
||||
<Input size="small" placeholder="简称 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片 (可选)" name="image">
|
||||
<Input size="small" placeholder="图片链接 (可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item label="值 (可选)" name="value">
|
||||
<Input size="small" placeholder="值 (可选)" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
{/* 字典项 Modal(添加或编辑) */}
|
||||
<DictItemModal
|
||||
visible={isDictItemModalVisible}
|
||||
isEdit={isEditDictItem}
|
||||
editingData={editingDictItemData}
|
||||
selectedDict={selectedDict}
|
||||
onCancel={() => {
|
||||
setIsDictItemModalVisible(false);
|
||||
setEditingDictItemData(null);
|
||||
}}
|
||||
onOk={handleDictItemFormSubmit}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export default function CsvTool() {
|
||||
return (
|
||||
<div>
|
||||
<h1>产品CSV 工具</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import {
|
|||
productcontrollerCreateproduct,
|
||||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerGetproductlist,
|
||||
} from '@/servers/api/product';
|
||||
import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
|
||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
||||
|
|
@ -76,7 +77,7 @@ const CreateForm: React.FC<{
|
|||
const strengthName: string = String(strengthValues?.[0] || '');
|
||||
const flavorName: string = String(flavorValues?.[0] || '');
|
||||
const humidityName: string = String(humidityValues?.[0] || '');
|
||||
|
||||
console.log(formValues)
|
||||
// 调用模板渲染API来生成SKU
|
||||
const {
|
||||
data: rendered,
|
||||
|
|
@ -85,10 +86,25 @@ const CreateForm: React.FC<{
|
|||
} = await templatecontrollerRendertemplate(
|
||||
{ name: 'product.sku' },
|
||||
{
|
||||
brand: brandName || '',
|
||||
strength: strengthName || '',
|
||||
flavor: flavorName || '',
|
||||
humidity: humidityName ? capitalize(humidityName) : '',
|
||||
category: formValues.category,
|
||||
attributes: [
|
||||
{
|
||||
dict: {name: "brand"},
|
||||
shortName: brandName || '',
|
||||
},
|
||||
{
|
||||
dict: {name: "flavor"},
|
||||
shortName: flavorName || '',
|
||||
},
|
||||
{
|
||||
dict: {name: "strength"},
|
||||
shortName: strengthName || '',
|
||||
},
|
||||
{
|
||||
dict: {name: "humidity"},
|
||||
shortName: humidityName ? capitalize(humidityName) : '',
|
||||
},
|
||||
]
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
|
|
@ -137,8 +153,8 @@ const CreateForm: React.FC<{
|
|||
humidityName === 'dry'
|
||||
? 'Dry'
|
||||
: humidityName === 'moisture'
|
||||
? 'Moisture'
|
||||
: capitalize(humidityName),
|
||||
? 'Moisture'
|
||||
: capitalize(humidityName),
|
||||
},
|
||||
);
|
||||
if (!success) {
|
||||
|
|
@ -200,19 +216,23 @@ const CreateForm: React.FC<{
|
|||
}
|
||||
}}
|
||||
onFinish={async (values: any) => {
|
||||
// 组装 attributes(根据 activeAttributes 动态组装)
|
||||
const attributes = activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.name;
|
||||
const key = `${dictName}Values`;
|
||||
const vals = values[key];
|
||||
if (vals && Array.isArray(vals)) {
|
||||
return vals.map((v: string) => ({
|
||||
dictName: dictName,
|
||||
name: v,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
// 根据产品类型决定是否组装 attributes
|
||||
// 如果产品类型为 bundle,则 attributes 为空数组
|
||||
// 如果产品类型为 single,则根据 activeAttributes 动态组装 attributes
|
||||
const attributes = values.type === 'bundle'
|
||||
? []
|
||||
: activeAttributes.flatMap((attr: any) => {
|
||||
const dictName = attr.name;
|
||||
const key = `${dictName}Values`;
|
||||
const vals = values[key];
|
||||
if (vals && Array.isArray(vals)) {
|
||||
return vals.map((v: string) => ({
|
||||
dictName: dictName,
|
||||
name: v,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const payload: any = {
|
||||
name: (values as any).name,
|
||||
|
|
@ -246,13 +266,7 @@ const CreateForm: React.FC<{
|
|||
placeholder="请输入SKU"
|
||||
rules={[{ required: true, message: '请输入SKU' }]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="siteSkus"
|
||||
label="站点 SKU 列表"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入站点 SKU,回车添加"
|
||||
/>
|
||||
|
||||
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
|
||||
自动生成
|
||||
</Button>
|
||||
|
|
@ -265,6 +279,14 @@ const CreateForm: React.FC<{
|
|||
</Tag>
|
||||
)}
|
||||
</ProForm.Group>
|
||||
<ProFormSelect
|
||||
name="siteSkus"
|
||||
initialValue={[]}
|
||||
label="站点 SKU 列表"
|
||||
width="md"
|
||||
mode="tags"
|
||||
placeholder="输入站点 SKU,回车添加"
|
||||
/>
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
name="name"
|
||||
|
|
@ -313,15 +335,18 @@ const CreateForm: React.FC<{
|
|||
rules={[{ required: true, message: '请输入子产品SKU' }]}
|
||||
request={async ({ keyWords }) => {
|
||||
const params = keyWords
|
||||
? { sku: keyWords, name: keyWords }
|
||||
: { pageSize: 9999 };
|
||||
const { data } = await getStocks(params as any);
|
||||
? { sku: keyWords, name: keyWords, type: 'single' }
|
||||
: { pageSize: 9999, type: 'single' };
|
||||
const { data } = await productcontrollerGetproductlist(
|
||||
params as any,
|
||||
);
|
||||
if (!data || !data.items) {
|
||||
return [];
|
||||
}
|
||||
// 只返回类型为单品的产品
|
||||
return data.items
|
||||
.filter((item) => item.sku)
|
||||
.map((item) => ({
|
||||
.filter((item: any) => item.type === 'single' && item.sku)
|
||||
.map((item: any) => ({
|
||||
label: `${item.sku} - ${item.name}`,
|
||||
value: item.sku,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
productcontrollerGetcategoriesall,
|
||||
productcontrollerGetcategoryattributes,
|
||||
productcontrollerGetproductcomponents,
|
||||
productcontrollerGetproductlist,
|
||||
productcontrollerGetproductsiteskus,
|
||||
productcontrollerUpdateproduct,
|
||||
} from '@/servers/api/product';
|
||||
|
|
@ -315,17 +316,17 @@ const EditForm: React.FC<{
|
|||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
name="sku"
|
||||
label="库存SKU"
|
||||
label="单品SKU"
|
||||
width="md"
|
||||
showSearch
|
||||
debounceTime={300}
|
||||
placeholder="请输入库存SKU"
|
||||
rules={[{ required: true, message: '请输入库存SKU' }]}
|
||||
placeholder="请输入单品SKU"
|
||||
rules={[{ required: true, message: '请输入单品SKU' }]}
|
||||
request={async ({ keyWords }) => {
|
||||
const params = keyWords
|
||||
? { sku: keyWords, name: keyWords }
|
||||
: { pageSize: 9999 };
|
||||
const { data } = await getStocks(params as any);
|
||||
? { where: {sku: keyWords, name: keyWords, type: 'single'} }
|
||||
: { 'per_page': 9999 , where: {type: 'single'} };
|
||||
const { data } = await productcontrollerGetproductlist(params);
|
||||
if (!data || !data.items) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
import { showBatchOperationResult } from '@/utils/showResult';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import { templatecontrollerRendertemplate } from '@/servers/api/template';
|
||||
import { productcontrollerBatchsynctosite } from '@/servers/api/product';
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormSelect,
|
||||
ProFormDependency,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { App, Button, Tag } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface SyncToSiteModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
products: API.Product[];
|
||||
site?: any;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const SyncToSiteModal: React.FC<SyncToSiteModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
products,
|
||||
site,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { message } = App.useApp();
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
const formRef = useRef<any>();
|
||||
|
||||
// 生成单个产品的站点SKU
|
||||
const generateSingleSiteSku = async (
|
||||
currentSite: API.Site,
|
||||
product: API.Product,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
console.log('site', currentSite)
|
||||
const { data: renderedSku } = await templatecontrollerRendertemplate(
|
||||
{ name: 'site.product.sku' },
|
||||
{ site: currentSite, product },
|
||||
);
|
||||
return renderedSku || `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
||||
} catch (error) {
|
||||
return `${currentSite.skuPrefix || ''}${product.sku || ''}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成所有产品的站点SKU并设置到表单
|
||||
const generateAndSetSiteSkus = async (currentSite: any) => {
|
||||
const siteSkus: Record<string, string> = {};
|
||||
for (const product of products) {
|
||||
const siteSku = await generateSingleSiteSku(currentSite, product);
|
||||
siteSkus[product.id] = siteSku;
|
||||
}
|
||||
// 设置表单值
|
||||
formRef.current?.setFieldsValue({ siteSkus });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
sitecontrollerAll().then((res: any) => {
|
||||
const siteList = res?.data || [];
|
||||
setSites(siteList);
|
||||
// 如果有站点列表,默认选择第一个站点或传入的site
|
||||
const targetSite = site || (siteList.length > 0 ? siteList[0] : null);
|
||||
if (targetSite) {
|
||||
// 使用 setTimeout 确保 formRef 已经准备好
|
||||
setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
formRef.current.setFieldsValue({ siteId: targetSite.id });
|
||||
// 自动生成所有产品的站点 SKU
|
||||
generateAndSetSiteSkus(targetSite);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [visible, products, site]);
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title={`同步到站点 (${products.length} 项)`}
|
||||
open={visible}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
modalProps={{ destroyOnClose: true }}
|
||||
formRef={formRef}
|
||||
onValuesChange={async (changedValues) => {
|
||||
if ('siteId' in changedValues && changedValues.siteId) {
|
||||
const siteId = changedValues.siteId;
|
||||
const currentSite = sites.find((s: any) => s.id === siteId) || {};
|
||||
// 站点改变时,重新生成所有产品的站点SKU
|
||||
generateAndSetSiteSkus(currentSite);
|
||||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
console.log(`values`,values)
|
||||
if (!values.siteId) return false;
|
||||
try {
|
||||
const siteSkusMap = values.siteSkus || {};
|
||||
const data = products.map((product) => ({
|
||||
productId: product.id,
|
||||
siteSku: siteSkusMap[product.id] || `${values.siteId}-${product.sku}`,
|
||||
}));
|
||||
console.log(`data`,data)
|
||||
const result = await productcontrollerBatchsynctosite({
|
||||
siteId: values.siteId,
|
||||
data,
|
||||
});
|
||||
|
||||
showBatchOperationResult(result, '同步到站点');
|
||||
onSuccess();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '同步失败');
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormSelect
|
||||
name="siteId"
|
||||
label="选择站点"
|
||||
options={sites.map((site) => ({ label: site.name, value: site.id }))}
|
||||
rules={[{ required: true, message: '请选择站点' }]}
|
||||
/>
|
||||
{products.map((row) => (
|
||||
<ProFormDependency key={row.id} name={['siteId']}>
|
||||
{({ siteId }) => (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
|
||||
<div style={{ minWidth: 220 }}>原始SKU: {row.sku || '-'}</div>
|
||||
<div style={{ minWidth: 150 }}>
|
||||
已有商品SKU:{' '}
|
||||
{row.siteSkus && row.siteSkus.length > 0
|
||||
? row.siteSkus.map((siteSku: string, idx: number) => (
|
||||
<Tag key={idx} color="cyan">
|
||||
{siteSku}
|
||||
</Tag>
|
||||
))
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ProFormText
|
||||
name={['siteSkus', row.id]}
|
||||
label={`商品 ${row.sku} 站点SKU`}
|
||||
placeholder="请输入站点SKU"
|
||||
fieldProps={{
|
||||
onChange: (e) => {
|
||||
// 手动输入时更新表单值
|
||||
const currentValues = formRef.current?.getFieldValue('siteSkus') || {};
|
||||
currentValues[row.id] = e.target.value;
|
||||
formRef.current?.setFieldsValue({ siteSkus: currentValues });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
if (siteId) {
|
||||
const currentSite = sites.find((s: any) => s.id === siteId) || {};
|
||||
const siteSku = await generateSingleSiteSku(currentSite, row);
|
||||
const currentValues = formRef.current?.getFieldValue('siteSkus') || {};
|
||||
currentValues[row.id] = siteSku;
|
||||
formRef.current?.setFieldsValue({ siteSkus: currentValues });
|
||||
}
|
||||
}}
|
||||
>
|
||||
自动生成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ProFormDependency>
|
||||
))}
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default SyncToSiteModal;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import React from "react";
|
||||
import { ProTable, ProColumns } from "@ant-design/pro-components";
|
||||
|
||||
interface ProductComponentListProps {
|
||||
record: API.Product;
|
||||
columns: ProColumns<API.Product>[];
|
||||
dataSource?: API.Product[];
|
||||
}
|
||||
|
||||
const ProductComponentList: React.FC<ProductComponentListProps> = ({ record, columns, dataSource }) => {
|
||||
if (record.type !== "bundle" || !record.components || record.components.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const componentSkus = record.components.map(component => component.sku);
|
||||
|
||||
const includedProducts = [];
|
||||
|
||||
if (dataSource) {
|
||||
includedProducts = dataSource
|
||||
.filter(product => product.type === "single" && componentSkus.includes(product.sku));
|
||||
}
|
||||
|
||||
if (includedProducts.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: "16px", textAlign: "center", color: "#999" }}>
|
||||
未找到包含的单品信息
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentColumns = columns.filter(col =>
|
||||
[200~cd ../api"option", "siteSkus", "category", "type"].includes(col.dataIndex as string)
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "8px 16px", backgroundColor: "#fafafa" }}>
|
||||
<ProTable
|
||||
dataSource={includedProducts}
|
||||
columns={componentColumns}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered
|
||||
size="small"
|
||||
scroll={{ x: "max-content" }}
|
||||
headerTitle={null}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductComponentList;
|
||||
|
|
@ -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;
|
||||
|
|
@ -69,16 +67,7 @@ const AttributesCell: React.FC<{ record: any }> = ({ record }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const ComponentsCell: React.FC<{ productId: number }> = ({ productId }) => {
|
||||
const [components, setComponents] = React.useState<any[]>([]);
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const { data = [] } = await productcontrollerGetproductcomponents({
|
||||
id: productId,
|
||||
});
|
||||
setComponents(data || []);
|
||||
})();
|
||||
}, [productId]);
|
||||
const ComponentsCell: React.FC<{ components?: any[] }> = ({ components }) => {
|
||||
return (
|
||||
<div>
|
||||
{components && components.length ? (
|
||||
|
|
@ -165,251 +154,25 @@ const BatchEditModal: React.FC<{
|
|||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
const SyncToSiteModal: React.FC<{
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
productIds: number[];
|
||||
productRows: API.Product[];
|
||||
onSuccess: () => void;
|
||||
}> = ({ visible, onClose, productIds, productRows, onSuccess }) => {
|
||||
const { message } = App.useApp();
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
const formRef = useRef<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
sitecontrollerAll().then((res: any) => {
|
||||
setSites(res?.data || []);
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title={`同步到站点 (${productIds.length} 项)`}
|
||||
open={visible}
|
||||
onOpenChange={(open) => !open && onClose()}
|
||||
modalProps={{ destroyOnClose: true }}
|
||||
formRef={formRef}
|
||||
onValuesChange={(changedValues) => {
|
||||
if ('siteId' in changedValues && changedValues.siteId) {
|
||||
const siteId = changedValues.siteId;
|
||||
const site = sites.find((s: any) => s.id === siteId) || {};
|
||||
const prefix = site.skuPrefix || '';
|
||||
const map: Record<string, any> = {};
|
||||
productRows.forEach((p) => {
|
||||
map[p.id] = {
|
||||
code: `${prefix}${p.sku || ''}`,
|
||||
quantity: undefined,
|
||||
};
|
||||
});
|
||||
formRef.current?.setFieldsValue({ productSiteSkus: map });
|
||||
}
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
if (!values.siteId) return false;
|
||||
try {
|
||||
await wpproductcontrollerBatchsynctosite(
|
||||
{ siteId: values.siteId },
|
||||
{ productIds },
|
||||
);
|
||||
const map = values.productSiteSkus || {};
|
||||
for (const currentProductId of productIds) {
|
||||
const entry = map?.[currentProductId];
|
||||
if (entry && entry.code) {
|
||||
await productcontrollerBindproductsiteskus(
|
||||
{ id: currentProductId },
|
||||
{
|
||||
siteSkus: [
|
||||
{
|
||||
siteId: values.siteId,
|
||||
code: entry.code,
|
||||
quantity: entry.quantity,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
message.success('同步任务已提交');
|
||||
onSuccess();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '同步失败');
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormSelect
|
||||
name="siteId"
|
||||
label="选择站点"
|
||||
options={sites.map((site) => ({ label: site.name, value: site.id }))}
|
||||
rules={[{ required: true, message: '请选择站点' }]}
|
||||
/>
|
||||
{productRows.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
style={{ display: 'flex', gap: 12, alignItems: 'flex-end' }}
|
||||
>
|
||||
<div style={{ minWidth: 220 }}>原始SKU: {row.sku || '-'}</div>
|
||||
<ProFormText
|
||||
name={['productSiteSkus', row.id, 'code']}
|
||||
label={`商品 ${row.id} 站点SKU`}
|
||||
placeholder="请输入站点SKU"
|
||||
/>
|
||||
<ProFormText
|
||||
name={['productSiteSkus', row.id, 'quantity']}
|
||||
label="数量"
|
||||
placeholder="请输入数量"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
const WpProductInfo: React.FC<{
|
||||
skus: string[];
|
||||
record: API.Product;
|
||||
parentTableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
}> = ({ skus, record, parentTableRef }) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const { message } = App.useApp();
|
||||
const ProductList = ({ filter, columns }: { filter: { skus: string[] }, columns: any[] }) => {
|
||||
|
||||
return (
|
||||
<ProTable
|
||||
headerTitle="站点产品信息"
|
||||
actionRef={actionRef}
|
||||
search={false}
|
||||
options={false}
|
||||
pagination={false}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="refresh"
|
||||
type="primary"
|
||||
onClick={() => actionRef.current?.reload()}
|
||||
>
|
||||
刷新
|
||||
</Button>,
|
||||
]}
|
||||
request={async () => {
|
||||
// 判断是否存在站点SKU列表
|
||||
if (!skus || skus.length === 0) return { data: [] };
|
||||
try {
|
||||
// 获取所有站点列表用于遍历查询
|
||||
const { data: siteResponse } = await sitecontrollerAll();
|
||||
const siteList = siteResponse || [];
|
||||
// 聚合所有站点的产品数据
|
||||
const aggregatedProducts: any[] = [];
|
||||
// 遍历每一个站点
|
||||
for (const siteItem of siteList) {
|
||||
// 遍历每一个SKU在当前站点进行搜索
|
||||
for (const skuCode of skus) {
|
||||
// 直接调用站点API根据搜索关键字获取产品列表
|
||||
const response = await siteapicontrollerGetproducts({
|
||||
siteId: Number(siteItem.id),
|
||||
per_page: 100,
|
||||
search: skuCode,
|
||||
});
|
||||
const productPage = response as any;
|
||||
const siteProducts = productPage?.data?.items || [];
|
||||
// 将站点信息附加到产品数据中便于展示
|
||||
siteProducts.forEach((p: any) => {
|
||||
aggregatedProducts.push({
|
||||
...p,
|
||||
siteId: siteItem.id,
|
||||
siteName: siteItem.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return { data: aggregatedProducts, success: true };
|
||||
} catch (error: any) {
|
||||
// 请求失败进行错误提示
|
||||
message.error(error?.message || '获取站点产品失败');
|
||||
return { data: [], success: false };
|
||||
}
|
||||
request={async (pag) => {
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
where: filter
|
||||
});
|
||||
if (!success) return [];
|
||||
return data || [];
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '站点',
|
||||
dataIndex: 'siteName',
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'sku',
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'regular_price',
|
||||
render: (_, row) => (
|
||||
<div>
|
||||
<div>常规: {row.regular_price}</div>
|
||||
<div>促销: {row.sale_price}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
render: (_, wpRow) => [
|
||||
<a
|
||||
key="syncToSite"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await wpproductcontrollerBatchsynctosite(
|
||||
{ siteId: wpRow.siteId },
|
||||
{ productIds: [record.id] },
|
||||
);
|
||||
message.success('同步到站点成功');
|
||||
actionRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '同步失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
同步到站点
|
||||
</a>,
|
||||
<a
|
||||
key="syncToProduct"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await wpproductcontrollerSynctoproduct({ id: wpRow.id });
|
||||
message.success('同步进商品成功');
|
||||
parentTableRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '同步失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
同步进商品
|
||||
</a>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="删除"
|
||||
description="确认删除?"
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await request(`/wp_product/${wpRow.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
message.success('删除成功');
|
||||
actionRef.current?.reload();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '删除失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<a style={{ color: 'red' }}>删除</a>
|
||||
</Popconfirm>,
|
||||
],
|
||||
},
|
||||
]}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered
|
||||
size="small"
|
||||
scroll={{ x: "max-content" }}
|
||||
headerTitle={null}
|
||||
toolBarRender={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -419,8 +182,8 @@ const List: React.FC = () => {
|
|||
// 状态:存储当前选中的行
|
||||
const [selectedRows, setSelectedRows] = React.useState<API.Product[]>([]);
|
||||
const [batchEditModalVisible, setBatchEditModalVisible] = useState(false);
|
||||
const [syncProducts, setSyncProducts] = useState<API.Product[]>([]);
|
||||
const [syncModalVisible, setSyncModalVisible] = useState(false);
|
||||
const [syncProductIds, setSyncProductIds] = useState<number[]>([]);
|
||||
|
||||
const { message } = App.useApp();
|
||||
// 导出产品 CSV(带认证请求)
|
||||
|
|
@ -454,13 +217,13 @@ const List: React.FC = () => {
|
|||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '商品SKU',
|
||||
title: '关联商品',
|
||||
dataIndex: 'siteSkus',
|
||||
render: (_, record) => (
|
||||
<>
|
||||
{record.siteSkus?.map((siteSku, index) => (
|
||||
<Tag key={index} color="cyan">
|
||||
{siteSku.siteSku}
|
||||
{siteSku}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -481,13 +244,7 @@ const List: React.FC = () => {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '商品类型',
|
||||
dataIndex: 'category',
|
||||
render: (_, record: any) => {
|
||||
return record.category?.title || record.category?.name || '-';
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
|
|
@ -499,6 +256,13 @@ const List: React.FC = () => {
|
|||
dataIndex: 'promotionPrice',
|
||||
hideInSearch: true,
|
||||
sorter: true,
|
||||
},
|
||||
{
|
||||
title: '商品类型',
|
||||
dataIndex: 'category',
|
||||
render: (_, record: any) => {
|
||||
return record.category?.title || record.category?.name || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '属性',
|
||||
|
|
@ -531,7 +295,7 @@ const List: React.FC = () => {
|
|||
title: '构成',
|
||||
dataIndex: 'components',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => <ComponentsCell productId={(record as any).id} />,
|
||||
render: (_, record) => <ComponentsCell components={record.components} />,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -564,7 +328,7 @@ const List: React.FC = () => {
|
|||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setSyncProductIds([record.id]);
|
||||
setSyncProducts([record]);
|
||||
setSyncModalVisible(true);
|
||||
}}
|
||||
>
|
||||
|
|
@ -679,6 +443,7 @@ const List: React.FC = () => {
|
|||
onError?.(error);
|
||||
}
|
||||
}}
|
||||
|
||||
>
|
||||
<Button>批量导入</Button>
|
||||
</Upload>,
|
||||
|
|
@ -693,7 +458,7 @@ const List: React.FC = () => {
|
|||
<Button
|
||||
disabled={selectedRows.length <= 0}
|
||||
onClick={() => {
|
||||
setSyncProductIds(selectedRows.map((row) => row.id));
|
||||
setSyncProducts(selectedRows);
|
||||
setSyncModalVisible(true);
|
||||
}}
|
||||
>
|
||||
|
|
@ -741,9 +506,12 @@ const List: React.FC = () => {
|
|||
sortField = field;
|
||||
sortOrder = sort[field];
|
||||
}
|
||||
|
||||
const { current, pageSize, ...where } = params
|
||||
console.log(`params`, params)
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
...params,
|
||||
where,
|
||||
page: current || 1,
|
||||
per_page: pageSize || 10,
|
||||
sortField,
|
||||
sortOrder,
|
||||
} as any);
|
||||
|
|
@ -754,17 +522,18 @@ const List: React.FC = () => {
|
|||
};
|
||||
}}
|
||||
columns={columns}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<WpProductInfo
|
||||
skus={(record.siteSkus as string[]) || []}
|
||||
record={record}
|
||||
parentTableRef={actionRef}
|
||||
/>
|
||||
),
|
||||
rowExpandable: (record) =>
|
||||
!!(record.siteSkus && record.siteSkus.length > 0),
|
||||
}}
|
||||
// expandable={{
|
||||
// expandedRowRender: (record) => {
|
||||
// return <ProductList filter={{
|
||||
// skus: record.components?.map(component => component.sku) || [],
|
||||
// }}
|
||||
// columns={columns}
|
||||
// ></ProductList>
|
||||
// }
|
||||
// ,
|
||||
// rowExpandable: (record) =>
|
||||
// !!(record.type==='bundle'),
|
||||
// }}
|
||||
editable={{
|
||||
type: 'single',
|
||||
onSave: async (key, record, originRow) => {
|
||||
|
|
@ -774,6 +543,11 @@ const List: React.FC = () => {
|
|||
rowSelection={{
|
||||
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
|
||||
}}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
|
||||
}}
|
||||
/>
|
||||
<BatchEditModal
|
||||
visible={batchEditModalVisible}
|
||||
|
|
@ -788,8 +562,7 @@ const List: React.FC = () => {
|
|||
<SyncToSiteModal
|
||||
visible={syncModalVisible}
|
||||
onClose={() => setSyncModalVisible(false)}
|
||||
productIds={syncProductIds}
|
||||
productRows={selectedRows}
|
||||
products={syncProducts}
|
||||
onSuccess={() => {
|
||||
setSyncModalVisible(false);
|
||||
setSelectedRows([]);
|
||||
|
|
@ -800,4 +573,5 @@ const List: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
export default List;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -313,11 +400,21 @@ const PermutationPage: React.FC = () => {
|
|||
pagination={{
|
||||
defaultPageSize: 50,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['50', '100', '200', '500', '1000', '2000'],
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={false}
|
||||
toolBarRender={false}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="export"
|
||||
type="default"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
>
|
||||
导出列表
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</ProCard>
|
||||
|
|
|
|||
|
|
@ -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<SiteProductCellProps> = ({
|
||||
product,
|
||||
site,
|
||||
onSyncSuccess,
|
||||
}) => {
|
||||
// 存储该站点对应的产品数据
|
||||
const [siteProduct, setSiteProduct] = useState<SiteProductData | null>(null);
|
||||
// 存储本地产品完整数据
|
||||
const [localProduct, setLocalProduct] = useState<LocalProduct | null>(null);
|
||||
// 加载状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 是否已加载过数据
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
// 同步中状态
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
// 组件挂载时加载数据
|
||||
useEffect(() => {
|
||||
loadSiteProduct();
|
||||
}, [product.id, site.id]);
|
||||
|
||||
// 加载站点产品数据
|
||||
const loadSiteProduct = async () => {
|
||||
// 如果已经加载过,则不再重复加载
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 首先查找该产品在该站点的实际SKU
|
||||
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
|
||||
// 这里使用模板生成的 SKU 作为默认值
|
||||
let siteProductSku = '';
|
||||
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
|
||||
|
||||
// 如果没有找到实际的siteSku,则根据模板或默认规则生成期望的SKU
|
||||
const expectedSku =
|
||||
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
|
||||
|
||||
// 使用 siteapicontrollerGetproducts 获取该站点的所有产品
|
||||
const productsRes = await siteapicontrollerGetproducts({
|
||||
siteId: site.id,
|
||||
current: 1,
|
||||
pageSize: 10000,
|
||||
} as any);
|
||||
|
||||
if (productsRes.data?.items) {
|
||||
// 在该站点的产品数据中查找匹配的产品
|
||||
let foundProduct = productsRes.data.items.find(
|
||||
(item: any) => item.sku === expectedSku,
|
||||
);
|
||||
|
||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
||||
if (!foundProduct && siteProductSku) {
|
||||
foundProduct = productsRes.data.items.find(
|
||||
(item: any) => item.sku === siteProductSku,
|
||||
);
|
||||
}
|
||||
|
||||
if (foundProduct) {
|
||||
setSiteProduct(foundProduct as SiteProductData);
|
||||
}
|
||||
}
|
||||
|
||||
// 标记为已加载
|
||||
setLoaded(true);
|
||||
} catch (error) {
|
||||
console.error(`加载站点 ${site.name} 的产品数据失败:`, error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取本地产品完整信息
|
||||
const getLocalProduct = async (): Promise<LocalProduct | null> => {
|
||||
try {
|
||||
// 如果已经有本地产品数据,直接返回
|
||||
if (localProduct) {
|
||||
return localProduct;
|
||||
}
|
||||
|
||||
// 使用 productcontrollerGetproductlist 获取本地产品完整信息
|
||||
const res = await productcontrollerGetproductlist({
|
||||
where: {
|
||||
id: product.id,
|
||||
},
|
||||
} as any);
|
||||
|
||||
if (res.success && res.data) {
|
||||
const productData = res.data as LocalProduct;
|
||||
setLocalProduct(productData);
|
||||
return productData;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取本地产品信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染站点SKU
|
||||
const renderSiteSku = async (data: any): Promise<string> => {
|
||||
try {
|
||||
// 使用 templatecontrollerRendertemplate API 渲染模板
|
||||
const res = await templatecontrollerRendertemplate(
|
||||
{ name: 'siteproduct-sku' } as any,
|
||||
data,
|
||||
);
|
||||
|
||||
return res?.template || res?.result || '';
|
||||
} catch (error) {
|
||||
console.error('渲染SKU模板失败:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 同步产品到站点
|
||||
const syncProductToSite = async (values: any) => {
|
||||
try {
|
||||
setSyncing(true);
|
||||
const hide = message.loading('正在同步...', 0);
|
||||
|
||||
// 获取本地产品完整信息
|
||||
const productDetail = await getLocalProduct();
|
||||
|
||||
if (!productDetail) {
|
||||
hide();
|
||||
message.error('获取本地产品信息失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构造要同步的产品数据
|
||||
const productData: any = {
|
||||
sku: values.sku,
|
||||
name: productDetail.name,
|
||||
description: productDetail.description || '',
|
||||
regular_price: productDetail.price,
|
||||
price: productDetail.price,
|
||||
stock_quantity: productDetail.stock,
|
||||
status: 'publish',
|
||||
};
|
||||
|
||||
// 如果有图片,添加图片信息
|
||||
if (productDetail.images && productDetail.images.length > 0) {
|
||||
productData.images = productDetail.images;
|
||||
}
|
||||
|
||||
// 如果有重量,添加重量信息
|
||||
if (productDetail.weight) {
|
||||
productData.weight = productDetail.weight;
|
||||
}
|
||||
|
||||
// 如果有尺寸,添加尺寸信息
|
||||
if (productDetail.dimensions) {
|
||||
productData.dimensions = productDetail.dimensions;
|
||||
}
|
||||
|
||||
// 使用 siteapicontrollerUpsertproduct API 同步产品到站点
|
||||
const res = await siteapicontrollerUpsertproduct(
|
||||
{ siteId: site.id } as any,
|
||||
productData as any,
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
hide();
|
||||
throw new Error(res.message || '同步失败');
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
setSiteProduct(res.data as SiteProductData);
|
||||
}
|
||||
|
||||
hide();
|
||||
message.success('同步成功');
|
||||
|
||||
// 触发回调
|
||||
if (onSyncSuccess) {
|
||||
onSyncSuccess();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error('同步失败: ' + (error.message || error.toString()));
|
||||
return false;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新同步产品到站点
|
||||
const updateSyncProduct = async (values: any) => {
|
||||
try {
|
||||
setSyncing(true);
|
||||
const hide = message.loading('正在更新...', 0);
|
||||
|
||||
// 获取本地产品完整信息
|
||||
const productDetail = await getLocalProduct();
|
||||
|
||||
if (!productDetail) {
|
||||
hide();
|
||||
message.error('获取本地产品信息失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构造要更新的产品数据
|
||||
const productData: any = {
|
||||
...siteProduct,
|
||||
sku: values.sku,
|
||||
name: productDetail.name,
|
||||
description: productDetail.description || '',
|
||||
regular_price: productDetail.price,
|
||||
price: productDetail.price,
|
||||
stock_quantity: productDetail.stock,
|
||||
status: 'publish',
|
||||
};
|
||||
|
||||
// 如果有图片,添加图片信息
|
||||
if (productDetail.images && productDetail.images.length > 0) {
|
||||
productData.images = productDetail.images;
|
||||
}
|
||||
|
||||
// 如果有重量,添加重量信息
|
||||
if (productDetail.weight) {
|
||||
productData.weight = productDetail.weight;
|
||||
}
|
||||
|
||||
// 如果有尺寸,添加尺寸信息
|
||||
if (productDetail.dimensions) {
|
||||
productData.dimensions = productDetail.dimensions;
|
||||
}
|
||||
|
||||
// 使用 siteapicontrollerUpsertproduct API 更新产品到站点
|
||||
const res = await siteapicontrollerUpsertproduct(
|
||||
{ siteId: site.id } as any,
|
||||
productData as any,
|
||||
);
|
||||
|
||||
if (!res.success) {
|
||||
hide();
|
||||
throw new Error(res.message || '更新失败');
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
setSiteProduct(res.data as SiteProductData);
|
||||
}
|
||||
|
||||
hide();
|
||||
message.success('更新成功');
|
||||
|
||||
// 触发回调
|
||||
if (onSyncSuccess) {
|
||||
onSyncSuccess();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error('更新失败: ' + (error.message || error.toString()));
|
||||
return false;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果正在加载,显示加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 10 }}>
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有找到站点产品,显示同步按钮
|
||||
if (!siteProduct) {
|
||||
// 首先查找该产品在该站点的实际SKU
|
||||
// 注意:siteSkus 现在是字符串数组,无法直接匹配站点
|
||||
// 这里使用模板生成的 SKU 作为默认值
|
||||
let siteProductSku = '';
|
||||
// 如果需要更精确的站点 SKU 匹配,需要后端提供额外的接口
|
||||
|
||||
const defaultSku =
|
||||
siteProductSku || `${site.skuPrefix || ''}-${product.sku}`;
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
title="同步产品"
|
||||
trigger={
|
||||
<Button type="link" icon={<SyncOutlined />}>
|
||||
同步到站点
|
||||
</Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(values);
|
||||
}}
|
||||
initialValues={{
|
||||
sku: defaultSku,
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="sku"
|
||||
label="商店 SKU"
|
||||
placeholder="请输入商店 SKU"
|
||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
}
|
||||
|
||||
// 显示站点产品信息
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>{siteProduct.sku}</div>
|
||||
<ModalForm
|
||||
title="更新同步"
|
||||
trigger={
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SyncOutlined spin={false} />}
|
||||
>
|
||||
更新
|
||||
</Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await updateSyncProduct(values);
|
||||
}}
|
||||
initialValues={{
|
||||
sku: siteProduct.sku,
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="sku"
|
||||
label="商店 SKU"
|
||||
placeholder="请输入商店 SKU"
|
||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
||||
disabled
|
||||
/>
|
||||
<div style={{ marginBottom: 16, color: '#666' }}>
|
||||
确定要将本地产品数据更新到站点吗?
|
||||
</div>
|
||||
</ModalForm>
|
||||
</div>
|
||||
<div>Price: {siteProduct.regular_price ?? siteProduct.price}</div>
|
||||
{siteProduct.sale_price && (
|
||||
<div style={{ color: 'red' }}>Sale: {siteProduct.sale_price}</div>
|
||||
)}
|
||||
<div>
|
||||
Stock: {siteProduct.stock_quantity ?? siteProduct.stockQuantity}
|
||||
</div>
|
||||
<div style={{ marginTop: 2 }}>
|
||||
Status:{' '}
|
||||
{siteProduct.status === 'publish' ? (
|
||||
<Tag color="green">Published</Tag>
|
||||
) : (
|
||||
<Tag>{siteProduct.status}</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteProductCell;
|
||||
|
|
@ -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<string, WpProduct>;
|
||||
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<ApiResponse<Site>> => {
|
|||
};
|
||||
};
|
||||
|
||||
const getWPProducts = async (): Promise<ApiResponse<WpProduct>> => {
|
||||
return request('/product/wp-products', {
|
||||
method: 'GET',
|
||||
});
|
||||
};
|
||||
|
||||
const ProductSyncPage: React.FC = () => {
|
||||
const [sites, setSites] = useState<Site[]>([]);
|
||||
// 存储所有 WP 产品,用于查找匹配。 Key: SKU (包含前缀)
|
||||
const [wpProductMap, setWpProductMap] = useState<Map<string, WpProduct>>(
|
||||
new Map(),
|
||||
);
|
||||
const [skuTemplate, setSkuTemplate] = useState<string>('');
|
||||
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [selectedSiteId, setSelectedSiteId] = useState<string>('');
|
||||
|
|
@ -104,102 +88,46 @@ const ProductSyncPage: React.FC = () => {
|
|||
errors: string[];
|
||||
}>({ success: 0, failed: 0, errors: [] });
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<ProductWithWP[]>([]);
|
||||
|
||||
// 初始化数据:获取站点和所有 WP 产品
|
||||
const [selectedRows, setSelectedRows] = useState<SiteProduct[]>([]);
|
||||
// 初始化加载站点列表
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const initializeData = async () => {
|
||||
try {
|
||||
setInitialLoading(true);
|
||||
// 获取所有站点
|
||||
const sitesResponse = await getSites();
|
||||
const rawSiteList = sitesResponse.data || [];
|
||||
// 过滤掉已禁用的站点
|
||||
const siteList: Site[] = rawSiteList.filter((site) => !site.isDisabled);
|
||||
setSites(siteList);
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
const wpProductsResponse = await getWPProducts();
|
||||
const wpProductList: WpProduct[] = wpProductsResponse.data || [];
|
||||
|
||||
// 构建 WP 产品 Map,Key 为 SKU
|
||||
const map = new Map<string, WpProduct>();
|
||||
wpProductList.forEach((p) => {
|
||||
if (p.sku) {
|
||||
map.set(p.sku, p);
|
||||
}
|
||||
});
|
||||
setWpProductMap(map);
|
||||
|
||||
// 获取 SKU 模板
|
||||
try {
|
||||
const templateRes = await templatecontrollerGettemplatebyname({
|
||||
name: 'site.product.sku',
|
||||
});
|
||||
if (templateRes && templateRes.value) {
|
||||
setSkuTemplate(templateRes.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Template site.product.sku not found, using default.');
|
||||
// 获取站点列表
|
||||
const sitesRes = await getSites();
|
||||
if (sitesRes.success && sitesRes.data.length > 0) {
|
||||
setSites(sitesRes.data);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取基础数据失败,请重试');
|
||||
console.error('Error fetching data:', error);
|
||||
console.error('初始化数据失败:', error);
|
||||
message.error('初始化数据失败');
|
||||
} finally {
|
||||
setInitialLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
initializeData();
|
||||
}, []);
|
||||
|
||||
// 同步产品到站点
|
||||
const syncProductToSite = async (
|
||||
values: any,
|
||||
record: ProductWithWP,
|
||||
record: SiteProduct,
|
||||
site: Site,
|
||||
wpProductId?: string,
|
||||
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 (wpProductId) {
|
||||
res = await request(`/site-api/${site.id}/products/${wpProductId}`, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
res = await request(`/site-api/${site.id}/products`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 使用 productcontrollerSynctosite API 同步产品到站点
|
||||
const res = await productcontrollerSynctosite({
|
||||
productId: Number(record.id),
|
||||
siteId: Number(site.id),
|
||||
} as any);
|
||||
|
||||
console.log('res', res);
|
||||
if (!res.success) {
|
||||
hide();
|
||||
throw new Error(res.message || '同步失败');
|
||||
}
|
||||
// 更新本地缓存 Map,避免刷新
|
||||
setWpProductMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
newMap.set(values.sku, res.data as WpProduct);
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
|
||||
hide();
|
||||
message.success('同步成功');
|
||||
|
|
@ -207,12 +135,11 @@ const ProductSyncPage: React.FC = () => {
|
|||
} catch (error: any) {
|
||||
message.error('同步失败: ' + (error.message || error.toString()));
|
||||
return false;
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
// 批量同步产品到指定站点
|
||||
const batchSyncProducts = async (productsToSync?: ProductWithWP[]) => {
|
||||
const batchSyncProducts = async (productsToSync?: SiteProduct[]) => {
|
||||
if (!selectedSiteId) {
|
||||
message.error('请选择要同步到的站点');
|
||||
return;
|
||||
|
|
@ -239,7 +166,7 @@ const ProductSyncPage: React.FC = () => {
|
|||
message.error('获取产品列表失败');
|
||||
return;
|
||||
}
|
||||
products = data.items as ProductWithWP[];
|
||||
products = data.items as SiteProduct[];
|
||||
} catch (error) {
|
||||
message.error('获取产品列表失败');
|
||||
return;
|
||||
|
|
@ -250,108 +177,44 @@ const ProductSyncPage: React.FC = () => {
|
|||
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;
|
||||
}
|
||||
}
|
||||
// 使用 productcontrollerBatchsynctosite API 批量同步
|
||||
const productIds = products.map((product) => Number(product.id));
|
||||
|
||||
// 如果没有找到实际的siteSku,则根据模板生成
|
||||
const expectedSku =
|
||||
siteProductSku ||
|
||||
(skuTemplate
|
||||
? renderSiteSku(skuTemplate, { site: targetSite, product })
|
||||
: `${targetSite.skuPrefix || ''}-${product.sku}`);
|
||||
// 更新进度为50%,表示正在处理
|
||||
setSyncProgress(50);
|
||||
|
||||
// 检查是否已存在
|
||||
const existingProduct = wpProductMap.get(expectedSku);
|
||||
const res = await productcontrollerBatchsynctosite({
|
||||
productIds: productIds,
|
||||
siteId: Number(targetSite.id),
|
||||
} as any);
|
||||
|
||||
// 准备同步数据
|
||||
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,
|
||||
};
|
||||
if (res.success) {
|
||||
const syncedCount = res.data?.synced || 0;
|
||||
const errors = res.data?.errors || [];
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
// 更新进度为100%,表示完成
|
||||
setSyncProgress(100);
|
||||
|
||||
console.log('res', res);
|
||||
setSyncResults({
|
||||
success: syncedCount,
|
||||
failed: errors.length,
|
||||
errors: errors.map((err: any) => err.error || '未知错误'),
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
successCount++;
|
||||
// 更新本地缓存
|
||||
setWpProductMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
newMap.set(expectedSku, res.data as WpProduct);
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
} else {
|
||||
failedCount++;
|
||||
errors.push(`产品 ${product.sku}: ${res.message || '同步失败'}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
failedCount++;
|
||||
errors.push(`产品 ${product.sku}: ${error.message || '未知错误'}`);
|
||||
if (errors.length === 0) {
|
||||
message.success(`批量同步完成,成功同步 ${syncedCount} 个产品`);
|
||||
} else {
|
||||
message.warning(
|
||||
`批量同步完成,成功 ${syncedCount} 个,失败 ${errors.length} 个`,
|
||||
);
|
||||
}
|
||||
|
||||
processed++;
|
||||
setSyncProgress(Math.round((processed / totalProducts) * 100));
|
||||
}
|
||||
|
||||
setSyncResults({ success: successCount, failed: failedCount, errors });
|
||||
|
||||
if (failedCount === 0) {
|
||||
message.success(`批量同步完成,成功同步 ${successCount} 个产品`);
|
||||
// 刷新表格
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.warning(
|
||||
`批量同步完成,成功 ${successCount} 个,失败 ${failedCount} 个`,
|
||||
);
|
||||
throw new Error(res.message || '批量同步失败');
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
actionRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
message.error('批量同步失败: ' + (error.message || error.toString()));
|
||||
} finally {
|
||||
|
|
@ -359,27 +222,9 @@ const ProductSyncPage: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 简单的模板渲染函数
|
||||
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>[] = [
|
||||
const generateColumns = (): ProColumns<Site>[] => {
|
||||
const columns: ProColumns<SiteProduct>[] = [
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'sku',
|
||||
|
|
@ -471,137 +316,21 @@ const ProductSyncPage: React.FC = () => {
|
|||
|
||||
// 为每个站点生成列
|
||||
sites.forEach((site: Site) => {
|
||||
const siteColumn: ProColumns<ProductWithWP> = {
|
||||
const siteColumn: ProColumns<SiteProduct> = {
|
||||
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 wpProduct = wpProductMap.get(expectedSku);
|
||||
|
||||
// 如果根据实际SKU没找到,再尝试用模板生成的SKU查找
|
||||
if (!wpProduct && siteProductSku && skuTemplate) {
|
||||
const templateSku = renderSiteSku(skuTemplate, {
|
||||
site,
|
||||
product: record,
|
||||
});
|
||||
wpProduct = wpProductMap.get(templateSku);
|
||||
}
|
||||
|
||||
if (!wpProduct) {
|
||||
return (
|
||||
<ModalForm
|
||||
title="同步产品"
|
||||
trigger={
|
||||
<Button type="link" icon={<SyncOutlined />}>
|
||||
同步到站点
|
||||
</Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(values, record, site);
|
||||
}}
|
||||
initialValues={{
|
||||
sku:
|
||||
siteProductSku ||
|
||||
(skuTemplate
|
||||
? 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' }}>{wpProduct.sku}</div>
|
||||
<ModalForm
|
||||
title="更新同步"
|
||||
trigger={
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SyncOutlined spin={false} />}
|
||||
></Button>
|
||||
}
|
||||
width={400}
|
||||
onFinish={async (values) => {
|
||||
return await syncProductToSite(
|
||||
values,
|
||||
record,
|
||||
site,
|
||||
wpProduct.externalProductId,
|
||||
);
|
||||
}}
|
||||
initialValues={{
|
||||
sku: wpProduct.sku,
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="sku"
|
||||
label="商店 SKU"
|
||||
placeholder="请输入商店 SKU"
|
||||
rules={[{ required: true, message: '请输入 SKU' }]}
|
||||
disabled
|
||||
/>
|
||||
<div style={{ marginBottom: 16, color: '#666' }}>
|
||||
确定要将本地产品数据更新到站点吗?
|
||||
</div>
|
||||
</ModalForm>
|
||||
</div>
|
||||
<div>Price: {wpProduct.regular_price ?? wpProduct.price}</div>
|
||||
{wpProduct.sale_price && (
|
||||
<div style={{ color: 'red' }}>Sale: {wpProduct.sale_price}</div>
|
||||
)}
|
||||
<div>
|
||||
Stock: {wpProduct.stock_quantity ?? wpProduct.stockQuantity}
|
||||
</div>
|
||||
<div style={{ marginTop: 2 }}>
|
||||
Status:{' '}
|
||||
{wpProduct.status === 'publish' ? (
|
||||
<Tag color="green">Published</Tag>
|
||||
) : (
|
||||
<Tag>{wpProduct.status}</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SiteProductCell
|
||||
product={record}
|
||||
site={site}
|
||||
onSyncSuccess={() => {
|
||||
// 同步成功后刷新表格
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -624,7 +353,7 @@ const ProductSyncPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<Card title="商品同步状态" className="product-sync-card">
|
||||
<ProTable<ProductWithWP>
|
||||
<ProTable<SiteProduct>
|
||||
columns={generateColumns()}
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
|
|
@ -665,26 +394,27 @@ const ProductSyncPage: React.FC = () => {
|
|||
]}
|
||||
request={async (params, sort, filter) => {
|
||||
// 调用本地获取产品列表 API
|
||||
const { data, success } = await productcontrollerGetproductlist({
|
||||
const response = await productcontrollerGetproductlist({
|
||||
...params,
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
// 传递搜索参数
|
||||
keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
|
||||
// keyword: params.keyword, // 假设 ProTable 的 search 表单会传递 keyword 或其他字段
|
||||
sku: (params as any).sku,
|
||||
name: (params as any).name,
|
||||
} as any);
|
||||
|
||||
console.log('result', response);
|
||||
// 返回给 ProTable
|
||||
return {
|
||||
data: (data?.items || []) as ProductWithWP[],
|
||||
success,
|
||||
total: data?.total || 0,
|
||||
data: response.data?.items || [],
|
||||
success: response.success,
|
||||
total: response.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
search={{
|
||||
|
|
@ -708,7 +438,7 @@ const ProductSyncPage: React.FC = () => {
|
|||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p>
|
||||
目标站点:
|
||||
目标站点:
|
||||
<strong>{sites.find((s) => s.id === selectedSiteId)?.name}</strong>
|
||||
</p>
|
||||
{selectedRows.length > 0 ? (
|
||||
|
|
@ -722,17 +452,17 @@ const ProductSyncPage: React.FC = () => {
|
|||
|
||||
{syncing && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>同步进度:</div>
|
||||
<div style={{ marginBottom: 8 }}>同步进度:</div>
|
||||
<Progress percent={syncProgress} status="active" />
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
|
||||
成功:{syncResults.success} | 失败:{syncResults.failed}
|
||||
成功:{syncResults.success} | 失败:{syncResults.failed}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{syncResults.errors.length > 0 && (
|
||||
<div style={{ marginBottom: 16, maxHeight: 200, overflow: 'auto' }}>
|
||||
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
||||
<div style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情:</div>
|
||||
{syncResults.errors.slice(0, 10).map((error, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ordercontrollerSyncorder } from '@/servers/api/order';
|
||||
import { ordercontrollerSyncorders } from '@/servers/api/order';
|
||||
import {
|
||||
sitecontrollerCreate,
|
||||
sitecontrollerDisable,
|
||||
|
|
@ -6,10 +6,13 @@ import {
|
|||
sitecontrollerUpdate,
|
||||
} from '@/servers/api/site';
|
||||
import { subscriptioncontrollerSync } from '@/servers/api/subscription';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button, message, notification, Popconfirm, Space, Tag } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
|
||||
import { ActionType, ProColumns, ProTable, DrawerForm, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
|
||||
import { Button, message, notification, Popconfirm, Space, Tag, Form } from 'antd';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import EditSiteForm from '../Shop/EditSiteForm'; // 引入重构后的表单组件
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||
|
||||
// 区域数据项类型
|
||||
interface AreaItem {
|
||||
|
|
@ -42,29 +45,24 @@ const SiteList: React.FC = () => {
|
|||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<SiteItem | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchEditOpen, setBatchEditOpen] = useState(false);
|
||||
const [batchEditForm] = Form.useForm();
|
||||
countries.registerLocale(zhCN);
|
||||
|
||||
|
||||
const handleSync = async (ids: number[]) => {
|
||||
if (!ids.length) return;
|
||||
const hide = message.loading('正在同步...', 0);
|
||||
|
||||
const stats = {
|
||||
products: { success: 0, fail: 0 },
|
||||
orders: { success: 0, fail: 0 },
|
||||
subscriptions: { success: 0, fail: 0 },
|
||||
};
|
||||
|
||||
try {
|
||||
for (const id of ids) {
|
||||
// 同步产品
|
||||
const prodRes = await wpproductcontrollerSyncproducts({ siteId: id });
|
||||
if (prodRes.success) {
|
||||
stats.products.success += 1;
|
||||
} else {
|
||||
stats.products.fail += 1;
|
||||
}
|
||||
|
||||
// 同步订单
|
||||
const orderRes = await ordercontrollerSyncorder({ siteId: id });
|
||||
const orderRes = await ordercontrollerSyncorders({ siteId: id });
|
||||
if (orderRes.success) {
|
||||
stats.orders.success += 1;
|
||||
} else {
|
||||
|
|
@ -85,9 +83,6 @@ const SiteList: React.FC = () => {
|
|||
message: '同步完成',
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
产品: 成功 {stats.products.success}, 失败 {stats.products.fail}
|
||||
</p>
|
||||
<p>
|
||||
订单: 成功 {stats.orders.success}, 失败 {stats.orders.fail}
|
||||
</p>
|
||||
|
|
@ -108,6 +103,68 @@ const SiteList: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 获取所有国家/地区的选项
|
||||
const getCountryOptions = () => {
|
||||
// 获取所有国家的 ISO 代码
|
||||
const countryCodes = countries.getAlpha2Codes();
|
||||
// 将国家代码转换为选项数组
|
||||
return Object.keys(countryCodes).map((code) => ({
|
||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
||||
value: code,
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理批量编辑提交
|
||||
const handleBatchEditFinish = async (values: any) => {
|
||||
if (!selectedRowKeys.length) return;
|
||||
const hide = message.loading('正在批量更新...', 0);
|
||||
|
||||
try {
|
||||
// 遍历所有选中的站点 ID
|
||||
for (const id of selectedRowKeys) {
|
||||
// 构建更新数据对象,只包含用户填写了值的字段
|
||||
const updateData: any = {};
|
||||
|
||||
// 如果用户选择了区域,则更新区域
|
||||
if (values.areas && values.areas.length > 0) {
|
||||
updateData.areas = values.areas;
|
||||
}
|
||||
|
||||
// 如果用户选择了仓库,则更新仓库
|
||||
if (values.stockPointIds && values.stockPointIds.length > 0) {
|
||||
updateData.stockPointIds = values.stockPointIds;
|
||||
}
|
||||
|
||||
// 如果用户设置了禁用状态,则更新状态
|
||||
if (values.isDisabled !== undefined) {
|
||||
updateData.isDisabled = values.isDisabled;
|
||||
}
|
||||
|
||||
// 如果有需要更新的字段,则调用更新接口
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await sitecontrollerUpdate({ id: String(id) }, updateData);
|
||||
}
|
||||
}
|
||||
|
||||
hide();
|
||||
message.success('批量更新成功');
|
||||
setBatchEditOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
batchEditForm.resetFields();
|
||||
actionRef.current?.reload();
|
||||
} catch (error: any) {
|
||||
hide();
|
||||
message.error(error.message || '批量更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 当批量编辑弹窗打开时,重置表单
|
||||
useEffect(() => {
|
||||
if (batchEditOpen) {
|
||||
batchEditForm.resetFields();
|
||||
}
|
||||
}, [batchEditOpen, batchEditForm]);
|
||||
|
||||
// 表格列定义
|
||||
const columns: ProColumns<SiteItem>[] = [
|
||||
{
|
||||
|
|
@ -131,26 +188,47 @@ const SiteList: React.FC = () => {
|
|||
</a>
|
||||
),
|
||||
},
|
||||
{ title: 'webhook地址', dataIndex: 'webhookUrl', width: 280, hideInSearch: true },
|
||||
|
||||
{
|
||||
title: 'SKU 前缀',
|
||||
dataIndex: 'skuPrefix',
|
||||
width: 160,
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'type',
|
||||
width: 140,
|
||||
valueType: 'select',
|
||||
request: async () => [
|
||||
{ label: 'WooCommerce', value: 'woocommerce' },
|
||||
{ label: 'Shopyy', value: 'shopyy' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// 地区列配置
|
||||
title: "地区",
|
||||
dataIndex: "areas",
|
||||
hideInSearch: true,
|
||||
render: (_, row) => {
|
||||
// 如果没有关联地区,显示"全局"标签
|
||||
if (!row.areas || row.areas.length === 0) {
|
||||
return <Tag color="default">全局</Tag>;
|
||||
}
|
||||
// 遍历显示所有关联的地区名称
|
||||
return (
|
||||
<Space wrap>
|
||||
{row.areas.map((area) => (
|
||||
<Tag color="geekblue" key={area.code}>
|
||||
{area.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '关联仓库',
|
||||
dataIndex: 'stockPoints',
|
||||
width: 200,
|
||||
hideInSearch: true,
|
||||
render: (_, row) => {
|
||||
if (!row.stockPoints || row.stockPoints.length === 0) {
|
||||
|
|
@ -232,10 +310,10 @@ const SiteList: React.FC = () => {
|
|||
try {
|
||||
const { current, pageSize, name, type } = params;
|
||||
const resp = await sitecontrollerList({
|
||||
current,
|
||||
pageSize,
|
||||
keyword: name || undefined,
|
||||
type: type || undefined,
|
||||
current,
|
||||
pageSize,
|
||||
keyword: name || undefined,
|
||||
type: type || undefined,
|
||||
});
|
||||
// 假设 resp 直接就是后端返回的结构,包含 items 和 total
|
||||
return {
|
||||
|
|
@ -279,6 +357,11 @@ const SiteList: React.FC = () => {
|
|||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
@ -289,6 +372,12 @@ const SiteList: React.FC = () => {
|
|||
>
|
||||
新建站点
|
||||
</Button>,
|
||||
<Button
|
||||
disabled={!selectedRowKeys.length}
|
||||
onClick={() => setBatchEditOpen(true)}
|
||||
>
|
||||
批量编辑
|
||||
</Button>,
|
||||
<Button
|
||||
disabled={!selectedRowKeys.length}
|
||||
onClick={() => handleSync(selectedRowKeys as number[])}
|
||||
|
|
@ -310,6 +399,51 @@ const SiteList: React.FC = () => {
|
|||
isEdit={!!editing}
|
||||
onFinish={handleFinish}
|
||||
/>
|
||||
|
||||
{/* 批量编辑弹窗 */}
|
||||
<DrawerForm
|
||||
title={`批量编辑站点 (${selectedRowKeys.length} 个)`}
|
||||
form={batchEditForm}
|
||||
open={batchEditOpen}
|
||||
onOpenChange={setBatchEditOpen}
|
||||
onFinish={handleBatchEditFinish}
|
||||
layout="vertical"
|
||||
>
|
||||
<ProFormSelect
|
||||
name="areas"
|
||||
label="区域"
|
||||
mode="multiple"
|
||||
placeholder="请选择区域(留空表示不修改)"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="stockPointIds"
|
||||
label="关联仓库"
|
||||
mode="multiple"
|
||||
placeholder="请选择关联仓库(留空表示不修改)"
|
||||
request={async () => {
|
||||
// 从后端接口获取仓库数据
|
||||
const res = await stockcontrollerGetallstockpoints();
|
||||
// 使用可选链和空值合并运算符来安全地处理可能未定义的数据
|
||||
return (
|
||||
res?.data?.map((sp: any) => ({ label: sp.name, value: sp.id })) ??
|
||||
[]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ProFormSwitch
|
||||
name="isDisabled"
|
||||
label="是否禁用"
|
||||
fieldProps={{
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
}}
|
||||
/>
|
||||
</DrawerForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ const CustomerPage: React.FC = () => {
|
|||
const response = await request(`/site-api/${siteId}/customers`, {
|
||||
params: {
|
||||
page: current,
|
||||
page_size: pageSize,
|
||||
per_page: pageSize,
|
||||
where,
|
||||
...(orderObj ? { order: orderObj } : {}),
|
||||
...(name || email ? { search: name || email } : {}),
|
||||
|
|
@ -465,7 +465,7 @@ const CustomerPage: React.FC = () => {
|
|||
<ProTable
|
||||
rowKey="id"
|
||||
search={false}
|
||||
pagination={{ pageSize: 20 }}
|
||||
pagination={{ pageSize: 20 ,showSizeChanger: true, showQuickJumper: true,}}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'number', copyable: true },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from '@ant-design/pro-components';
|
||||
import { Form } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||
|
||||
// 定义组件的 props 类型
|
||||
interface EditSiteFormProps {
|
||||
|
|
@ -29,6 +31,9 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 初始化中文语言包
|
||||
countries.registerLocale(zhCN);
|
||||
|
||||
// 当 initialValues 或 open 状态变化时, 更新表单的值
|
||||
useEffect(() => {
|
||||
// 如果抽屉是打开的
|
||||
|
|
@ -36,8 +41,10 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
// 如果是编辑模式并且有初始值
|
||||
if (isEdit && initialValues) {
|
||||
// 编辑模式下, 设置表单值为初始值
|
||||
const { token, consumerKey, consumerSecret, ...safeInitialValues } = initialValues;
|
||||
// 清空敏感字段, 让用户输入最新的数据
|
||||
form.setFieldsValue({
|
||||
...initialValues,
|
||||
...safeInitialValues,
|
||||
isDisabled: initialValues.isDisabled === 1, // 将后端的 1/0 转换成 true/false
|
||||
});
|
||||
} else {
|
||||
|
|
@ -47,6 +54,17 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
}
|
||||
}, [initialValues, isEdit, open, form]);
|
||||
|
||||
// 获取所有国家/地区的选项
|
||||
const getCountryOptions = () => {
|
||||
// 获取所有国家的 ISO 代码
|
||||
const countryCodes = countries.getAlpha2Codes();
|
||||
// 将国家代码转换为选项数组
|
||||
return Object.keys(countryCodes).map((code) => ({
|
||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
||||
value: code,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<DrawerForm
|
||||
title={isEdit ? '编辑站点' : '新建站点'}
|
||||
|
|
@ -82,6 +100,11 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
label="网站地址"
|
||||
placeholder="请输入网站地址"
|
||||
/>
|
||||
<ProFormText
|
||||
name="webhookUrl"
|
||||
label="Webhook 地址"
|
||||
placeholder="请输入 Webhook 地址"
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="type"
|
||||
label="平台"
|
||||
|
|
@ -146,15 +169,12 @@ const EditSiteForm: React.FC<EditSiteFormProps> = ({
|
|||
label="区域"
|
||||
mode="multiple"
|
||||
placeholder="请选择区域"
|
||||
request={async () => {
|
||||
// 从后端接口获取区域数据
|
||||
const res = await areacontrollerGetarealist({ pageSize: 1000 });
|
||||
// areacontrollerGetarealist 直接返回数组, 所以不需要 .data.list
|
||||
return res.map((area: any) => ({
|
||||
label: area.name,
|
||||
value: area.code,
|
||||
}));
|
||||
}}
|
||||
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="stockPointIds"
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const ShopLayout: React.FC = () => {
|
|||
<Select
|
||||
style={{ flex: 1 }}
|
||||
placeholder="请选择店铺"
|
||||
options={sites.map((site) => ({
|
||||
options={sites?.map?.((site) => ({
|
||||
label: site.name,
|
||||
value: site.id,
|
||||
}))}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const MediaPage: React.FC = () => {
|
|||
const response = await request(`/site-api/${siteId}/media`, {
|
||||
params: {
|
||||
page: current,
|
||||
page_size: pageSize,
|
||||
per_page: pageSize,
|
||||
...(orderObj ? { order: orderObj } : {}),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
CreateOrder,
|
||||
EditOrder,
|
||||
OrderNote,
|
||||
ShipOrderForm,
|
||||
} from '../components/Order/Forms';
|
||||
|
||||
const OrdersPage: React.FC = () => {
|
||||
|
|
@ -61,11 +62,14 @@ const OrdersPage: React.FC = () => {
|
|||
return [{ key: 'all', label: `全部(${total})` }, ...tabs];
|
||||
}, [count]);
|
||||
|
||||
const columns: ProColumns<API.Order>[] = [
|
||||
const columns: ProColumns<API.UnifiedOrderDTO>[] = [
|
||||
{
|
||||
title: '订单号',
|
||||
title: '订单ID',
|
||||
dataIndex: 'id',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title:'订单号',
|
||||
dataIndex: 'number',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
|
|
@ -73,12 +77,6 @@ const OrdersPage: React.FC = () => {
|
|||
valueType: 'select',
|
||||
valueEnum: ORDER_STATUS_ENUM,
|
||||
},
|
||||
{
|
||||
title: '订单日期',
|
||||
dataIndex: 'date_created',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'total',
|
||||
|
|
@ -89,6 +87,38 @@ const OrdersPage: React.FC = () => {
|
|||
dataIndex: 'currency',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '财务状态',
|
||||
dataIndex: 'financial_status',
|
||||
},
|
||||
{
|
||||
title: '支付方式',
|
||||
dataIndex: 'payment_method',
|
||||
},
|
||||
{
|
||||
title: '支付时间',
|
||||
dataIndex: 'date_paid',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'date_created',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'date_modified',
|
||||
hideInSearch: true,
|
||||
valueType: 'dateTime',
|
||||
},
|
||||
|
||||
{
|
||||
title: '客户ID',
|
||||
dataIndex: 'customer_id',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '客户邮箱',
|
||||
dataIndex: 'email',
|
||||
|
|
@ -96,6 +126,28 @@ const OrdersPage: React.FC = () => {
|
|||
{
|
||||
title: '客户姓名',
|
||||
dataIndex: 'customer_name',
|
||||
},
|
||||
{
|
||||
title: '客户IP',
|
||||
dataIndex: 'customer_ip_address',
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
render: (_, record) => record.shipping?.phone || record.billing?.phone,
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
dataIndex: 'device_type',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '来源类型',
|
||||
dataIndex: 'source_type',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: 'UTM来源',
|
||||
dataIndex: 'utm_source',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -111,7 +163,9 @@ const OrdersPage: React.FC = () => {
|
|||
return (
|
||||
<div>
|
||||
{record.line_items.map((item: any) => (
|
||||
<div key={item.id}>{`${item.name} x ${item.quantity}`}</div>
|
||||
<div
|
||||
key={item.id}
|
||||
>{`${item.name}(${item.sku}) x ${item.quantity}`}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -120,15 +174,6 @@ const OrdersPage: React.FC = () => {
|
|||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '支付方式',
|
||||
dataIndex: 'payment_method',
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => record.shipping?.phone || record.billing?.phone,
|
||||
},
|
||||
{
|
||||
title: '账单地址',
|
||||
dataIndex: 'billing_full_address',
|
||||
|
|
@ -144,6 +189,66 @@ const OrdersPage: React.FC = () => {
|
|||
width: 200,
|
||||
ellipsis: true,
|
||||
copyable: true,
|
||||
},
|
||||
{
|
||||
title: '发货状态',
|
||||
dataIndex: 'fulfillment_status',
|
||||
// hideInSearch: true,
|
||||
// render: (_, record) => {
|
||||
// const fulfillmentStatus = record.fulfillment_status;
|
||||
// const fulfillmentStatusMap: Record<string, string> = {
|
||||
// '0': '未发货',
|
||||
// '1': '部分发货',
|
||||
// '2': '已发货',
|
||||
// '3': '已取消',
|
||||
// '4': '确认发货',
|
||||
// };
|
||||
// if (fulfillmentStatus === undefined || fulfillmentStatus === null) {
|
||||
// return '-';
|
||||
// }
|
||||
// return (
|
||||
// fulfillmentStatusMap[String(fulfillmentStatus)] ||
|
||||
// String(fulfillmentStatus)
|
||||
// );
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '物流',
|
||||
dataIndex: 'fulfillments',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
// 检查是否有物流信息
|
||||
if (
|
||||
!record.fulfillments ||
|
||||
!Array.isArray(record.fulfillments) ||
|
||||
record.fulfillments.length === 0
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
// 遍历物流信息数组, 显示每个物流的提供商和单号
|
||||
return (
|
||||
<div>
|
||||
{record.fulfillments.map((item: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<span>
|
||||
{item.tracking_provider
|
||||
? `快递方式: ${item.tracking_provider}`
|
||||
: ''}
|
||||
</span>
|
||||
{item.tracking_number
|
||||
? `物流单号: ${item.tracking_number}`
|
||||
: ''}
|
||||
<span>
|
||||
{item.date_shipped ? `发货日期: ${item.date_shipped}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
|
|
@ -186,28 +291,17 @@ const OrdersPage: React.FC = () => {
|
|||
>
|
||||
<Button type="text" icon={<EllipsisOutlined />} />
|
||||
</Dropdown>
|
||||
<Button
|
||||
type="link"
|
||||
title="发货"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await request(
|
||||
`/site-api/${siteId}/orders/${record.id}/ship`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
if (res.success) {
|
||||
message.success('发货成功');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.message || '发货失败');
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('发货失败');
|
||||
}
|
||||
}}
|
||||
>
|
||||
发货
|
||||
</Button>
|
||||
<ShipOrderForm
|
||||
orderId={record.id as number}
|
||||
tableRef={actionRef}
|
||||
siteId={siteId}
|
||||
orderItems={(record as any).line_items?.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
sku: item.sku,
|
||||
}))}
|
||||
/>
|
||||
{record.status === 'completed' && (
|
||||
<Popconfirm
|
||||
title="确定取消发货?"
|
||||
|
|
@ -279,6 +373,7 @@ const OrdersPage: React.FC = () => {
|
|||
pagination={{
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000'],
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
|
|
@ -289,6 +384,8 @@ const OrdersPage: React.FC = () => {
|
|||
setSelectedRowKeys={setSelectedRowKeys}
|
||||
siteId={siteId}
|
||||
/>,
|
||||
<Button disabled>批量发货</Button>
|
||||
,
|
||||
<Button
|
||||
title="批量删除"
|
||||
danger
|
||||
|
|
@ -369,18 +466,7 @@ const OrdersPage: React.FC = () => {
|
|||
</ModalForm>,
|
||||
]}
|
||||
request={async (params, sort, filter) => {
|
||||
const p: any = params || {};
|
||||
const current = p.current;
|
||||
const pageSize = p.pageSize;
|
||||
const date = p.date;
|
||||
const status = p.status;
|
||||
const {
|
||||
current: _c,
|
||||
pageSize: _ps,
|
||||
date: _d,
|
||||
status: _s,
|
||||
...rest
|
||||
} = p;
|
||||
const { current, pageSize, date, status, ...rest } = params;
|
||||
const where: Record<string, any> = { ...(filter || {}), ...rest };
|
||||
if (status && status !== 'all') {
|
||||
where.status = status;
|
||||
|
|
@ -401,9 +487,9 @@ const OrdersPage: React.FC = () => {
|
|||
const response = await request(`/site-api/${siteId}/orders`, {
|
||||
params: {
|
||||
page: current,
|
||||
page_size: pageSize,
|
||||
per_page: pageSize,
|
||||
where,
|
||||
...(orderObj ? { order: orderObj } : {}),
|
||||
...(orderObj ? { orderBy: orderObj } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -455,12 +541,8 @@ const OrdersPage: React.FC = () => {
|
|||
return { status: key, count: 0 };
|
||||
}
|
||||
try {
|
||||
const res = await request(`/site-api/${siteId}/orders`, {
|
||||
params: {
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
where: { ...baseWhere, status: rawStatus },
|
||||
},
|
||||
const res = await request(`/site-api/${siteId}/orders/count`, {
|
||||
params: { ...baseWhere, status: rawStatus },
|
||||
});
|
||||
const totalCount = Number(res?.data?.total || 0);
|
||||
return { status: key, count: totalCount };
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ const ProductsPage: React.FC = () => {
|
|||
// ID
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
hideInSearch: true,
|
||||
width: 120,
|
||||
copyable: true,
|
||||
render: (_, record) => {
|
||||
|
|
@ -156,7 +155,7 @@ const ProductsPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
// 库存
|
||||
title: '库存',
|
||||
title: '库存数量',
|
||||
dataIndex: 'stock_quantity',
|
||||
hideInSearch: true,
|
||||
},
|
||||
|
|
@ -394,6 +393,7 @@ const ProductsPage: React.FC = () => {
|
|||
pagination={{
|
||||
pageSizeOptions: ['10', '20', '50', '100', '1000', '2000'],
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
actionRef={actionRef}
|
||||
|
|
@ -422,7 +422,7 @@ const ProductsPage: React.FC = () => {
|
|||
params: {
|
||||
page,
|
||||
per_page: pageSize,
|
||||
...where,
|
||||
where,
|
||||
...(orderObj
|
||||
? {
|
||||
sortField: Object.keys(orderObj)[0],
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ const WebhooksPage: React.FC = () => {
|
|||
message.error(isEditMode ? '更新失败' : '创建失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('表单验证失败:' + error.message);
|
||||
message.error('表单验证失败:' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -305,12 +305,12 @@ const WebhooksPage: React.FC = () => {
|
|||
{ type: 'url', message: '请输入有效的URL' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入回调URL,如:https://example.com/webhook" />
|
||||
<Input placeholder="请输入回调URL,如:https://example.com/webhook" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="secret"
|
||||
label="密钥(可选)"
|
||||
label="密钥(可选)"
|
||||
rules={[{ max: 255, message: '密钥不能超过255个字符' }]}
|
||||
>
|
||||
<Input placeholder="请输入密钥,用于验证webhook请求" />
|
||||
|
|
|
|||
|
|
@ -47,6 +47,133 @@ const region = {
|
|||
YT: 'Yukon',
|
||||
};
|
||||
|
||||
// 定义发货订单表单的数据类型
|
||||
export interface ShipOrderFormData {
|
||||
tracking_number?: string;
|
||||
shipping_provider?: string;
|
||||
shipping_method?: string;
|
||||
items?: Array<{
|
||||
id?: string;
|
||||
quantity?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 发货订单表单组件
|
||||
export const ShipOrderForm: React.FC<{
|
||||
orderId: number;
|
||||
tableRef?: React.MutableRefObject<ActionType | undefined>;
|
||||
siteId?: string;
|
||||
orderItems?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
sku?: string;
|
||||
}>;
|
||||
}> = ({ orderId, tableRef, siteId, orderItems }) => {
|
||||
const { message } = App.useApp();
|
||||
const formRef = useRef<ProFormInstance>();
|
||||
|
||||
return (
|
||||
<ModalForm
|
||||
formRef={formRef}
|
||||
title="发货订单"
|
||||
width="600px"
|
||||
modalProps={{ destroyOnHidden: true }}
|
||||
trigger={
|
||||
<Button type="link" title="发货">
|
||||
发货
|
||||
</Button>
|
||||
}
|
||||
onFinish={async (values: ShipOrderFormData) => {
|
||||
if (!siteId) {
|
||||
message.error('缺少站点ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { success, message: errMsg } = await request(
|
||||
`/site-api/${siteId}/orders/${orderId}/ship`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: values,
|
||||
},
|
||||
);
|
||||
|
||||
if (success === false) {
|
||||
throw new Error(errMsg || '发货失败');
|
||||
}
|
||||
|
||||
message.success('发货成功');
|
||||
tableRef?.current?.reload();
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '发货失败');
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
onFinishFailed={() => {
|
||||
const element = document.querySelector('.ant-form-item-explain-error');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="tracking_number"
|
||||
label="物流单号"
|
||||
placeholder="请输入物流单号"
|
||||
rules={[{ required: true, message: '请输入物流单号' }]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name="shipping_provider"
|
||||
label="物流公司"
|
||||
placeholder="请输入物流公司名称"
|
||||
rules={[{ required: true, message: '请输入物流公司名称' }]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name="shipping_method"
|
||||
label="发货方式"
|
||||
placeholder="请输入发货方式"
|
||||
/>
|
||||
|
||||
{orderItems && orderItems.length > 0 && (
|
||||
<ProFormList
|
||||
label="发货商品项"
|
||||
name="items"
|
||||
tooltip="如果不选择,则默认发货所有商品"
|
||||
>
|
||||
<ProForm.Group>
|
||||
<ProFormSelect
|
||||
name="id"
|
||||
label="商品"
|
||||
placeholder="请选择商品"
|
||||
options={orderItems.map((item) => ({
|
||||
label: `${item.name} (SKU: ${item.sku || 'N/A'}) - 可发数量: ${
|
||||
item.quantity
|
||||
}`,
|
||||
value: item.id,
|
||||
}))}
|
||||
rules={[{ required: true, message: '请选择商品' }]}
|
||||
/>
|
||||
<ProFormDigit
|
||||
name="quantity"
|
||||
label="发货数量"
|
||||
placeholder="请输入发货数量"
|
||||
rules={[{ required: true, message: '请输入发货数量' }]}
|
||||
fieldProps={{
|
||||
precision: 0,
|
||||
min: 1,
|
||||
}}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</ProFormList>
|
||||
)}
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export const OrderNote: React.FC<{
|
||||
id: number;
|
||||
descRef?: React.MutableRefObject<ActionType | undefined>;
|
||||
|
|
|
|||
|
|
@ -69,12 +69,12 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
|
|||
onFinish={handleBind}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<strong>站点产品信息:</strong>
|
||||
<strong>站点产品信息:</strong>
|
||||
<div>SKU: {siteProduct.sku}</div>
|
||||
<div>名称: {siteProduct.name}</div>
|
||||
{siteProduct.erpProduct && (
|
||||
<div style={{ color: '#ff4d4f' }}>
|
||||
⚠️ 当前已绑定ERP产品:{siteProduct.erpProduct.sku} -{' '}
|
||||
⚠️ 当前已绑定ERP产品:{siteProduct.erpProduct.sku} -{' '}
|
||||
{siteProduct.erpProduct.name}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -148,7 +148,7 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
|
|||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
toolBarRender={false}
|
||||
options={false}
|
||||
|
|
@ -164,7 +164,7 @@ export const ErpProductBindModal: React.FC<ErpProductBindModalProps> = ({
|
|||
border: '1px solid #b7eb8f',
|
||||
}}
|
||||
>
|
||||
<strong>已选择:</strong>
|
||||
<strong>已选择:</strong>
|
||||
<div>SKU: {selectedProduct.sku}</div>
|
||||
<div>名称: {selectedProduct.name}</div>
|
||||
{selectedProduct.nameCn && (
|
||||
|
|
|
|||
|
|
@ -613,18 +613,6 @@ export const UpdateVaritation: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
// ... SetComponent, BatchEditProducts, BatchDeleteProducts, ImportCsv ...
|
||||
// I will keep them but comment out/disable parts that rely on old API if I can't easily fix them all.
|
||||
// BatchEdit/Delete rely on old API.
|
||||
// I'll comment out their usage in ProductsPage or just return null here.
|
||||
// I'll keep them but they might break if used.
|
||||
// Since I removed them from ProductsPage toolbar (Wait, I kept them in ProductsPage toolbar!), I should update them or remove them.
|
||||
// I'll update BatchDelete to use new API (loop delete).
|
||||
// BatchEdit? `wpproductcontrollerBatchUpdateProducts`.
|
||||
// I don't have batch update in my new API.
|
||||
// I'll remove BatchEdit from ProductsPage toolbar for now or implement batch update in Controller.
|
||||
// I'll update BatchDelete.
|
||||
|
||||
export const BatchDeleteProducts: React.FC<{
|
||||
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||
selectedRowKeys: React.Key[];
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ORDER_STATUS_ENUM } from '@/constants';
|
|||
import { AddTag } from '@/pages/Customer/List';
|
||||
import { customercontrollerDeltag } from '@/servers/api/customer';
|
||||
import { sitecontrollerAll } from '@/servers/api/site';
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import {
|
||||
statisticscontrollerGetorderbydate,
|
||||
statisticscontrollerGetorderbyemail,
|
||||
|
|
@ -22,7 +23,8 @@ import dayjs from 'dayjs';
|
|||
import ReactECharts from 'echarts-for-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
|
||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||
countries.registerLocale(zhCN);
|
||||
dayjs.extend(weekOfYear);
|
||||
const highlightText = (text: string, keyword: string) => {
|
||||
if (!keyword) return text;
|
||||
|
|
@ -38,6 +40,17 @@ const highlightText = (text: string, keyword: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
// 获取所有国家/地区的选项
|
||||
const getCountryOptions = () => {
|
||||
// 获取所有国家的 ISO 代码
|
||||
const countryCodes = countries.getAlpha2Codes();
|
||||
// 将国家代码转换为选项数组
|
||||
return Object.keys(countryCodes).map((code) => ({
|
||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
||||
value: code,
|
||||
}));
|
||||
};
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
const [xAxis, setXAxis] = useState([]);
|
||||
const [series, setSeries] = useState<any[]>([]);
|
||||
|
|
@ -620,6 +633,19 @@ const ListPage: React.FC = () => {
|
|||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProFormSelect
|
||||
name="country"
|
||||
label="区域"
|
||||
mode="multiple"
|
||||
placeholder="请选择区域"
|
||||
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
{/* <ProFormSelect
|
||||
label="类型"
|
||||
name="purchaseType"
|
||||
|
|
|
|||
|
|
@ -9,18 +9,28 @@ import {
|
|||
PageContainer,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
ProForm,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Space, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { HistoryOrder } from '../Order';
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||
countries.registerLocale(zhCN);
|
||||
const ListPage: React.FC = () => {
|
||||
const [data, setData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
statisticscontrollerGetordersorce().then(({ data, success }) => {
|
||||
const initialValues = {
|
||||
country: ['CA'],
|
||||
};
|
||||
function handleSubmit(values: typeof initialValues) {
|
||||
statisticscontrollerGetordersorce({params: values}).then(({ data, success }) => {
|
||||
if (success) setData(data);
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
handleSubmit(initialValues)
|
||||
}, []);
|
||||
|
||||
const option = useMemo(() => {
|
||||
|
|
@ -39,11 +49,11 @@ const ListPage: React.FC = () => {
|
|||
data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (params) {
|
||||
formatter: function (params) {
|
||||
if (!params.value) return '';
|
||||
return Math.abs(params.value)
|
||||
+'\n'
|
||||
+Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.new_user_total || 0);
|
||||
return Math.abs(params.value)
|
||||
+ '\n'
|
||||
+ Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.new_user_total || 0);
|
||||
},
|
||||
color: '#000000',
|
||||
},
|
||||
|
|
@ -59,11 +69,11 @@ const ListPage: React.FC = () => {
|
|||
data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
|
||||
label: {
|
||||
show: true,
|
||||
formatter: function (params) {
|
||||
formatter: function (params) {
|
||||
if (!params.value) return '';
|
||||
return Math.abs(params.value)
|
||||
+'\n'
|
||||
+Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.old_user_total || 0);
|
||||
return Math.abs(params.value)
|
||||
+ '\n'
|
||||
+ Math.abs(data?.inactiveRes?.find((item) => item.order_month === params.name)?.old_user_total || 0);
|
||||
},
|
||||
color: '#000000',
|
||||
},
|
||||
|
|
@ -83,11 +93,11 @@ const ListPage: React.FC = () => {
|
|||
show: true,
|
||||
formatter: function (params) {
|
||||
if (!params.value) return '';
|
||||
return Math.abs(params.value)
|
||||
+'\n'+
|
||||
+Math.abs(data?.res?.find((item) => item.order_month === params.name &&
|
||||
return Math.abs(params.value)
|
||||
+ '\n' +
|
||||
+Math.abs(data?.res?.find((item) => item.order_month === params.name &&
|
||||
item.first_order_month_group === v)?.total || 0);
|
||||
},
|
||||
},
|
||||
color: '#000000',
|
||||
},
|
||||
data: xAxisData.map((month) => {
|
||||
|
|
@ -258,9 +268,29 @@ const ListPage: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer ghost>
|
||||
<ProForm
|
||||
initialValues={initialValues}
|
||||
layout="inline"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
|
||||
<ProFormSelect
|
||||
name="country"
|
||||
label="区域"
|
||||
mode="multiple"
|
||||
placeholder="请选择区域"
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
</ProForm>
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ height: 1050 }}
|
||||
|
|
@ -277,6 +307,7 @@ const ListPage: React.FC = () => {
|
|||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{tableData?.length ? (
|
||||
<ProTable
|
||||
search={false}
|
||||
|
|
@ -293,4 +324,17 @@ const ListPage: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 获取所有国家/地区的选项
|
||||
const getCountryOptions = () => {
|
||||
// 获取所有国家的 ISO 代码
|
||||
const countryCodes = countries.getAlpha2Codes();
|
||||
// 将国家代码转换为选项数组
|
||||
|
||||
return Object.keys(countryCodes).map((code) => ({
|
||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
||||
value: code,
|
||||
}));
|
||||
};
|
||||
export default ListPage;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ import {
|
|||
ProFormText,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { request } from '@umijs/max';
|
||||
import { App, Button, Divider, Popconfirm, Space, Tag } from 'antd';
|
||||
import { useRef } from 'react';
|
||||
import * as countries from 'i18n-iso-countries';
|
||||
import zhCN from 'i18n-iso-countries/langs/zh';
|
||||
|
||||
// 初始化中文语言包
|
||||
countries.registerLocale(zhCN);
|
||||
|
||||
// 区域数据项类型
|
||||
interface AreaItem {
|
||||
|
|
@ -25,6 +29,17 @@ interface AreaItem {
|
|||
name: string;
|
||||
}
|
||||
|
||||
// 获取所有国家/地区的选项
|
||||
const getCountryOptions = () => {
|
||||
// 获取所有国家的 ISO 代码
|
||||
const countryCodes = countries.getAlpha2Codes();
|
||||
// 将国家代码转换为选项数组
|
||||
return Object.keys(countryCodes).map((code) => ({
|
||||
label: countries.getName(code, 'zh') || code, // 使用中文名称, 如果没有则使用代码
|
||||
value: code,
|
||||
}));
|
||||
};
|
||||
|
||||
const ListPage: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const actionRef = useRef<ActionType>();
|
||||
|
|
@ -190,23 +205,11 @@ const CreateForm: React.FC<{
|
|||
width="lg"
|
||||
mode="multiple"
|
||||
placeholder="留空表示全球"
|
||||
request={async () => {
|
||||
try {
|
||||
const resp = await request('/area', {
|
||||
method: 'GET',
|
||||
params: { pageSize: 1000 },
|
||||
});
|
||||
if (resp.success) {
|
||||
return resp.data.list.map((area: AreaItem) => ({
|
||||
label: area.name,
|
||||
value: area.code,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</DrawerForm>
|
||||
|
|
@ -289,23 +292,11 @@ const UpdateForm: React.FC<{
|
|||
width="lg"
|
||||
mode="multiple"
|
||||
placeholder="留空表示全球"
|
||||
request={async () => {
|
||||
try {
|
||||
const resp = await request('/area', {
|
||||
method: 'GET',
|
||||
params: { pageSize: 1000 },
|
||||
});
|
||||
if (resp.success) {
|
||||
return resp.data.list.map((area: AreaItem) => ({
|
||||
label: area.name,
|
||||
value: area.code,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={getCountryOptions()}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
</DrawerForm>
|
||||
|
|
|
|||
|
|
@ -161,7 +161,10 @@ const OrdersPage: React.FC = () => {
|
|||
rowKey="id"
|
||||
columns={columns}
|
||||
request={request}
|
||||
pagination={{ showSizeChanger: true }}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
search={{
|
||||
labelWidth: 90,
|
||||
span: 6,
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ export async function categorycontrollerGetlist(
|
|||
|
||||
/** 此处后端没有提供注释 POST /category/ */
|
||||
export async function categorycontrollerCreate(
|
||||
body: Record<string, any>,
|
||||
body: API.CreateCategoryDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/category/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
|
|
@ -40,14 +40,14 @@ export async function categorycontrollerCreate(
|
|||
export async function categorycontrollerUpdate(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.categorycontrollerUpdateParams,
|
||||
body: Record<string, any>,
|
||||
body: API.UpdateCategoryDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/category/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,68 @@
|
|||
/* eslint-disable */
|
||||
import { request } from 'umi';
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/ */
|
||||
export async function customercontrollerCreatecustomer(
|
||||
body: API.CreateCustomerDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.GetCustomerDTO>('/customer/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/${param0} */
|
||||
export async function customercontrollerGetcustomerbyid(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerGetcustomerbyidParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /customer/${param0} */
|
||||
export async function customercontrollerUpdatecustomer(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerUpdatecustomerParams,
|
||||
body: API.UpdateCustomerDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<API.GetCustomerDTO>(`/customer/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 DELETE /customer/${param0} */
|
||||
export async function customercontrollerDeletecustomer(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerDeletecustomerParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<Record<string, any>>(`/customer/${param0}`, {
|
||||
method: 'DELETE',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/addtag */
|
||||
export async function customercontrollerAddtag(
|
||||
body: API.CustomerTagDTO,
|
||||
|
|
@ -17,6 +79,51 @@ export async function customercontrollerAddtag(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /customer/batch */
|
||||
export async function customercontrollerBatchupdatecustomers(
|
||||
body: API.BatchUpdateCustomerDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/batch', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/batch */
|
||||
export async function customercontrollerBatchcreatecustomers(
|
||||
body: API.BatchCreateCustomerDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 DELETE /customer/batch */
|
||||
export async function customercontrollerBatchdeletecustomers(
|
||||
body: API.BatchDeleteCustomerDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/batch', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/deltag */
|
||||
export async function customercontrollerDeltag(
|
||||
body: API.CustomerTagDTO,
|
||||
|
|
@ -32,36 +139,6 @@ export async function customercontrollerDeltag(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/getcustomerlist */
|
||||
export async function customercontrollerGetcustomerlist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerGetcustomerlistParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/getcustomerlist', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/getcustomerstatisticlist */
|
||||
export async function customercontrollerGetcustomerstatisticlist(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.customercontrollerGetcustomerstatisticlistParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/getcustomerstatisticlist', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/gettags */
|
||||
export async function customercontrollerGettags(options?: {
|
||||
[key: string]: any;
|
||||
|
|
@ -72,6 +149,16 @@ export async function customercontrollerGettags(options?: {
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/list */
|
||||
export async function customercontrollerGetcustomerlist(options?: {
|
||||
[key: string]: any;
|
||||
}) {
|
||||
return request<API.ApiResponse>('/customer/list', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/setrate */
|
||||
export async function customercontrollerSetrate(
|
||||
body: Record<string, any>,
|
||||
|
|
@ -87,15 +174,25 @@ export async function customercontrollerSetrate(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /customer/statistic/list */
|
||||
export async function customercontrollerGetcustomerstatisticlist(options?: {
|
||||
[key: string]: any;
|
||||
}) {
|
||||
return request<Record<string, any>>('/customer/statistic/list', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /customer/sync */
|
||||
export async function customercontrollerSynccustomers(
|
||||
body: Record<string, any>,
|
||||
body: API.SyncCustomersDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<Record<string, any>>('/customer/sync', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
|
|
|
|||
|
|
@ -151,12 +151,27 @@ export async function dictcontrollerDeletedictitem(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /dict/item/export */
|
||||
export async function dictcontrollerExportdictitems(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.dictcontrollerExportdictitemsParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/dict/item/export', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /dict/item/import */
|
||||
export async function dictcontrollerImportdictitems(
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/dict/item/import', {
|
||||
return request<API.ApiResponse>('/dict/item/import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
// @ts-ignore
|
||||
/* eslint-disable */
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
// API 更新时间:
|
||||
// API 唯一标识:
|
||||
import * as area from './area';
|
||||
import * as category from './category';
|
||||
import * as customer from './customer';
|
||||
import * as dict from './dict';
|
||||
import * as locales from './locales';
|
||||
import * as logistics from './logistics';
|
||||
import * as media from './media';
|
||||
import * as order from './order';
|
||||
import * as product from './product';
|
||||
import * as site from './site';
|
||||
|
|
@ -26,7 +25,6 @@ export default {
|
|||
dict,
|
||||
locales,
|
||||
logistics,
|
||||
media,
|
||||
order,
|
||||
product,
|
||||
siteApi,
|
||||
|
|
|
|||
|
|
@ -1,92 +0,0 @@
|
|||
// @ts-ignore
|
||||
/* eslint-disable */
|
||||
import { request } from 'umi';
|
||||
|
||||
/** 此处后端没有提供注释 DELETE /media/${param0} */
|
||||
export async function mediacontrollerDelete(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.mediacontrollerDeleteParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/media/${param0}`, {
|
||||
method: 'DELETE',
|
||||
params: {
|
||||
...queryParams,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /media/list */
|
||||
export async function mediacontrollerList(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.mediacontrollerListParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/media/list', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /media/update/${param0} */
|
||||
export async function mediacontrollerUpdate(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.mediacontrollerUpdateParams,
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/media/update/${param0}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /media/upload */
|
||||
export async function mediacontrollerUpload(
|
||||
body: {},
|
||||
files?: File[],
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const formData = new FormData();
|
||||
|
||||
if (files) {
|
||||
files.forEach((f) => formData.append('files', f || ''));
|
||||
}
|
||||
|
||||
Object.keys(body).forEach((ele) => {
|
||||
const item = (body as any)[ele];
|
||||
|
||||
if (item !== undefined && item !== null) {
|
||||
if (typeof item === 'object' && !(item instanceof File)) {
|
||||
if (item instanceof Array) {
|
||||
item.forEach((f) => formData.append(ele, f || ''));
|
||||
} else {
|
||||
formData.append(
|
||||
ele,
|
||||
new Blob([JSON.stringify(item)], { type: 'application/json' }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
formData.append(ele, item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return request<any>('/media/upload', {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
requestType: 'form',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
|
@ -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 */
|
||||
export async function ordercontrollerGetorderbynumber(
|
||||
body: string,
|
||||
|
|
@ -225,15 +240,15 @@ export async function ordercontrollerChangestatus(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /order/syncOrder/${param0} */
|
||||
export async function ordercontrollerSyncorder(
|
||||
/** 此处后端没有提供注释 POST /order/sync/${param0} */
|
||||
export async function ordercontrollerSyncorders(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.ordercontrollerSyncorderParams,
|
||||
params: API.ordercontrollerSyncordersParams,
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { siteId: param0, ...queryParams } = params;
|
||||
return request<API.BooleanRes>(`/order/syncOrder/${param0}`, {
|
||||
return request<API.BooleanRes>(`/order/sync/${param0}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
|
|
@ -251,11 +266,14 @@ export async function ordercontrollerSyncorderbyid(
|
|||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { orderId: param0, siteId: param1, ...queryParams } = params;
|
||||
return request<API.BooleanRes>(`/order/syncOrder/${param1}/order/${param0}`, {
|
||||
method: 'POST',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
});
|
||||
return request<API.SyncOperationResult>(
|
||||
`/order/syncOrder/${param1}/order/${param0}`,
|
||||
{
|
||||
method: 'POST',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /order/updateOrderItems/${param0} */
|
||||
|
|
|
|||
|
|
@ -229,6 +229,36 @@ export async function productcontrollerBatchdeleteproduct(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/batch-sync-from-site */
|
||||
export async function productcontrollerBatchsyncfromsite(
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.SyncOperationResultDTO>('/product/batch-sync-from-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/batch-sync-to-site */
|
||||
export async function productcontrollerBatchsynctosite(
|
||||
body: API.BatchSyncProductToSiteDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.SyncOperationResultDTO>('/product/batch-sync-to-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /product/batch-update */
|
||||
export async function productcontrollerBatchupdateproduct(
|
||||
body: API.BatchUpdateProductDTO,
|
||||
|
|
@ -333,13 +363,13 @@ export async function productcontrollerGetcategoriesall(options?: {
|
|||
|
||||
/** 此处后端没有提供注释 POST /product/category */
|
||||
export async function productcontrollerCreatecategory(
|
||||
body: Record<string, any>,
|
||||
body: API.CreateCategoryDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/product/category', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
|
|
@ -350,14 +380,14 @@ export async function productcontrollerCreatecategory(
|
|||
export async function productcontrollerUpdatecategory(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.productcontrollerUpdatecategoryParams,
|
||||
body: Record<string, any>,
|
||||
body: API.UpdateCategoryDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, ...queryParams } = params;
|
||||
return request<any>(`/product/category/${param0}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
|
|
@ -760,6 +790,21 @@ export async function productcontrollerCompatstrengthall(options?: {
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/sync-from-site */
|
||||
export async function productcontrollerSyncproductfromsite(
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.ProductRes>('/product/sync-from-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/sync-stock */
|
||||
export async function productcontrollerSyncstocktoproduct(options?: {
|
||||
[key: string]: any;
|
||||
|
|
@ -770,6 +815,21 @@ export async function productcontrollerSyncstocktoproduct(options?: {
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /product/sync-to-site */
|
||||
export async function productcontrollerSynctosite(
|
||||
body: API.SyncProductToSiteDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.SyncProductToSiteResultDTO>('/product/sync-to-site', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /productupdateNameCn/${param1}/${param0} */
|
||||
export async function productcontrollerUpdatenamecn(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -59,9 +59,16 @@ export async function sitecontrollerGet(
|
|||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site/list */
|
||||
export async function sitecontrollerList(options?: { [key: string]: any }) {
|
||||
export async function sitecontrollerList(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.sitecontrollerListParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/site/list', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@ export async function siteapicontrollerGetcustomers(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
|
|
@ -74,10 +70,6 @@ export async function siteapicontrollerExportcustomers(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -127,10 +119,6 @@ export async function siteapicontrollerGetmedia(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -210,10 +198,6 @@ export async function siteapicontrollerExportmedia(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -230,10 +214,6 @@ export async function siteapicontrollerGetorders(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -280,21 +260,38 @@ export async function siteapicontrollerBatchorders(
|
|||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-ship */
|
||||
export async function siteapicontrollerBatchshiporders(
|
||||
/** 此处后端没有提供注释 POST /site-api/${param0}/orders/batch-fulfill */
|
||||
export async function siteapicontrollerBatchfulfillorders(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerBatchshipordersParams,
|
||||
body: API.BatchShipOrdersDTO,
|
||||
params: API.siteapicontrollerBatchfulfillordersParams,
|
||||
body: API.BatchFulfillmentsDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { siteId: param0, ...queryParams } = params;
|
||||
return request<Record<string, any>>(`/site-api/${param0}/orders/batch-ship`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
return request<Record<string, any>>(
|
||||
`/site-api/${param0}/orders/batch-fulfill`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site-api/${param0}/orders/count */
|
||||
export async function siteapicontrollerCountorders(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerCountordersParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { siteId: param0, ...queryParams } = params;
|
||||
return request<Record<string, any>>(`/site-api/${param0}/orders/count`, {
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
|
@ -310,10 +307,6 @@ export async function siteapicontrollerExportorders(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -351,10 +344,6 @@ export async function siteapicontrollerGetproducts(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
|
|
@ -402,6 +391,28 @@ export async function siteapicontrollerBatchproducts(
|
|||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param0}/products/batch-upsert */
|
||||
export async function siteapicontrollerBatchupsertproduct(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerBatchupsertproductParams,
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { siteId: param0, ...queryParams } = params;
|
||||
return request<API.BatchOperationResultDTO>(
|
||||
`/site-api/${param0}/products/batch-upsert`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site-api/${param0}/products/export */
|
||||
export async function siteapicontrollerExportproducts(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
@ -413,10 +424,6 @@ export async function siteapicontrollerExportproducts(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -433,10 +440,6 @@ export async function siteapicontrollerExportproductsspecial(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -483,6 +486,25 @@ export async function siteapicontrollerImportproductsspecial(
|
|||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param0}/products/upsert */
|
||||
export async function siteapicontrollerUpsertproduct(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerUpsertproductParams,
|
||||
body: API.UnifiedProductDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { siteId: param0, ...queryParams } = params;
|
||||
return request<API.UnifiedProductDTO>(`/site-api/${param0}/products/upsert`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site-api/${param0}/reviews */
|
||||
export async function siteapicontrollerGetreviews(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
@ -496,10 +518,6 @@ export async function siteapicontrollerGetreviews(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
|
|
@ -538,10 +556,6 @@ export async function siteapicontrollerGetsubscriptions(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
|
|
@ -559,10 +573,6 @@ export async function siteapicontrollerExportsubscriptions(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -579,10 +589,6 @@ export async function siteapicontrollerGetwebhooks(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
});
|
||||
|
|
@ -676,10 +682,6 @@ export async function siteapicontrollerGetcustomerorders(
|
|||
method: 'GET',
|
||||
params: {
|
||||
...queryParams,
|
||||
where: undefined,
|
||||
...queryParams['where'],
|
||||
orderBy: undefined,
|
||||
...queryParams['orderBy'],
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
|
|
@ -766,16 +768,55 @@ export async function siteapicontrollerDeleteorder(
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/cancel-ship */
|
||||
export async function siteapicontrollerCancelshiporder(
|
||||
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/cancel-fulfill */
|
||||
export async function siteapicontrollerCancelfulfillment(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerCancelshiporderParams,
|
||||
body: API.CancelShipOrderDTO,
|
||||
params: API.siteapicontrollerCancelfulfillmentParams,
|
||||
body: API.CancelFulfillmentDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, siteId: param1, ...queryParams } = params;
|
||||
return request<Record<string, any>>(
|
||||
`/site-api/${param1}/orders/${param0}/cancel-ship`,
|
||||
`/site-api/${param1}/orders/${param0}/cancel-fulfill`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site-api/${param1}/orders/${param0}/fulfillments */
|
||||
export async function siteapicontrollerGetorderfulfillments(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerGetorderfulfillmentsParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { orderId: param0, siteId: param1, ...queryParams } = params;
|
||||
return request<Record<string, any>>(
|
||||
`/site-api/${param1}/orders/${param0}/fulfillments`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/fulfillments */
|
||||
export async function siteapicontrollerCreateorderfulfillment(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerCreateorderfulfillmentParams,
|
||||
body: API.UnifiedOrderTrackingDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { orderId: param0, siteId: param1, ...queryParams } = params;
|
||||
return request<API.UnifiedOrderTrackingDTO>(
|
||||
`/site-api/${param1}/orders/${param0}/fulfillments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -827,28 +868,6 @@ export async function siteapicontrollerCreateordernote(
|
|||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /site-api/${param1}/orders/${param0}/ship */
|
||||
export async function siteapicontrollerShiporder(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerShiporderParams,
|
||||
body: API.ShipOrderDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const { id: param0, siteId: param1, ...queryParams } = params;
|
||||
return request<Record<string, any>>(
|
||||
`/site-api/${param1}/orders/${param0}/ship`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 GET /site-api/${param1}/products/${param0} */
|
||||
export async function siteapicontrollerGetproduct(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
@ -997,6 +1016,55 @@ export async function siteapicontrollerDeletewebhook(
|
|||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /site-api/${param2}/orders/${param1}/fulfillments/${param0} */
|
||||
export async function siteapicontrollerUpdateorderfulfillment(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerUpdateorderfulfillmentParams,
|
||||
body: API.UnifiedOrderTrackingDTO,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const {
|
||||
fulfillmentId: param0,
|
||||
orderId: param1,
|
||||
siteId: param2,
|
||||
...queryParams
|
||||
} = params;
|
||||
return request<API.UnifiedOrderTrackingDTO>(
|
||||
`/site-api/${param2}/orders/${param1}/fulfillments/${param0}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: { ...queryParams },
|
||||
data: body,
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 DELETE /site-api/${param2}/orders/${param1}/fulfillments/${param0} */
|
||||
export async function siteapicontrollerDeleteorderfulfillment(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.siteapicontrollerDeleteorderfulfillmentParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
const {
|
||||
fulfillmentId: param0,
|
||||
orderId: param1,
|
||||
siteId: param2,
|
||||
...queryParams
|
||||
} = params;
|
||||
return request<Record<string, any>>(
|
||||
`/site-api/${param2}/orders/${param1}/fulfillments/${param0}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
params: { ...queryParams },
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 PUT /site-api/${param2}/products/${param1}/variations/${param0} */
|
||||
export async function siteapicontrollerUpdatevariation(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -10,6 +10,26 @@ export async function webhookcontrollerTest(options?: { [key: string]: any }) {
|
|||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /webhook/shoppy */
|
||||
export async function webhookcontrollerHandleshoppywebhook(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.webhookcontrollerHandleshoppywebhookParams,
|
||||
body: Record<string, any>,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<any>('/webhook/shoppy', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
data: body,
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** 此处后端没有提供注释 POST /webhook/woocommerce */
|
||||
export async function webhookcontrollerHandlewoowebhook(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { message } from 'antd';
|
||||
|
||||
/**
|
||||
* 批量操作错误项接口
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
identifier: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果接口
|
||||
*/
|
||||
export interface BatchOperationResult {
|
||||
total: number;
|
||||
processed: number;
|
||||
created?: number;
|
||||
updated?: number;
|
||||
deleted?: number;
|
||||
synced?: number;
|
||||
errors?: BatchErrorItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示批量操作结果(导入、删除等)
|
||||
* @param result 批量操作结果对象
|
||||
* @param operationType 操作类型,用于显示在消息中
|
||||
*/
|
||||
export function showBatchOperationResult(
|
||||
result: BatchOperationResult,
|
||||
operationType: string = '操作',
|
||||
): string {
|
||||
// 从 result.data 中获取实际数据(因为后端返回格式为 { success: true, data: {...} })
|
||||
const data = (result as any).data || result;
|
||||
const { total, processed, created, updated, deleted, errors } = data;
|
||||
|
||||
// 构建结果消息
|
||||
let messageContent = `${operationType}结果:共 ${total} 条,成功 ${processed} 条`;
|
||||
|
||||
if (created) {
|
||||
messageContent += `,创建 ${created} 条`;
|
||||
}
|
||||
if (updated) {
|
||||
messageContent += `,更新 ${updated} 条`;
|
||||
}
|
||||
if (deleted) {
|
||||
messageContent += `,删除 ${deleted} 条`;
|
||||
}
|
||||
|
||||
// 处理错误情况
|
||||
if (errors && errors.length > 0) {
|
||||
messageContent += `,失败 ${errors.length} 条`;
|
||||
// 显示错误详情
|
||||
const errorDetails = errors
|
||||
.map((err: BatchErrorItem) => `${err.identifier}: ${err.error}`)
|
||||
.join('\n');
|
||||
message.warning(messageContent + '\n\n错误详情:\n' + errorDetails);
|
||||
} else {
|
||||
message.success(messageContent);
|
||||
}
|
||||
|
||||
return messageContent;
|
||||
}
|
||||
Loading…
Reference in New Issue