forked from yoone/WEB
Compare commits
5 Commits
adbc087171
...
bd3d9a00c5
| Author | SHA1 | Date |
|---|---|---|
|
|
bd3d9a00c5 | |
|
|
0c90b2ae8e | |
|
|
a6e569c50b | |
|
|
8b7af3e6a4 | |
|
|
97d4540345 |
|
|
@ -16,3 +16,4 @@
|
||||||
*.yaml
|
*.yaml
|
||||||
#
|
#
|
||||||
/docs
|
/docs
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
30
.umirc.ts
30
.umirc.ts
|
|
@ -43,6 +43,18 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '站点管理',
|
||||||
|
path: '/site',
|
||||||
|
access: 'canSeeSite',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: '站点列表',
|
||||||
|
path: '/site/list',
|
||||||
|
component: './Site/List',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '商品管理',
|
name: '商品管理',
|
||||||
path: '/product',
|
path: '/product',
|
||||||
|
|
@ -124,6 +136,24 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 新增:订阅管理路由分组(权限复用 canSeeOrder)
|
||||||
|
{
|
||||||
|
name: '订阅管理',
|
||||||
|
path: '/subscription',
|
||||||
|
access: 'canSeeOrder',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: '订阅列表',
|
||||||
|
path: '/subscription/list',
|
||||||
|
component: './Subscription/List',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '订单管理',
|
||||||
|
path: '/subscription/orders',
|
||||||
|
component: './Subscription/Orders',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '客户管理',
|
name: '客户管理',
|
||||||
path: '/customer',
|
path: '/customer',
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
"code-inspector-plugin": "^1.2.10",
|
"code-inspector-plugin": "^1.2.10",
|
||||||
"husky": "^9",
|
"husky": "^9",
|
||||||
"lint-staged": "^13.2.0",
|
"lint-staged": "^13.2.0",
|
||||||
|
"openapi2ts": "^1.1.14",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"prettier-plugin-organize-imports": "^3.2.2",
|
"prettier-plugin-organize-imports": "^3.2.2",
|
||||||
"prettier-plugin-packagejson": "^2.4.3",
|
"prettier-plugin-packagejson": "^2.4.3",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export default (initialState: any) => {
|
||||||
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
|
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
|
||||||
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
|
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
|
||||||
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
|
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
|
||||||
|
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canSeeOrganiza,
|
canSeeOrganiza,
|
||||||
|
|
@ -18,5 +19,6 @@ export default (initialState: any) => {
|
||||||
canSeeCustomer,
|
canSeeCustomer,
|
||||||
canSeeLogistics,
|
canSeeLogistics,
|
||||||
canSeeStatistics,
|
canSeeStatistics,
|
||||||
|
canSeeSite,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { usercontrollerGetuser } from './servers/api/user';
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||||
export async function getInitialState(): Promise<{
|
export async function getInitialState(): Promise<{
|
||||||
user?: Record<string, any>;
|
user?: Record<string, any>;
|
||||||
categoryList?: ProSchemaValueEnumObj;
|
categoryList?: ProSchemaValueEnumObj;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook: 获取设备指纹(visitorId)
|
* Hook: 获取设备指纹(visitorId)
|
||||||
*/
|
*/
|
||||||
export function useDeviceFingerprint() {
|
export function useDeviceFingerprint() {
|
||||||
const [fingerprint, setFingerprint] = useState<string | null>(null);
|
const [fingerprint, setFingerprint] = useState<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
|
||||||
|
import { ProTable } from '@ant-design/pro-components';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ordercontrollerGetordersales } from '@/servers/api/order';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
|
// 列表行数据结构(订单商品聚合)
|
||||||
|
interface OrderItemAggRow {
|
||||||
|
externalProductId: number; // 商品ID(来自 WooCommerce 产品ID)
|
||||||
|
externalVariationId: number; // 变体ID(来自 WooCommerce 变体ID)
|
||||||
|
name: string; // 商品名称
|
||||||
|
totalQuantity: number; // 总售出数量(时间范围内)
|
||||||
|
totalOrders: number; // 涉及订单数(去重)
|
||||||
|
firstOrderCount: number; // 客户首单次数(该商品)
|
||||||
|
secondOrderCount: number; // 客户第二次购买次数(该商品)
|
||||||
|
thirdOrderCount: number; // 客户第三次购买次数(该商品)
|
||||||
|
moreThirdOrderCount: number; // 客户超过三次购买次数(该商品)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderItemsPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
|
||||||
|
const columns: ProColumns<OrderItemAggRow>[] = [
|
||||||
|
{
|
||||||
|
title: '商品名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品ID',
|
||||||
|
dataIndex: 'externalProductId',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '变体ID',
|
||||||
|
dataIndex: 'externalVariationId',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '总售出数量',
|
||||||
|
dataIndex: 'totalQuantity',
|
||||||
|
width: 130,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订单数',
|
||||||
|
dataIndex: 'totalOrders',
|
||||||
|
width: 110,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '首单次数',
|
||||||
|
dataIndex: 'firstOrderCount',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '第二次购买',
|
||||||
|
dataIndex: 'secondOrderCount',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '第三次购买',
|
||||||
|
dataIndex: 'thirdOrderCount',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '超过三次购买',
|
||||||
|
dataIndex: 'moreThirdOrderCount',
|
||||||
|
width: 140,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
// 搜索区域字段
|
||||||
|
{
|
||||||
|
title: '站点',
|
||||||
|
dataIndex: 'siteId',
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
// 拉取站点列表(后台 /site/all)
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间范围',
|
||||||
|
dataIndex: 'dateRange',
|
||||||
|
valueType: 'dateRange',
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品关键字',
|
||||||
|
dataIndex: 'name',
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
|
||||||
|
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => {
|
||||||
|
try {
|
||||||
|
const { current = 1, pageSize = 10, siteId, name } = params as any;
|
||||||
|
const [startDate, endDate] = (params as any).dateRange || [];
|
||||||
|
// 调用后端接口(isSource=true 表示按订单项聚合)
|
||||||
|
const resp = await ordercontrollerGetordersales({
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
siteId,
|
||||||
|
name,
|
||||||
|
isSource: true as any,
|
||||||
|
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||||
|
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||||
|
} as any);
|
||||||
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
|
return {
|
||||||
|
data: (data?.items ?? []) as OrderItemAggRow[],
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '获取失败');
|
||||||
|
return { data: [], total: 0, success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title='订单商品概览'>
|
||||||
|
<ProTable<OrderItemAggRow>
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`}
|
||||||
|
columns={columns}
|
||||||
|
request={request}
|
||||||
|
pagination={{ showSizeChanger: true }}
|
||||||
|
search={{ labelWidth: 90, span: 6 }}
|
||||||
|
toolBarRender={false}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderItemsPage;
|
||||||
|
|
@ -77,9 +77,10 @@ import {
|
||||||
Space,
|
Space,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsProps,
|
TabsProps,
|
||||||
|
Tag,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import Item from 'antd/es/list/Item';
|
import Item from 'antd/es/list/Item';
|
||||||
import dayjs from 'dayjs';
|
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
|
||||||
import React, { useMemo, useRef, useState } from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import { printPDF } from '@/utils/util';
|
import { printPDF } from '@/utils/util';
|
||||||
|
|
||||||
|
|
@ -172,9 +173,16 @@ const ListPage: React.FC = () => {
|
||||||
hideInTable: true,
|
hideInTable: true,
|
||||||
valueType: 'dateRange',
|
valueType: 'dateRange',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: '订单号',
|
title: '订阅',
|
||||||
dataIndex: 'externalOrderId',
|
dataIndex: 'isSubscription',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => {
|
||||||
|
const related = Array.isArray((record as any)?.related) ? (record as any).related : [];
|
||||||
|
const isSub = related.some((it) => it?.externalSubscriptionId || it?.billing_period || it?.line_items);
|
||||||
|
return <Tag color={isSub ? 'green' : 'default'}>{isSub ? '是' : '否'}</Tag>;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '站点',
|
title: '站点',
|
||||||
|
|
@ -816,7 +824,7 @@ const Detail: React.FC<{
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
company:
|
company:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.company ||
|
{record?.shipping?.company ||
|
||||||
record?.billing?.company ||
|
record?.billing?.company ||
|
||||||
|
|
@ -824,7 +832,7 @@ const Detail: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
first_name:
|
first_name:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.first_name ||
|
{record?.shipping?.first_name ||
|
||||||
record?.billing?.first_name ||
|
record?.billing?.first_name ||
|
||||||
|
|
@ -832,7 +840,7 @@ const Detail: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
last_name:
|
last_name:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.last_name ||
|
{record?.shipping?.last_name ||
|
||||||
record?.billing?.last_name ||
|
record?.billing?.last_name ||
|
||||||
|
|
@ -840,7 +848,7 @@ const Detail: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
country:
|
country:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.country ||
|
{record?.shipping?.country ||
|
||||||
record?.billing?.country ||
|
record?.billing?.country ||
|
||||||
|
|
@ -848,19 +856,19 @@ const Detail: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
state:
|
state:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.state || record?.billing?.state || '-'}
|
{record?.shipping?.state || record?.billing?.state || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
city:
|
city:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.city || record?.billing?.city || '-'}
|
{record?.shipping?.city || record?.billing?.city || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
postcode:
|
postcode:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.postcode ||
|
{record?.shipping?.postcode ||
|
||||||
record?.billing?.postcode ||
|
record?.billing?.postcode ||
|
||||||
|
|
@ -868,13 +876,13 @@ const Detail: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
phone:
|
phone:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.phone || record?.billing?.phone || '-'}
|
{record?.shipping?.phone || record?.billing?.phone || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
address_1:
|
address_1:
|
||||||
<span>
|
<span>
|
||||||
{record?.shipping?.address_1 ||
|
{record?.shipping?.address_1 ||
|
||||||
record?.billing?.address_1 ||
|
record?.billing?.address_1 ||
|
||||||
|
|
@ -885,6 +893,7 @@ const Detail: React.FC<{
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* 原始订单 */}
|
||||||
<ProDescriptions.Item
|
<ProDescriptions.Item
|
||||||
label="原始订单"
|
label="原始订单"
|
||||||
span={3}
|
span={3}
|
||||||
|
|
@ -893,13 +902,22 @@ const Detail: React.FC<{
|
||||||
<ul>
|
<ul>
|
||||||
{record?.items?.map((item: any) => (
|
{record?.items?.map((item: any) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
{item.name}:{item.quantity}
|
{item.name}:{item.quantity}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* 显示 related order */}
|
||||||
|
<ProDescriptions.Item
|
||||||
|
label="关联"
|
||||||
|
span={3}
|
||||||
|
render={(_, record) => {
|
||||||
|
return <RelatedOrders data={record?.related} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 订单内容 */}
|
||||||
<ProDescriptions.Item
|
<ProDescriptions.Item
|
||||||
label="订单内容"
|
label="订单内容"
|
||||||
span={3}
|
span={3}
|
||||||
|
|
@ -908,7 +926,7 @@ const Detail: React.FC<{
|
||||||
<ul>
|
<ul>
|
||||||
{record?.sales?.map((item: any) => (
|
{record?.sales?.map((item: any) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
{item.name}:{item.quantity}
|
{item.name}:{item.quantity}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -1021,7 +1039,7 @@ const Detail: React.FC<{
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>订单号: {v?.orderIds?.join(',')}</div>
|
<div>订单号: {v?.orderIds?.join(',')}</div>
|
||||||
{v?.items?.map((item) => (
|
{v?.items?.map((item) => (
|
||||||
<div>
|
<div>
|
||||||
{item.name}: {item.quantity}
|
{item.name}: {item.quantity}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { ActionType, ProColumns, ProTable, ProFormInstance } from '@ant-design/pro-components';
|
||||||
|
import { DrawerForm, ProFormText, ProFormSelect, ProFormSwitch } from '@ant-design/pro-components';
|
||||||
|
import { Button, message, Popconfirm, Space, Tag } from 'antd';
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
|
||||||
|
interface SiteItem {
|
||||||
|
id: number;
|
||||||
|
siteName: string;
|
||||||
|
apiUrl?: string;
|
||||||
|
type?: 'woocommerce' | 'shopyy';
|
||||||
|
skuPrefix?: string;
|
||||||
|
isDisabled: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建/更新表单的值类型,包含可选的密钥字段
|
||||||
|
interface SiteFormValues {
|
||||||
|
siteName: string;
|
||||||
|
apiUrl?: string;
|
||||||
|
type?: 'woocommerce' | 'shopyy';
|
||||||
|
isDisabled?: boolean;
|
||||||
|
consumerKey?: string; // WooCommerce REST API 的 consumer key
|
||||||
|
consumerSecret?: string; // WooCommerce REST API 的 consumer secret
|
||||||
|
skuPrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SiteList: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const formRef = useRef<ProFormInstance>();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<SiteItem | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
if (editing) {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
siteName: editing.siteName,
|
||||||
|
apiUrl: editing.apiUrl,
|
||||||
|
type: editing.type,
|
||||||
|
skuPrefix: editing.skuPrefix,
|
||||||
|
isDisabled: !!editing.isDisabled,
|
||||||
|
consumerKey: undefined,
|
||||||
|
consumerSecret: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
siteName: undefined,
|
||||||
|
apiUrl: undefined,
|
||||||
|
type: 'woocommerce',
|
||||||
|
skuPrefix: undefined,
|
||||||
|
isDisabled: false,
|
||||||
|
consumerKey: undefined,
|
||||||
|
consumerSecret: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open, editing]);
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: ProColumns<SiteItem>[] = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', width: 80, sorter: true, hideInSearch: true },
|
||||||
|
{ title: '站点名称', dataIndex: 'siteName', width: 220 },
|
||||||
|
{ title: 'API 地址', dataIndex: 'apiUrl', 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: 'isDisabled',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Tag color={row.isDisabled ? 'red' : 'green'}>
|
||||||
|
{row.isDisabled ? '已禁用' : '启用中'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'actions',
|
||||||
|
width: 240,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(row);
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title={row.isDisabled ? '启用站点' : '禁用站点'}
|
||||||
|
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
await request(`/site/disable/${row.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
data: { disabled: !row.isDisabled },
|
||||||
|
});
|
||||||
|
message.success('更新成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '更新失败');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button size="small" type="primary" danger={!row.isDisabled}>
|
||||||
|
{row.isDisabled ? '启用' : '禁用'}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表格数据请求
|
||||||
|
const tableRequest = async (params: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const { current = 1, pageSize = 10, siteName, type } = params;
|
||||||
|
const resp = await request('/site/list', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
keyword: siteName || undefined,
|
||||||
|
type: type || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
|
return {
|
||||||
|
data: (data?.items ?? []) as SiteItem[],
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '获取失败');
|
||||||
|
return { data: [], total: 0, success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交创建/更新逻辑;编辑时未填写密钥则不提交(保持原值)
|
||||||
|
const handleSubmit = async (values: SiteFormValues) => {
|
||||||
|
try {
|
||||||
|
if (editing) {
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
// 仅提交存在的字段,避免覆盖为 null/空
|
||||||
|
...(values.siteName ? { siteName: values.siteName } : {}),
|
||||||
|
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
|
||||||
|
...(values.type ? { type: values.type } : {}),
|
||||||
|
...(typeof values.isDisabled === 'boolean' ? { isDisabled: values.isDisabled } : {}),
|
||||||
|
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
|
||||||
|
};
|
||||||
|
// 仅当输入了新密钥时才提交,未输入则保持原本值
|
||||||
|
if (values.consumerKey && values.consumerKey.trim()) {
|
||||||
|
payload.consumerKey = values.consumerKey.trim();
|
||||||
|
}
|
||||||
|
if (values.consumerSecret && values.consumerSecret.trim()) {
|
||||||
|
payload.consumerSecret = values.consumerSecret.trim();
|
||||||
|
}
|
||||||
|
await request(`/site/update/${editing.id}`, { method: 'PUT', data: payload });
|
||||||
|
} else {
|
||||||
|
// 新增站点时要求填写 consumerKey 和 consumerSecret
|
||||||
|
if (!values.consumerKey || !values.consumerSecret) {
|
||||||
|
throw new Error('Consumer Key and Secret are required');
|
||||||
|
}
|
||||||
|
await request('/site/create', {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
siteName: values.siteName,
|
||||||
|
apiUrl: values.apiUrl,
|
||||||
|
type: values.type || 'woocommerce',
|
||||||
|
consumerKey: values.consumerKey,
|
||||||
|
consumerSecret: values.consumerSecret,
|
||||||
|
skuPrefix: values.skuPrefix,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.success('提交成功');
|
||||||
|
setOpen(false);
|
||||||
|
setEditing(null);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '提交失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProTable<SiteItem>
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
request={tableRequest}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
key="new"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(null);
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新增站点
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DrawerForm<SiteFormValues>
|
||||||
|
title={editing ? '编辑站点' : '新增站点'}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
formRef={formRef}
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
>
|
||||||
|
{/* 站点名称,必填 */}
|
||||||
|
<ProFormText name="siteName" label="站点名称" placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} />
|
||||||
|
{/* API 地址,可选 */}
|
||||||
|
<ProFormText name="apiUrl" label="API 地址" placeholder="例如:https://shop.example.com" />
|
||||||
|
{/* 平台类型选择 */}
|
||||||
|
<ProFormSelect
|
||||||
|
name="type"
|
||||||
|
label="平台"
|
||||||
|
options={[
|
||||||
|
{ label: 'WooCommerce', value: 'woocommerce' },
|
||||||
|
{ label: 'Shopyy', value: 'shopyy' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{/* 是否禁用 */}
|
||||||
|
<ProFormSwitch name="isDisabled" label="禁用" />
|
||||||
|
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder={editing ? '留空表示不修改' : '可选'} />
|
||||||
|
{/* WooCommerce REST consumer key;新增必填,编辑不填则保持原值 */}
|
||||||
|
<ProFormText name="consumerKey" label="Key" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: 'Key 为必填项' }]} />
|
||||||
|
{/* WooCommerce REST consumer secret;新增必填,编辑不填则保持原值 */}
|
||||||
|
<ProFormText name="consumerSecret" label="Secret" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: 'Secret 为必填项' }]} />
|
||||||
|
</DrawerForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SiteList;
|
||||||
|
|
@ -192,7 +192,7 @@ const ListPage: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
{record?.first_hot_purchase?.map((v) => (
|
{record?.first_hot_purchase?.map((v) => (
|
||||||
<div>
|
<div>
|
||||||
产品名称:{v.name} 用户数:{v.user_count}
|
产品名称:{v.name} 用户数:{v.user_count}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -206,7 +206,7 @@ const ListPage: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
{record?.second_hot_purchase?.map((v) => (
|
{record?.second_hot_purchase?.map((v) => (
|
||||||
<div>
|
<div>
|
||||||
产品名称:{v.name} 用户数:{v.user_count}
|
产品名称:{v.name} 用户数:{v.user_count}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,7 +220,7 @@ const ListPage: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
{record?.third_hot_purchase?.map((v) => (
|
{record?.third_hot_purchase?.map((v) => (
|
||||||
<div>
|
<div>
|
||||||
产品名称:{v.name} 用户数:{v.user_count}
|
产品名称:{v.name} 用户数:{v.user_count}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ const ListPage: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dateFormatter="number"
|
dateFormatter="number"
|
||||||
footer={() => `总计: ${total}`}
|
footer={() => `总计: ${total}`}
|
||||||
toolBarRender={() => [
|
toolBarRender={() => [
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ const CreateForm: React.FC<{
|
||||||
/>
|
/>
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
|
return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
<ProFormList<API.PurchaseOrderItem>
|
<ProFormList<API.PurchaseOrderItem>
|
||||||
|
|
@ -428,7 +428,7 @@ const UpdateForm: React.FC<{
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return (
|
return (
|
||||||
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
|
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ const CreateForm: React.FC<{
|
||||||
}}>引用</Button>} />
|
}}>引用</Button>} />
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0);
|
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0);
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
<ProFormList
|
<ProFormList
|
||||||
|
|
@ -465,7 +465,7 @@ const UpdateForm: React.FC<{
|
||||||
<ProFormTextArea name="note" label="备注" />
|
<ProFormTextArea name="note" label="备注" />
|
||||||
<ProFormDependency name={['items']}>
|
<ProFormDependency name={['items']}>
|
||||||
{({ items }) => {
|
{({ items }) => {
|
||||||
return '数量:' + items?.reduce?.((acc, cur) => acc + cur.quantity, 0);
|
return '数量:' + items?.reduce?.((acc, cur) => acc + cur.quantity, 0);
|
||||||
}}
|
}}
|
||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
<ProFormList
|
<ProFormList
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
DrawerForm,
|
||||||
|
PageContainer,
|
||||||
|
ProColumns,
|
||||||
|
ProFormSelect,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { App, Button, Tag, Drawer, List } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { request } from 'umi';
|
||||||
|
import {
|
||||||
|
subscriptioncontrollerList,
|
||||||
|
subscriptioncontrollerSync,
|
||||||
|
} from '@/servers/api/subscription';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅状态枚举(用于筛选与展示)
|
||||||
|
* 保持与后端同步的原始状态值
|
||||||
|
*/
|
||||||
|
const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
|
||||||
|
active: { text: '激活' },
|
||||||
|
cancelled: { text: '已取消' },
|
||||||
|
expired: { text: '已过期' },
|
||||||
|
pending: { text: '待处理' },
|
||||||
|
'on-hold': { text: '暂停' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅列表页:展示、筛选、触发订阅同步
|
||||||
|
*/
|
||||||
|
const ListPage: React.FC = () => {
|
||||||
|
// 表格操作引用:用于在同步后触发表格刷新
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
// 关联订单抽屉状态
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [drawerTitle, setDrawerTitle] = useState('详情');
|
||||||
|
const [relatedOrders, setRelatedOrders] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 表格列定义(尽量与项目风格保持一致)
|
||||||
|
const columns: ProColumns<API.Subscription>[] = [
|
||||||
|
{
|
||||||
|
title: '站点',
|
||||||
|
dataIndex: 'siteId',
|
||||||
|
valueType: 'select',
|
||||||
|
// 动态加载站点选项
|
||||||
|
request: async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.siteName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
render: (_, row) => row?.siteId ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订阅ID',
|
||||||
|
dataIndex: 'externalSubscriptionId',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '父订单号',
|
||||||
|
dataIndex: 'parent_id',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (row?.parent_id ? <Tag>{row.parent_id}</Tag> : '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: SUBSCRIPTION_STATUS_ENUM,
|
||||||
|
// 以 Tag 形式展示,更易辨识
|
||||||
|
render: (_, row) =>
|
||||||
|
row?.status ? <Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag> : '-',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '客户邮箱',
|
||||||
|
dataIndex: 'customer_email',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
dataIndex: 'total',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '币种',
|
||||||
|
dataIndex: 'currency',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开始时间',
|
||||||
|
dataIndex: 'start_date',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '下次支付',
|
||||||
|
dataIndex: 'next_payment_date',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结束时间',
|
||||||
|
dataIndex: 'end_date',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
valueType: 'dateTime',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'actions',
|
||||||
|
hideInSearch: true,
|
||||||
|
width: 120,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const parentNumber = String(row?.parent_id || '');
|
||||||
|
if (!parentNumber) {
|
||||||
|
message.warning('该订阅缺少父订单号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 通过父订单号查询关联订单(模糊匹配)
|
||||||
|
const resp = await request('/order/getOrderByNumber', {
|
||||||
|
method: 'POST',
|
||||||
|
data: { number: parentNumber },
|
||||||
|
});
|
||||||
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
|
// 仅保留与父订单号完全一致的订单(避免模糊匹配误入)
|
||||||
|
const candidates: any[] = (Array.isArray(data) ? data : []).filter(
|
||||||
|
(c: any) => String(c?.externalOrderId) === parentNumber
|
||||||
|
);
|
||||||
|
// 拉取详情,补充状态、金额、时间
|
||||||
|
const details = [] as any[];
|
||||||
|
for (const c of candidates) {
|
||||||
|
const d = await request(`/order/${c.id}`, { method: 'GET' });
|
||||||
|
if ((d as any)?.success) {
|
||||||
|
const od = (d as any)?.data || {};
|
||||||
|
details.push({
|
||||||
|
id: c.id,
|
||||||
|
externalOrderId: c.externalOrderId,
|
||||||
|
siteName: c.siteName,
|
||||||
|
status: od?.status,
|
||||||
|
total: od?.total,
|
||||||
|
currency_symbol: od?.currency_symbol,
|
||||||
|
date_created: od?.date_created,
|
||||||
|
relationship: 'Parent Order',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
id: c.id,
|
||||||
|
externalOrderId: c.externalOrderId,
|
||||||
|
siteName: c.siteName,
|
||||||
|
relationship: 'Parent Order',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRelatedOrders(details);
|
||||||
|
setDrawerTitle(`详情`);
|
||||||
|
setDrawerOpen(true);
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '获取失败');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer header={{ title: '订阅列表' }}>
|
||||||
|
<ProTable<API.Subscription>
|
||||||
|
headerTitle="查询表格"
|
||||||
|
rowKey="id"
|
||||||
|
actionRef={actionRef}
|
||||||
|
/**
|
||||||
|
* 列表数据请求;保持与后端分页参数一致
|
||||||
|
* 兼容后端 data.items 或 data.list 返回字段
|
||||||
|
*/
|
||||||
|
request={async (params) => {
|
||||||
|
const { data, success } = await subscriptioncontrollerList(params);
|
||||||
|
return {
|
||||||
|
total: data?.total || 0,
|
||||||
|
data: data?.items || data?.list || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
// 工具栏:订阅同步入口
|
||||||
|
toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]}
|
||||||
|
/>
|
||||||
|
{/* 关联订单抽屉:展示订单号、关系、时间、状态与金额 */}
|
||||||
|
<Drawer
|
||||||
|
open={drawerOpen}
|
||||||
|
title={drawerTitle}
|
||||||
|
width={720}
|
||||||
|
onClose={() => setDrawerOpen(false)}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
header={<div>关联订单</div>}
|
||||||
|
dataSource={relatedOrders}
|
||||||
|
renderItem={(item: any) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={`#${item?.externalOrderId || '-'}`}
|
||||||
|
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
||||||
|
<span>{item?.date_created ? dayjs(item.date_created).format('YYYY-MM-DD HH:mm') : '-'}</span>
|
||||||
|
<Tag>{item?.status || '-'}</Tag>
|
||||||
|
<span>
|
||||||
|
{item?.currency_symbol || ''}
|
||||||
|
{typeof item?.total === 'number' ? item.total.toFixed(2) : item?.total ?? '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步订阅抽屉表单:选择站点后触发同步
|
||||||
|
*/
|
||||||
|
const SyncForm: React.FC<{
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>;
|
||||||
|
}> = ({ tableRef }) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DrawerForm<API.subscriptioncontrollerSyncParams>
|
||||||
|
title="同步订阅"
|
||||||
|
trigger={
|
||||||
|
<Button key="syncSite" type="primary">
|
||||||
|
同步订阅
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
drawerProps={{ destroyOnHidden: true }}
|
||||||
|
/**
|
||||||
|
* 提交逻辑:
|
||||||
|
* 1. 必填校验由 ProForm + rules 保证
|
||||||
|
* 2. 调用同步接口,失败时友好提示
|
||||||
|
* 3. 成功后刷新列表
|
||||||
|
*/
|
||||||
|
onFinish={async (values) => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } = await subscriptioncontrollerSync(values);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
message.success('同步成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '同步失败');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormSelect
|
||||||
|
name="siteId"
|
||||||
|
width="lg"
|
||||||
|
label="站点"
|
||||||
|
placeholder="请选择站点"
|
||||||
|
// 动态加载站点选项
|
||||||
|
request={async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({
|
||||||
|
label: item.siteName,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
rules={[{ required: true, message: '请选择站点' }]}
|
||||||
|
/>
|
||||||
|
</DrawerForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListPage;
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
} from 'antd';
|
||||||
|
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
|
||||||
|
import { CopyOutlined, DeleteFilled } from '@ant-design/icons';
|
||||||
|
|
||||||
|
// 服务器 API 引用(保持与原 index.tsx 一致)
|
||||||
|
import {
|
||||||
|
ordercontrollerChangestatus,
|
||||||
|
ordercontrollerGetorderdetail,
|
||||||
|
ordercontrollerSyncorderbyid,
|
||||||
|
} from '@/servers/api/order';
|
||||||
|
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
|
// 工具与子组件
|
||||||
|
import { formatShipmentState, formatSource } from '@/utils/format';
|
||||||
|
import RelatedOrders from './RelatedOrders';
|
||||||
|
import { ORDER_STATUS_ENUM } from '@/constants';
|
||||||
|
|
||||||
|
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
||||||
|
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
||||||
|
// 当前按你的要求仅抽离详情 Drawer
|
||||||
|
|
||||||
|
type OrderRecord = API.Order;
|
||||||
|
|
||||||
|
interface OrderDetailDrawerProps {
|
||||||
|
tableRef: React.MutableRefObject<ActionType | undefined>; // 列表刷新引用
|
||||||
|
orderId: number; // 订单主键 ID
|
||||||
|
record: OrderRecord; // 订单行记录
|
||||||
|
open: boolean; // 是否打开抽屉
|
||||||
|
onClose: () => void; // 关闭抽屉回调
|
||||||
|
setActiveLine: (id: number) => void; // 高亮当前行
|
||||||
|
OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入)
|
||||||
|
SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
||||||
|
tableRef,
|
||||||
|
orderId,
|
||||||
|
record,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
setActiveLine,
|
||||||
|
OrderNoteComponent,
|
||||||
|
SalesChangeComponent,
|
||||||
|
}) => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const ref = useRef<ActionType>();
|
||||||
|
|
||||||
|
// 加载详情数据(与 index.tsx 中完全保持一致)
|
||||||
|
const initRequest = async () => {
|
||||||
|
const { data, success }: API.OrderDetailRes = await ordercontrollerGetorderdetail({ orderId });
|
||||||
|
if (!success || !data) return { data: {} } as any;
|
||||||
|
data.sales = data.sales?.reduce((acc: API.OrderSale[], cur: API.OrderSale) => {
|
||||||
|
const idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
||||||
|
if (idx === -1) acc.push(cur);
|
||||||
|
else acc[idx].quantity += cur.quantity;
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
return { data } as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && record?.id) {
|
||||||
|
setActiveLine(record.id as number);
|
||||||
|
}
|
||||||
|
}, [open, record?.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="订单详情"
|
||||||
|
open={open}
|
||||||
|
destroyOnHidden
|
||||||
|
size="large"
|
||||||
|
onClose={onClose}
|
||||||
|
footer={[
|
||||||
|
// 备注组件(外部传入以避免循环依赖)
|
||||||
|
<OrderNoteComponent key="order-note" id={orderId} descRef={ref} />,
|
||||||
|
...(['after_sale_pending', 'pending_reshipment'].includes(
|
||||||
|
record.orderStatus,
|
||||||
|
)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
<Divider key="divider-sync" type="vertical" />,
|
||||||
|
<Button
|
||||||
|
key="btn-sync"
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerSyncorderbyid({
|
||||||
|
siteId: record.siteId as string,
|
||||||
|
orderId: record.externalOrderId as string,
|
||||||
|
});
|
||||||
|
if (!success) throw new Error(errMsg);
|
||||||
|
message.success('同步成功');
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error?.message || '同步失败');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
同步订单
|
||||||
|
</Button>,
|
||||||
|
]),
|
||||||
|
...([
|
||||||
|
'processing',
|
||||||
|
'pending_reshipment',
|
||||||
|
'completed',
|
||||||
|
'pending_refund',
|
||||||
|
].includes(record.orderStatus)
|
||||||
|
? [
|
||||||
|
<Divider key="divider-after-sale" type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
key="btn-after-sale"
|
||||||
|
title="转至售后"
|
||||||
|
description="确认转至售后?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
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 key="divider-cancel" type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
key="btn-cancel"
|
||||||
|
title="转至取消"
|
||||||
|
description="确认转至取消?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{ id: record.id },
|
||||||
|
{ status: 'cancelled' },
|
||||||
|
);
|
||||||
|
if (!success) throw new Error(errMsg);
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至取消
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider key="divider-refund" type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
key="btn-refund"
|
||||||
|
title="转至退款"
|
||||||
|
description="确认转至退款?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{ id: record.id },
|
||||||
|
{ status: 'refund_requested' },
|
||||||
|
);
|
||||||
|
if (!success) throw new Error(errMsg);
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至退款
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
<Divider key="divider-completed" type="vertical" />,
|
||||||
|
<Popconfirm
|
||||||
|
key="btn-completed"
|
||||||
|
title="转至完成"
|
||||||
|
description="确认转至完成?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const { success, message: errMsg } =
|
||||||
|
await ordercontrollerChangestatus(
|
||||||
|
{ id: record.id },
|
||||||
|
{ status: 'completed' },
|
||||||
|
);
|
||||||
|
if (!success) throw new Error(errMsg);
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" ghost>
|
||||||
|
转至完成
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ProDescriptions labelStyle={{ width: '100px' }} actionRef={ref} request={initRequest}>
|
||||||
|
<ProDescriptions.Item label="站点" dataIndex="siteId" valueType="select" request={async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return data.map((item) => ({ label: item.siteName, value: item.id }));
|
||||||
|
}} />
|
||||||
|
<ProDescriptions.Item label="订单日期" dataIndex="date_created" valueType="dateTime" />
|
||||||
|
<ProDescriptions.Item label="订单状态" dataIndex="orderStatus" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} />
|
||||||
|
<ProDescriptions.Item label="金额" dataIndex="total" />
|
||||||
|
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
|
||||||
|
<ProDescriptions.Item label="联系电话" span={3} render={(_, r: any) => (
|
||||||
|
<div><span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div>
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
|
||||||
|
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
|
||||||
|
<ProDescriptions.Item label="设备" dataIndex="device_type" />
|
||||||
|
<ProDescriptions.Item label="来源" render={(_, r: any) => formatSource(r.source_type, r.utm_source)} />
|
||||||
|
<ProDescriptions.Item label="原订单状态" dataIndex="status" valueType="select" valueEnum={ORDER_STATUS_ENUM as any} />
|
||||||
|
<ProDescriptions.Item label="支付链接" dataIndex="payment_url" span={3} copyable />
|
||||||
|
<ProDescriptions.Item label="客户备注" dataIndex="customer_note" span={3} />
|
||||||
|
<ProDescriptions.Item label="发货信息" span={3} render={(_, r: any) => (
|
||||||
|
<div>
|
||||||
|
<div>company:<span>{r?.shipping?.company || r?.billing?.company || '-'}</span></div>
|
||||||
|
<div>first_name:<span>{r?.shipping?.first_name || r?.billing?.first_name || '-'}</span></div>
|
||||||
|
<div>last_name:<span>{r?.shipping?.last_name || r?.billing?.last_name || '-'}</span></div>
|
||||||
|
<div>country:<span>{r?.shipping?.country || r?.billing?.country || '-'}</span></div>
|
||||||
|
<div>state:<span>{r?.shipping?.state || r?.billing?.state || '-'}</span></div>
|
||||||
|
<div>city:<span>{r?.shipping?.city || r?.billing?.city || '-'}</span></div>
|
||||||
|
<div>postcode:<span>{r?.shipping?.postcode || r?.billing?.postcode || '-'}</span></div>
|
||||||
|
<div>phone:<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span></div>
|
||||||
|
<div>address_1:<span>{r?.shipping?.address_1 || r?.billing?.address_1 || '-'}</span></div>
|
||||||
|
</div>
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="原始订单" span={3} render={(_, r: any) => (
|
||||||
|
<ul>
|
||||||
|
{(r?.items || []).map((item: any) => (
|
||||||
|
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="关联" span={3} render={(_, r: any) => (
|
||||||
|
<RelatedOrders data={r?.related} />
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="订单内容" span={3} render={(_, r: any) => (
|
||||||
|
<ul>
|
||||||
|
{(r?.sales || []).map((item: any) => (
|
||||||
|
<li key={item.id}>{item.name}:{item.quantity}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="换货" span={3} render={(_, r: any) => (
|
||||||
|
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
||||||
|
)} />
|
||||||
|
<ProDescriptions.Item label="备注" span={3} render={(_, r: any) => {
|
||||||
|
if (!r.notes || r.notes.length === 0) return (<Empty description="暂无备注" />);
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
{r.notes.map((note: any) => (
|
||||||
|
<div style={{ marginBottom: 10 }} key={note.id}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span>{note.username}</span>
|
||||||
|
<span>{note.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
<div>{note.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}} />
|
||||||
|
<ProDescriptions.Item label="物流信息" span={3} render={(_, r: any) => {
|
||||||
|
if (!r.shipment || r.shipment.length === 0) return (<Empty description="暂无物流信息" />);
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
|
{r.shipment.map((v: any) => (
|
||||||
|
<Card key={v.id} style={{ marginBottom: '10px' }} extra={formatShipmentState(v.state)} title={<>
|
||||||
|
{v.tracking_provider}
|
||||||
|
{v.primary_tracking_number}
|
||||||
|
<CopyOutlined onClick={async () => {
|
||||||
|
try { await navigator.clipboard.writeText(v.tracking_url); message.success('复制成功!'); }
|
||||||
|
catch { message.error('复制失败!'); }
|
||||||
|
}} />
|
||||||
|
</>}
|
||||||
|
actions={ (v.state === 'waiting-for-scheduling' || v.state === 'waiting-for-transit') ? [
|
||||||
|
<Popconfirm key="action-cancel" title="取消运单" description="确认取消运单?" onConfirm={async () => {
|
||||||
|
try { const { success, message: errMsg } = await logisticscontrollerDelshipment({ id: v.id }); if (!success) throw new Error(errMsg); tableRef.current?.reload(); ref.current?.reload?.(); }
|
||||||
|
catch (error: any) { message.error(error.message); }
|
||||||
|
}}>
|
||||||
|
<DeleteFilled />取消运单
|
||||||
|
</Popconfirm>
|
||||||
|
] : [] }
|
||||||
|
>
|
||||||
|
<div>订单号: {Array.isArray(v?.orderIds) ? v.orderIds.join(',') : '-'}</div>
|
||||||
|
{Array.isArray(v?.items) && v.items.map((item: any) => (
|
||||||
|
<div key={item.id}>{item.name}: {item.quantity}</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}} />
|
||||||
|
</ProDescriptions>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderDetailDrawer;
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Empty, Tag } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RelatedOrders 表格组件
|
||||||
|
* 用于展示订单详情中的关联数据(订阅/订单),按统一表格样式渲染
|
||||||
|
* 本组件将订阅与订单统一归一化为五列展示,便于快速浏览
|
||||||
|
*/
|
||||||
|
const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
|
||||||
|
const rows = (Array.isArray(data) ? data : []).map((it: any) => {
|
||||||
|
const isSubscription = !!it?.externalSubscriptionId || !!it?.billing_period || !!it?.line_items;
|
||||||
|
const number = isSubscription ? `#${it?.externalSubscriptionId || it?.id}` : `#${it?.externalOrderId || it?.id}`;
|
||||||
|
const relationship = isSubscription ? 'Subscription' : 'Order';
|
||||||
|
const dateRaw = it?.start_date || it?.date_created || it?.createdAt || it?.updatedAt;
|
||||||
|
const dateText = dateRaw ? dayjs(dateRaw).fromNow() : '-';
|
||||||
|
const status = (isSubscription ? it?.status : it?.orderStatus) || '-';
|
||||||
|
const statusLower = String(status).toLowerCase();
|
||||||
|
const color = statusLower === 'active' ? 'green' : statusLower === 'cancelled' ? 'red' : 'default';
|
||||||
|
const totalNum = Number(it?.total || 0);
|
||||||
|
const totalText = isSubscription ? `$${totalNum.toFixed(2)} / ${it?.billing_period || 'period'}` : `$${totalNum.toFixed(2)}`;
|
||||||
|
return { key: `${isSubscription ? 'sub' : 'order'}-${it?.id}`, number, relationship, dateText, status, color, totalText };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length === 0) return <Empty description="暂无关联" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
{/* 表头(英文文案,符合国际化默认英文的要求) */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '8px 0', fontWeight: 600 }}>
|
||||||
|
<div>订单编号</div>
|
||||||
|
<div>关系</div>
|
||||||
|
<div>日期</div>
|
||||||
|
<div>状态</div>
|
||||||
|
<div>金额</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{rows.map((r) => (
|
||||||
|
<div key={r.key} style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '6px 0', borderTop: '1px solid #f0f0f0' }}>
|
||||||
|
<div><a>{r.number}</a></div>
|
||||||
|
<div>{r.relationship}</div>
|
||||||
|
<div style={{ color: '#1677ff' }}>{r.dateText}</div>
|
||||||
|
<div><Tag color={r.color}>{r.status}</Tag></div>
|
||||||
|
<div>{r.totalText}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelatedOrders;
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
|
||||||
|
import { ProTable } from '@ant-design/pro-components';
|
||||||
|
import { App, Tag, Button } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { ordercontrollerGetorders } from '@/servers/api/order';
|
||||||
|
import OrderDetailDrawer from './OrderDetailDrawer';
|
||||||
|
import { sitecontrollerAll } from '@/servers/api/site';
|
||||||
|
|
||||||
|
interface OrderItemRow {
|
||||||
|
id: number;
|
||||||
|
externalOrderId: string;
|
||||||
|
siteId: string;
|
||||||
|
date_created: string;
|
||||||
|
customer_email: string;
|
||||||
|
payment_method: string;
|
||||||
|
total: number;
|
||||||
|
orderStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrdersPage: React.FC = () => {
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
// 抽屉状态:改为复用订单详情抽屉组件
|
||||||
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
|
const [detailRecord, setDetailRecord] = useState<any | null>(null);
|
||||||
|
const [detailOrderId, setDetailOrderId] = useState<number | null>(null);
|
||||||
|
const Noop: React.FC<any> = () => null;
|
||||||
|
|
||||||
|
const columns: ProColumns<OrderItemRow>[] = [
|
||||||
|
{
|
||||||
|
title: '订单ID',
|
||||||
|
dataIndex: 'externalOrderId',
|
||||||
|
width: 120,
|
||||||
|
ellipsis: true,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '站点',
|
||||||
|
dataIndex: 'siteId',
|
||||||
|
width: 120,
|
||||||
|
valueType: 'select',
|
||||||
|
request: async () => {
|
||||||
|
const { data = [] } = await sitecontrollerAll();
|
||||||
|
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '下单时间',
|
||||||
|
dataIndex: 'date_created',
|
||||||
|
width: 180,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, row) => (row?.date_created ? dayjs(row.date_created).format('YYYY-MM-DD HH:mm') : '-'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '邮箱',
|
||||||
|
dataIndex: 'customer_email',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '支付方式',
|
||||||
|
dataIndex: 'payment_method',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '金额',
|
||||||
|
dataIndex: 'total',
|
||||||
|
width: 100,
|
||||||
|
hideInSearch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ERP状态',
|
||||||
|
dataIndex: 'orderStatus',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, row) => <Tag>{row.orderStatus}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '订阅关联',
|
||||||
|
dataIndex: 'subscription_related',
|
||||||
|
width: 120,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setDetailRecord(row as any);
|
||||||
|
setDetailOrderId(row.id as number);
|
||||||
|
setDetailOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间范围',
|
||||||
|
dataIndex: 'dateRange',
|
||||||
|
valueType: 'dateRange',
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品关键字',
|
||||||
|
dataIndex: 'keyword',
|
||||||
|
hideInTable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
|
||||||
|
try {
|
||||||
|
const { current = 1, pageSize = 10, siteId, keyword, customer_email, payment_method } = params as any;
|
||||||
|
const [startDate, endDate] = (params as any).dateRange || [];
|
||||||
|
const resp = await ordercontrollerGetorders({
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
siteId,
|
||||||
|
keyword,
|
||||||
|
customer_email,
|
||||||
|
payment_method,
|
||||||
|
isSubscriptionOnly: true as any,
|
||||||
|
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
|
||||||
|
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
|
||||||
|
} as any);
|
||||||
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
if (!success) throw new Error(errMsg || '获取失败');
|
||||||
|
return {
|
||||||
|
data: (data?.items ?? []) as OrderItemRow[],
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '获取失败');
|
||||||
|
return { data: [], total: 0, success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title='订阅订单'>
|
||||||
|
<ProTable<OrderItemRow>
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey='id'
|
||||||
|
columns={columns}
|
||||||
|
request={request}
|
||||||
|
pagination={{ showSizeChanger: true }}
|
||||||
|
search={{
|
||||||
|
labelWidth: 90,
|
||||||
|
span: 6,
|
||||||
|
}}
|
||||||
|
toolBarRender={false}
|
||||||
|
/>
|
||||||
|
{/* 订阅关联:直接使用订单详情抽屉组件 */}
|
||||||
|
{detailRecord && detailOrderId !== null && (
|
||||||
|
<OrderDetailDrawer
|
||||||
|
open={detailOpen}
|
||||||
|
onClose={() => setDetailOpen(false)}
|
||||||
|
tableRef={actionRef}
|
||||||
|
orderId={detailOrderId as number}
|
||||||
|
record={detailRecord as any}
|
||||||
|
setActiveLine={() => {}}
|
||||||
|
OrderNoteComponent={Noop}
|
||||||
|
SalesChangeComponent={Noop}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrdersPage;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// API 更新时间:
|
// API 更新时间:
|
||||||
// API 唯一标识:
|
// API 唯一标识:
|
||||||
import * as customer from './customer';
|
import * as customer from './customer';
|
||||||
import * as logistics from './logistics';
|
import * as logistics from './logistics';
|
||||||
import * as order from './order';
|
import * as order from './order';
|
||||||
|
|
@ -9,6 +9,7 @@ import * as product from './product';
|
||||||
import * as site from './site';
|
import * as site from './site';
|
||||||
import * as statistics from './statistics';
|
import * as statistics from './statistics';
|
||||||
import * as stock from './stock';
|
import * as stock from './stock';
|
||||||
|
import * as subscription from './subscription';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
import * as webhook from './webhook';
|
import * as webhook from './webhook';
|
||||||
import * as wpProduct from './wpProduct';
|
import * as wpProduct from './wpProduct';
|
||||||
|
|
@ -20,6 +21,7 @@ export default {
|
||||||
site,
|
site,
|
||||||
statistics,
|
statistics,
|
||||||
stock,
|
stock,
|
||||||
|
subscription,
|
||||||
user,
|
user,
|
||||||
webhook,
|
webhook,
|
||||||
wpProduct,
|
wpProduct,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// @ts-ignore
|
||||||
|
/* eslint-disable */
|
||||||
|
import { request } from 'umi';
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /subscription/list */
|
||||||
|
export async function subscriptioncontrollerList(
|
||||||
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
|
params: API.subscriptioncontrollerListParams,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.SubscriptionListRes>('/subscription/list', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 POST /subscription/sync/${param0} */
|
||||||
|
export async function subscriptioncontrollerSync(
|
||||||
|
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||||
|
params: API.subscriptioncontrollerSyncParams,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { siteId: param0, ...queryParams } = params;
|
||||||
|
return request<API.BooleanRes>(`/subscription/sync/${param0}`, {
|
||||||
|
method: 'POST',
|
||||||
|
params: { ...queryParams },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -334,9 +334,9 @@ declare namespace API {
|
||||||
| 'after_sale_pending'
|
| 'after_sale_pending'
|
||||||
| 'pending_reshipment'
|
| 'pending_reshipment'
|
||||||
| 'pending_refund'
|
| 'pending_refund'
|
||||||
| 'refund_requested'
|
| 'return-requested'
|
||||||
| 'refund_approved'
|
| 'return-approved'
|
||||||
| 'refund_cancelled';
|
| 'return-cancelled';
|
||||||
payment_method?: string;
|
payment_method?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -863,9 +863,9 @@ declare namespace API {
|
||||||
| 'after_sale_pending'
|
| 'after_sale_pending'
|
||||||
| 'pending_reshipment'
|
| 'pending_reshipment'
|
||||||
| 'pending_refund'
|
| 'pending_refund'
|
||||||
| 'refund_requested'
|
| 'return-requested'
|
||||||
| 'refund_approved'
|
| 'return-approved'
|
||||||
| 'refund_cancelled';
|
| 'return-cancelled';
|
||||||
payment_method?: string;
|
payment_method?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -948,6 +948,27 @@ declare namespace API {
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QuerySubscriptionDTO = {
|
||||||
|
/** 页码 */
|
||||||
|
current?: number;
|
||||||
|
/** 每页大小 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 站点ID */
|
||||||
|
siteId?: string;
|
||||||
|
/** 订阅状态 */
|
||||||
|
status?:
|
||||||
|
| 'active'
|
||||||
|
| 'pending'
|
||||||
|
| 'on-hold'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'expired'
|
||||||
|
| 'pending-cancel';
|
||||||
|
/** 客户邮箱 */
|
||||||
|
customer_email?: string;
|
||||||
|
/** 关键字(订阅ID、邮箱等) */
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type QueryWpProductDTO = {
|
type QueryWpProductDTO = {
|
||||||
/** 页码 */
|
/** 页码 */
|
||||||
current?: number;
|
current?: number;
|
||||||
|
|
@ -1309,6 +1330,82 @@ declare namespace API {
|
||||||
items?: StockRecordDTO[];
|
items?: StockRecordDTO[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Subscription = {
|
||||||
|
id?: number;
|
||||||
|
/** 来源站点唯一标识 */
|
||||||
|
siteId?: string;
|
||||||
|
/** WooCommerce 订阅 ID */
|
||||||
|
externalSubscriptionId?: string;
|
||||||
|
status?: any;
|
||||||
|
currency?: string;
|
||||||
|
total?: number;
|
||||||
|
/** 计费周期 e.g. day/week/month/year */
|
||||||
|
billing_period?: string;
|
||||||
|
/** 计费周期间隔 e.g. 1/3/12 */
|
||||||
|
billing_interval?: number;
|
||||||
|
customer_id?: number;
|
||||||
|
customer_email?: string;
|
||||||
|
/** 父订单/父订阅ID(如有) */
|
||||||
|
parent_id?: number;
|
||||||
|
start_date?: string;
|
||||||
|
trial_end?: string;
|
||||||
|
next_payment_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
line_items?: any;
|
||||||
|
meta_data?: any;
|
||||||
|
/** 创建时间 */
|
||||||
|
createdAt: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type subscriptioncontrollerListParams = {
|
||||||
|
/** 页码 */
|
||||||
|
current?: number;
|
||||||
|
/** 每页大小 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 站点ID */
|
||||||
|
siteId?: string;
|
||||||
|
/** 订阅状态 */
|
||||||
|
status?:
|
||||||
|
| 'active'
|
||||||
|
| 'pending'
|
||||||
|
| 'on-hold'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'expired'
|
||||||
|
| 'pending-cancel';
|
||||||
|
/** 客户邮箱 */
|
||||||
|
customer_email?: string;
|
||||||
|
/** 关键字(订阅ID、邮箱等) */
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type subscriptioncontrollerSyncParams = {
|
||||||
|
siteId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionListRes = {
|
||||||
|
/** 状态码 */
|
||||||
|
code?: number;
|
||||||
|
/** 是否成功 */
|
||||||
|
success?: boolean;
|
||||||
|
/** 消息内容 */
|
||||||
|
message?: string;
|
||||||
|
/** 响应数据 */
|
||||||
|
data?: SubscriptionPaginatedResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionPaginatedResponse = {
|
||||||
|
/** 当前页码 */
|
||||||
|
page?: number;
|
||||||
|
/** 每页大小 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 总记录数 */
|
||||||
|
total?: number;
|
||||||
|
/** 数据列表 */
|
||||||
|
items?: Subscription[];
|
||||||
|
};
|
||||||
|
|
||||||
type Surcharges = {
|
type Surcharges = {
|
||||||
type?: string;
|
type?: string;
|
||||||
amount?: Money;
|
amount?: Money;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ declare namespace BaseType {
|
||||||
type EnumTransformOptions {
|
type EnumTransformOptions {
|
||||||
value: string; // 用于作为 value 的字段名
|
value: string; // 用于作为 value 的字段名
|
||||||
label: string; // 用于作为 text 的字段名
|
label: string; // 用于作为 text 的字段名
|
||||||
status?: string | undefined; // 可选:用于设置状态的字段名
|
status?: string | undefined; // 可选:用于设置状态的字段名
|
||||||
color?: string | undefined; // 可选:用于设置颜色的字段名
|
color?: string | undefined; // 可选:用于设置颜色的字段名
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue