forked from yoone/WEB
1
0
Fork 0

refactor: 统一代码格式并优化多个页面组件

style: 修复文件末尾缺少换行符的问题
style: 调整CSS缩进和对象属性逗号格式
style: 统一React导入顺序和组件命名

fix: 修正统计页面图表数据展示格式
fix: 修复订单页面日期显示问题

feat(statistics): 添加按周统计订单功能
feat(login): 增加设备指纹验证功能

chore: 更新依赖包和配置项
This commit is contained in:
zhuotianyuan 2025-12-22 11:03:34 +08:00
parent bd3d9a00c5
commit 885427e4b6
28 changed files with 1346 additions and 908 deletions

View File

@ -1,10 +1,10 @@
import { defineConfig } from '@umijs/max';
import { codeInspectorPlugin } from 'code-inspector-plugin';
const isDev = process.env.NODE_ENV === 'development';
const UMI_APP_API_URL = isDev
? 'http://localhost:7001'
: 'https://api.yoone.ca';
import { codeInspectorPlugin } from 'code-inspector-plugin';
export default defineConfig({
hash: true,
@ -23,7 +23,7 @@ export default defineConfig({
config.plugin('code-inspector-plugin').use(
codeInspectorPlugin({
bundler: 'webpack',
})
}),
);
},
routes: [

View File

@ -1,2 +1 @@
# WEB

View File

@ -1,15 +1,39 @@
export default (initialState: any) => {
const isSuper = initialState?.user?.isSuper ?? false;
const isAdmin = initialState?.user?.Admin ?? false;
const canSeeOrganiza = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('organiza') ?? false);
const canSeeProduct = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('product') ?? false);
const canSeeStock = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('stock') ?? false);
const canSeeOrder = (isSuper || isAdmin) ||
((initialState?.user?.permissions?.includes('order') ?? false) || (initialState?.user?.permissions?.includes('order-10-days') ?? false));
const canSeeCustomer = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('customer') ?? false);
const canSeeLogistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('logistics') ?? false);
const canSeeStatistics = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite = (isSuper || isAdmin) || (initialState?.user?.permissions?.includes('site') ?? false);
const canSeeOrganiza =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('organiza') ?? false);
const canSeeProduct =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('product') ?? false);
const canSeeStock =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('stock') ?? false);
const canSeeOrder =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('order') ?? false) ||
(initialState?.user?.permissions?.includes('order-10-days') ?? false);
const canSeeCustomer =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('customer') ?? false);
const canSeeLogistics =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('logistics') ?? false);
const canSeeStatistics =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('statistics') ?? false);
const canSeeSite =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('site') ?? false);
return {
canSeeOrganiza,

View File

@ -116,5 +116,5 @@ export const ORDER_STATUS_ENUM: ProSchemaValueEnumObj = {
refund_cancelled: {
text: '已取消退款',
status: 'refund_cancelled',
}
},
};

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { useEffect, useState } from 'react';
/**
* Hook: 获取设备指纹(visitorId)

View File

@ -41,7 +41,7 @@ const ListPage: React.FC = () => {
dataIndex: 'customerId',
render: (_, record) => {
if (!record.customerId) return '-';
return String(record.customerId).padStart(6,0)
return String(record.customerId).padStart(6, 0);
},
sorter: true,
},
@ -95,21 +95,25 @@ const ListPage: React.FC = () => {
title: '等级',
hideInSearch: true,
render: (_, record) => {
if(!record.yoone_orders || !record.yoone_total) return '-'
if(Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0 ) return 'B'
return '-'
}
if (!record.yoone_orders || !record.yoone_total) return '-';
if (Number(record.yoone_orders) === 1 && Number(record.yoone_total) > 0)
return 'B';
return '-';
},
},
{
title: '评星',
dataIndex: 'rate',
width: 200,
render: (_, record) => {
return <Rate onChange={async(val)=>{
return (
<Rate
onChange={async (val) => {
try {
const { success, message: msg } = await customercontrollerSetrate({
const { success, message: msg } =
await customercontrollerSetrate({
id: record.customerId,
rate: val
rate: val,
});
if (success) {
message.success(msg);
@ -117,9 +121,11 @@ const ListPage: React.FC = () => {
}
} catch (e) {
message.error(e.message);
}
}} value={record.rate} />
}}
value={record.rate}
/>
);
},
},
{

View File

@ -1,3 +1,4 @@
import { useDeviceFingerprint } from '@/hooks/useDeviceFingerprint';
import { usercontrollerGetuser, usercontrollerLogin } from '@/servers/api/user';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import {
@ -7,7 +8,6 @@ import {
} from '@ant-design/pro-components';
import { history, useModel } from '@umijs/max';
import { App, theme } from 'antd';
import {useDeviceFingerprint} from '@/hooks/useDeviceFingerprint';
import { useState } from 'react';
const Page = () => {
@ -15,28 +15,32 @@ const Page = () => {
const { token } = theme.useToken();
const { message } = App.useApp();
const deviceId = useDeviceFingerprint();
const [ isAuth, setIsAuth ] = useState(false)
const [isAuth, setIsAuth] = useState(false);
console.log(deviceId);
const onFinish = async (values: { username: string; password: string }) => {
try {
const { data, success, code, message: msg } = await usercontrollerLogin({...values, deviceId});
const {
data,
success,
code,
message: msg,
} = await usercontrollerLogin({ ...values, deviceId });
if (success) {
message.success('登录成功');
localStorage.setItem('token', data?.token as string);
const { data: user } = await usercontrollerGetuser();
setInitialState({ user });
history.push('/');
return
return;
}
if (code === 10001) {
message.info("验证码已发送至管理邮箱")
message.info('验证码已发送至管理邮箱');
setIsAuth(true);
return;
}
message.error(msg);
} catch {
message.error('登录失败');
}
@ -100,16 +104,17 @@ const Page = () => {
},
]}
/>
{
isAuth?
{isAuth ? (
<ProFormText
name="authCode"
label="验证码"
width="lg"
placeholder="请输入验证码"
rules={[{ required: true, message: '请输入验证码' }]}
/>:<></>
}
/>
) : (
<></>
)}
{/* <div
style={{
marginBlockEnd: 24,

View File

@ -1,12 +1,14 @@
import { logisticscontrollerGetlist, logisticscontrollerGetshipmentlabel,
import {
logisticscontrollerDeleteshipment,
logisticscontrollerUpdateshipmentstate
logisticscontrollerGetlist,
logisticscontrollerGetshipmentlabel,
logisticscontrollerUpdateshipmentstate,
} from '@/servers/api/logistics';
import { sitecontrollerAll } from '@/servers/api/site';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { formatUniuniShipmentState } from '@/utils/format';
import { printPDF } from '@/utils/util';
import { CopyOutlined } from '@ant-design/icons';
import { ToastContainer, toast } from 'react-toastify';
import {
ActionType,
PageContainer,
@ -15,7 +17,7 @@ import {
} from '@ant-design/pro-components';
import { App, Button, Divider, Popconfirm } from 'antd';
import { useRef, useState } from 'react';
import { sitecontrollerAll } from '@/servers/api/site';
import { ToastContainer } from 'react-toastify';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
@ -69,7 +71,9 @@ const ListPage: React.FC = () => {
<CopyOutlined
onClick={async () => {
try {
await navigator.clipboard.writeText(record.return_tracking_number);
await navigator.clipboard.writeText(
record.return_tracking_number,
);
message.success('复制成功!');
} catch (err) {
message.error('复制失败!');
@ -106,7 +110,9 @@ const ListPage: React.FC = () => {
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const { data } = await logisticscontrollerGetshipmentlabel({shipmentId:record.id});
const { data } = await logisticscontrollerGetshipmentlabel({
shipmentId: record.id,
});
const content = data.content;
printPDF([content]);
setIsLoading(false);
@ -120,7 +126,9 @@ const ListPage: React.FC = () => {
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const res = await logisticscontrollerUpdateshipmentstate({shipmentId:record.id});
const res = await logisticscontrollerUpdateshipmentstate({
shipmentId: record.id,
});
console.log('res', res);
setIsLoading(false);

View File

@ -1,6 +1,5 @@
import {
logisticscontrollerGetservicelist,
logisticscontrollerSyncservices,
logisticscontrollerToggleactive,
} from '@/servers/api/logistics';
import {
@ -10,7 +9,7 @@ import {
ProFormSwitch,
ProTable,
} from '@ant-design/pro-components';
import { App, Button } from 'antd';
import { App } from 'antd';
import { useRef } from 'react';
const ListPage: React.FC = () => {

View File

@ -1,11 +1,15 @@
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';
import type {
ActionType,
ProColumns,
ProTableProps,
} from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { PageContainer } from '@ant-design/pro-layout';
import { App } from 'antd';
import dayjs from 'dayjs';
import React, { useRef } from 'react';
// 列表行数据结构(订单商品聚合)
interface OrderItemAggRow {
@ -87,7 +91,10 @@ const OrderItemsPage: React.FC = () => {
request: async () => {
// 拉取站点列表(后台 /site/all)
const { data = [] } = await sitecontrollerAll();
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
return (data || []).map((item: any) => ({
label: item.siteName,
value: item.id,
}));
},
},
{
@ -104,7 +111,9 @@ const OrderItemsPage: React.FC = () => {
];
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => {
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 || [];
@ -115,7 +124,9 @@ const OrderItemsPage: React.FC = () => {
siteId,
name,
isSource: true as any,
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
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;
@ -132,10 +143,12 @@ const OrderItemsPage: React.FC = () => {
};
return (
<PageContainer title='订单商品概览'>
<PageContainer title="订单商品概览">
<ProTable<OrderItemAggRow>
actionRef={actionRef}
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`}
rowKey={(r) =>
`${r.externalProductId}-${r.externalVariationId}-${r.name}`
}
columns={columns}
request={request}
pagination={{ showSizeChanger: true }}

View File

@ -1,16 +1,13 @@
import styles from '../../../style/order-list.css';
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
import { HistoryOrder } from '@/pages/Statistics/Order';
import { ORDER_STATUS_ENUM } from '@/constants';
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
logisticscontrollerCreateshipment,
logisticscontrollerGetshipmentfee,
logisticscontrollerDelshipment,
logisticscontrollerGetpaymentmethods,
logisticscontrollerGetratelist,
logisticscontrollerGetshipmentfee,
logisticscontrollerGetshippingaddresslist,
// logisticscontrollerGetshipmentlabel,
} from '@/servers/api/logistics';
import {
ordercontrollerCancelorder,
@ -27,9 +24,9 @@ import {
ordercontrollerUpdateorderitems,
} from '@/servers/api/order';
import { productcontrollerSearchproducts } from '@/servers/api/product';
import { wpproductcontrollerSearchproducts } from '@/servers/api/wpProduct';
import { sitecontrollerAll } from '@/servers/api/site';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { wpproductcontrollerSearchproducts } from '@/servers/api/wpProduct';
import { formatShipmentState, formatSource } from '@/utils/format';
import {
CodeSandboxOutlined,
@ -65,7 +62,6 @@ import {
Button,
Card,
Col,
Descriptions,
Divider,
Drawer,
Dropdown,
@ -79,10 +75,8 @@ import {
TabsProps,
Tag,
} from 'antd';
import Item from 'antd/es/list/Item';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
import React, { useMemo, useRef, useState } from 'react';
import { printPDF } from '@/utils/util';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
const ListPage: React.FC = () => {
const actionRef = useRef<ActionType>();
@ -129,14 +123,13 @@ const ListPage: React.FC = () => {
label: '已申请退款',
},
{
key: 'refund_approved',
label: "已退款",
label: '已退款',
// label: '退款申请已通过',
},
{
key: 'refund_cancelled',
label: "已完成"
label: '已完成',
// label: '已取消退款',
},
// {
@ -167,6 +160,10 @@ const ListPage: React.FC = () => {
dataIndex: 'id',
hideInSearch: true,
},
{
title: '订单ID',
dataIndex: 'externalOrderId',
},
{
title: '日期',
dataIndex: 'date',
@ -179,9 +176,16 @@ const ListPage: React.FC = () => {
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>;
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>
);
},
},
{
@ -306,7 +310,11 @@ const ListPage: React.FC = () => {
record.orderStatus,
) ? (
<>
<Shipping id={record.id as number} tableRef={actionRef} setActiveLine={setActiveLine} />
<Shipping
id={record.id as number}
tableRef={actionRef}
setActiveLine={setActiveLine}
/>
<Divider type="vertical" />
</>
) : (
@ -359,11 +367,12 @@ const ListPage: React.FC = () => {
},
{
key: 'history',
label:
label: (
<HistoryOrder
email={record.customer_email}
tableRef={actionRef}
/>,
/>
),
},
{
key: 'note',
@ -436,7 +445,9 @@ const ListPage: React.FC = () => {
actionRef={actionRef}
rowKey="id"
rowClassName={(record) => {
return record.id === activeLine ? styles['selected-line-order-protable'] : '';
return record.id === activeLine
? styles['selected-line-order-protable']
: '';
}}
toolBarRender={() => [
<CreateOrder tableRef={actionRef} />,
@ -525,7 +536,7 @@ const Detail: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
orderId: number;
record: API.Order;
setActiveLine: Function
setActiveLine: Function;
}> = ({ tableRef, orderId, record, setActiveLine }) => {
const [visiable, setVisiable] = useState(false);
const { message } = App.useApp();
@ -557,10 +568,14 @@ const Detail: React.FC<{
return (
<>
<Button key="detail" type="primary" onClick={() => {
<Button
key="detail"
type="primary"
onClick={() => {
setVisiable(true);
setActiveLine(record.id);
}}>
}}
>
<FileDoneOutlined />
</Button>
@ -781,7 +796,9 @@ const Detail: React.FC<{
/>
<ProDescriptions.Item label="金额" dataIndex="total" />
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
<ProDescriptions.Item label="联系电话" span={3}
<ProDescriptions.Item
label="联系电话"
span={3}
render={(_, record) => {
return (
<div>
@ -790,7 +807,8 @@ const Detail: React.FC<{
</span>
</div>
);
}} />
}}
/>
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
<ProDescriptions.Item label="设备" dataIndex="device_type" />
@ -937,12 +955,7 @@ const Detail: React.FC<{
label="换货"
span={3}
render={(_, record) => {
return (
<SalesChange
detailRef={ref}
id={record.id as number}
/>
)
return <SalesChange detailRef={ref} id={record.id as number} />;
}}
/>
<ProDescriptions.Item
@ -1194,7 +1207,8 @@ const Shipping: React.FC<{
},
}}
trigger={
<Button type="primary"
<Button
type="primary"
onClick={() => {
setActiveLine(id);
}}
@ -1289,10 +1303,16 @@ const Shipping: React.FC<{
],
},
},
};
}}
onFinish={async ({ customer_note, notes, items, details, externalOrderId, ...data }) => {
onFinish={async ({
customer_note,
notes,
items,
details,
externalOrderId,
...data
}) => {
details.origin.email_addresses =
details.origin.email_addresses.split(',');
details.destination.email_addresses =
@ -1301,8 +1321,11 @@ const Shipping: React.FC<{
details.destination.phone_number.phone;
details.origin.phone_number.number = details.origin.phone_number.phone;
try {
const { success, message: errMsg, ...resShipment } =
await logisticscontrollerCreateshipment(
const {
success,
message: errMsg,
...resShipment
} = await logisticscontrollerCreateshipment(
{ orderId: id },
{
details,
@ -1344,11 +1367,7 @@ const Shipping: React.FC<{
}
}}
>
<ProFormText
label="订单号"
readonly
name={"externalOrderId"}
/>
<ProFormText label="订单号" readonly name={'externalOrderId'} />
<ProFormText label="客户备注" readonly name="customer_note" />
<ProFormList
label="后台备注"
@ -1901,7 +1920,14 @@ const Shipping: React.FC<{
loading={ratesLoading}
onClick={async () => {
try {
const { customer_note, notes, items, details, externalOrderId, ...data } = formRef.current?.getFieldsValue();
const {
customer_note,
notes,
items,
details,
externalOrderId,
...data
} = formRef.current?.getFieldsValue();
const originEmail = details.origin.email_addresses;
const destinationEmail = details.destination.email_addresses;
details.origin.email_addresses =
@ -1910,41 +1936,54 @@ const Shipping: React.FC<{
details.destination.email_addresses.split(',');
details.destination.phone_number.number =
details.destination.phone_number.phone;
details.origin.phone_number.number = details.origin.phone_number.phone;
const res =
await logisticscontrollerGetshipmentfee(
{
details.origin.phone_number.number =
details.origin.phone_number.phone;
const res = await logisticscontrollerGetshipmentfee({
stockPointId: data.stockPointId,
sender: details.origin.contact_name,
startPhone: details.origin.phone_number,
startPostalCode: details.origin.address.postal_code.replace(/\s/g, ''),
startPostalCode: details.origin.address.postal_code.replace(
/\s/g,
'',
),
pickupAddress: details.origin.address.address_line_1,
shipperCountryCode: details.origin.address.country,
receiver: details.destination.contact_name,
city: details.destination.address.city,
province: details.destination.address.region,
country: details.destination.address.country,
postalCode: details.destination.address.postal_code.replace(/\s/g, ''),
postalCode: details.destination.address.postal_code.replace(
/\s/g,
'',
),
deliveryAddress: details.destination.address.address_line_1,
receiverPhone: details.destination.phone_number.number,
receiverEmail: details.destination.email_addresses,
length: details.packaging_properties.packages[0].measurements.cuboid.l,
width: details.packaging_properties.packages[0].measurements.cuboid.w,
height: details.packaging_properties.packages[0].measurements.cuboid.h,
dimensionUom: details.packaging_properties.packages[0].measurements.cuboid.unit,
weight: details.packaging_properties.packages[0].measurements.weight.value,
weightUom: details.packaging_properties.packages[0].measurements.weight.unit,
},
);
length:
details.packaging_properties.packages[0].measurements.cuboid.l,
width:
details.packaging_properties.packages[0].measurements.cuboid.w,
height:
details.packaging_properties.packages[0].measurements.cuboid.h,
dimensionUom:
details.packaging_properties.packages[0].measurements.cuboid
.unit,
weight:
details.packaging_properties.packages[0].measurements.weight
.value,
weightUom:
details.packaging_properties.packages[0].measurements.weight
.unit,
});
if (!res?.success) throw new Error(res?.message);
const fee = res.data;
setShipmentFee(fee);
details.origin.email_addresses = originEmail;
details.destination.email_addresses = destinationEmail;
formRef.current?.setFieldValue("details", {
formRef.current?.setFieldValue('details', {
...details,
shipmentFee: fee
shipmentFee: fee,
});
message.success('获取运费成功');
} catch (error) {
@ -1956,9 +1995,9 @@ const Shipping: React.FC<{
</Button>
<ProFormText
readonly
name={["details", "shipmentFee"]}
name={['details', 'shipmentFee']}
fieldProps={{
value: (shipmentFee / 100.0).toFixed(2)
value: (shipmentFee / 100.0).toFixed(2),
}}
/>
</ModalForm>
@ -1972,7 +2011,6 @@ const SalesChange: React.FC<{
}> = ({ id, detailRef }) => {
const formRef = useRef<ProFormInstance>();
return (
<ModalForm
formRef={formRef}
@ -1997,7 +2035,8 @@ const SalesChange: React.FC<{
orderId: id,
});
if (!success || !data) return {};
data.sales = data.sales?.reduce((acc: API.OrderSale[], cur: API.OrderSale) => {
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
if (idx === -1) {
acc.push(cur);
@ -2009,7 +2048,6 @@ const SalesChange: React.FC<{
[],
);
// setOptions(
// data.sales?.map((item) => ({
// label: item.name,
@ -2020,22 +2058,21 @@ const SalesChange: React.FC<{
}}
onFinish={async (formData: any) => {
const { sales } = formData;
const res = await ordercontrollerUpdateorderitems({ orderId: id }, sales);
const res = await ordercontrollerUpdateorderitems(
{ orderId: id },
sales,
);
if (!res.success) {
message.error(`更新货物信息失败: ${res.message}`);
return false;
}
message.success('更新成功')
message.success('更新成功');
detailRef?.current?.reload();
return true;
}}
>
<ProFormList
label="换货订单"
name="items"
>
<ProFormList label="换货订单" name="items">
<ProForm.Group>
<ProFormSelect
params={{}}
request={async ({ keyWords }) => {
@ -2043,14 +2080,12 @@ const SalesChange: React.FC<{
const { data } = await wpproductcontrollerSearchproducts({
name: keyWords,
});
return (
data?.map((item) => {
return data?.map((item) => {
return {
label: `${item.name}`,
value: item?.sku,
};
})
);
});
} catch (error) {
return [];
}
@ -2076,17 +2111,11 @@ const SalesChange: React.FC<{
precision: 0,
}}
/>
</ProForm.Group>
</ProFormList>
<ProFormList
label="换货产品"
name="sales"
>
<ProFormList label="换货产品" name="sales">
<ProForm.Group>
<ProFormSelect
params={{}}
request={async ({ keyWords }) => {
@ -2094,14 +2123,12 @@ const SalesChange: React.FC<{
const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return (
data?.map((item) => {
return data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
})
);
});
} catch (error) {
return [];
}
@ -2131,7 +2158,7 @@ const SalesChange: React.FC<{
</ProFormList>
</ModalForm>
);
}
};
const CreateOrder: React.FC<{
tableRef?: React.MutableRefObject<ActionType | undefined>;

View File

@ -29,21 +29,28 @@ const NameCn: React.FC<{
}> = ({ value, tableRef, id }) => {
const { message } = App.useApp();
const [editable, setEditable] = React.useState<boolean>(false);
if (!editable) return <div onClick={() => setEditable(true)}>{value||'-'}</div>;
return <ProFormText fieldProps={{autoFocus:true}} initialValue={value} onBlur={async(e) => {
if(!e.target.value) return setEditable(false)
if (!editable)
return <div onClick={() => setEditable(true)}>{value || '-'}</div>;
return (
<ProFormText
fieldProps={{ autoFocus: true }}
initialValue={value}
onBlur={async (e) => {
if (!e.target.value) return setEditable(false);
const { success, message: errMsg } =
await productcontrollerUpdateproductnamecn({
id,
nameCn: e.target.value,
})
setEditable(false)
});
setEditable(false);
if (!success) {
return message.error(errMsg)
}
tableRef?.current?.reload()
}} />
return message.error(errMsg);
}
tableRef?.current?.reload();
}}
/>
);
};
const List: React.FC = () => {
const actionRef = useRef<ActionType>();
@ -59,7 +66,7 @@ const List: React.FC = () => {
render: (_, record) => {
return (
<NameCn value={record.nameCn} id={record.id} tableRef={actionRef} />
)
);
},
},
{

View File

@ -264,7 +264,7 @@ const UpdateStatus: React.FC<{
},
{
status,
stock_status
stock_status,
},
);
if (!success) {
@ -296,7 +296,6 @@ const UpdateStatus: React.FC<{
);
};
const UpdateForm: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
values: API.WpProductDTO;

View File

@ -1,8 +1,16 @@
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 {
ActionType,
DrawerForm,
ProColumns,
ProFormInstance,
ProFormSelect,
ProFormSwitch,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, message, Popconfirm, Space, Tag } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
// 站点数据项类型(前端不包含密钥字段,后端列表不返回密钥)
interface SiteItem {
@ -58,10 +66,21 @@ const SiteList: React.FC = () => {
// 表格列定义
const columns: ProColumns<SiteItem>[] = [
{ title: 'ID', dataIndex: 'id', width: 80, sorter: true, hideInSearch: true },
{
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: 'SKU 前缀',
dataIndex: 'skuPrefix',
width: 160,
hideInSearch: true,
},
{
title: '平台',
dataIndex: 'type',
@ -101,7 +120,9 @@ const SiteList: React.FC = () => {
</Button>
<Popconfirm
title={row.isDisabled ? '启用站点' : '禁用站点'}
description={row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'}
description={
row.isDisabled ? '确认启用该站点?' : '确认禁用该站点?'
}
onConfirm={async () => {
try {
await request(`/site/disable/${row.id}`, {
@ -159,7 +180,9 @@ const SiteList: React.FC = () => {
...(values.siteName ? { siteName: values.siteName } : {}),
...(values.apiUrl ? { apiUrl: values.apiUrl } : {}),
...(values.type ? { type: values.type } : {}),
...(typeof values.isDisabled === 'boolean' ? { isDisabled: values.isDisabled } : {}),
...(typeof values.isDisabled === 'boolean'
? { isDisabled: values.isDisabled }
: {}),
...(values.skuPrefix ? { skuPrefix: values.skuPrefix } : {}),
};
// 仅当输入了新密钥时才提交,未输入则保持原本值
@ -169,7 +192,10 @@ const SiteList: React.FC = () => {
if (values.consumerSecret && values.consumerSecret.trim()) {
payload.consumerSecret = values.consumerSecret.trim();
}
await request(`/site/update/${editing.id}`, { method: 'PUT', data: payload });
await request(`/site/update/${editing.id}`, {
method: 'PUT',
data: payload,
});
} else {
// 新增站点时要求填写 consumerKey 和 consumerSecret
if (!values.consumerKey || !values.consumerSecret) {
@ -227,9 +253,18 @@ const SiteList: React.FC = () => {
onFinish={handleSubmit}
>
{/* 站点名称,必填 */}
<ProFormText name="siteName" label="站点名称" placeholder="例如:本地商店" rules={[{ required: true, message: '站点名称为必填项' }]} />
<ProFormText
name="siteName"
label="站点名称"
placeholder="例如:本地商店"
rules={[{ required: true, message: '站点名称为必填项' }]}
/>
{/* API 地址,可选 */}
<ProFormText name="apiUrl" label="API 地址" placeholder="例如https://shop.example.com" />
<ProFormText
name="apiUrl"
label="API 地址"
placeholder="例如https://shop.example.com"
/>
{/* 平台类型选择 */}
<ProFormSelect
name="type"
@ -241,11 +276,27 @@ const SiteList: React.FC = () => {
/>
{/* 是否禁用 */}
<ProFormSwitch name="isDisabled" label="禁用" />
<ProFormText name="skuPrefix" label="SKU 前缀" placeholder={editing ? '留空表示不修改' : '可选'} />
<ProFormText
name="skuPrefix"
label="SKU 前缀"
placeholder={editing ? '留空表示不修改' : '可选'}
/>
{/* WooCommerce REST consumer key新增必填编辑不填则保持原值 */}
<ProFormText name="consumerKey" label="Key" placeholder={editing ? '留空表示不修改' : '必填'} rules={editing ? [] : [{ required: true, message: '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 为必填项' }]} />
<ProFormText
name="consumerSecret"
label="Secret"
placeholder={editing ? '留空表示不修改' : '必填'}
rules={
editing ? [] : [{ required: true, message: 'Secret 为必填项' }]
}
/>
</DrawerForm>
</>
);

View File

@ -21,7 +21,9 @@ import { Button, Space, Tag } from 'antd';
import dayjs from 'dayjs';
import ReactECharts from 'echarts-for-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.extend(weekOfYear);
const highlightText = (text: string, keyword: string) => {
if (!keyword) return text;
const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
@ -58,8 +60,7 @@ const ListPage: React.FC = () => {
}
return (
`<div>${xValue} ${
['周日', '周一', '周二', '周三', '周四', '周五', '周六'][
`<div>${xValue} ${['周日', '周一', '周二', '周三', '周四', '周五', '周六'][
dayjs(xValue).day()
]
}</div>` + rows.join('<br/>')
@ -128,7 +129,22 @@ const ListPage: React.FC = () => {
});
if (success) {
const res = data?.sort(() => -1);
setXAxis(res?.map((v) => dayjs(v.order_date).format('YYYY-MM-DD')));
const formatMap = {
month: 'YYYY-MM',
week: 'YYYY年第WW周',
day: 'YYYY-MM-DD',
};
const format = formatMap[params.grouping] || 'YYYY-MM-DD';
if (params.grouping === 'week') {
setXAxis(res?.map((v) => {
const [year, week] = v.order_date.split('-');
return `${year}年第${week}`;
}));
} else {
setXAxis(res?.map((v) => dayjs(v.order_date).format(format)));
}
setSeries([
{
name: 'TOGO CPC订单数',
@ -583,6 +599,16 @@ const ListPage: React.FC = () => {
name="date"
/>
{/* <ProFormText label="关键词" name="keyword" /> */}
<ProFormSelect
label="统计周期"
name="grouping"
initialValue="day"
options={[
{ label: '月', value: 'month' },
{ label: '周', value: 'week' },
{ label: '日', value: 'day' },
]}
/>
<ProFormSelect
label="站点"
name="siteId"

View File

@ -1,39 +1,51 @@
import React, { useEffect, useState, useMemo, useRef } from "react"
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
statisticscontrollerGetinativeusersbymonth,
statisticscontrollerGetordersorce,
} from '@/servers/api/statistics';
import {
ActionType,
PageContainer, ProColumns, ProTable,
PageContainer,
ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { statisticscontrollerGetordersorce, statisticscontrollerGetinativeusersbymonth } from "@/servers/api/statistics";
import ReactECharts from 'echarts-for-react';
import { App, Button, Space, Tag } from 'antd';
import { HistoryOrder } from "../Order";
import { Space, Tag } from 'antd';
import dayjs from 'dayjs';
import ReactECharts from 'echarts-for-react';
import { HistoryOrder } from '../Order';
const ListPage: React.FC = () => {
const [data, setData] = useState({});
useEffect(() => {
statisticscontrollerGetordersorce().then(({ data, success }) => {
if(success) setData(data)
if (success) setData(data);
});
}, []);
const option = useMemo(() => {
if(!data.inactiveRes) return {}
const xAxisData = data?.inactiveRes?.map(v=> v.order_month)?.sort(_=>-1)
const arr = data?.res?.map(v=>v.first_order_month_group)
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index).sort((a,b)=> a.localeCompare(b))
if (!data.inactiveRes) return {};
const xAxisData = data?.inactiveRes
?.map((v) => v.order_month)
?.sort((_) => -1);
const arr = data?.res?.map((v) => v.first_order_month_group);
const uniqueArr = arr
.filter((item, index) => arr.indexOf(item) === index)
.sort((a, b) => a.localeCompare(b));
const series = [
{
name: '新客户',
type: 'bar',
data: data?.inactiveRes?.map(v=> v.new_user_count)?.sort(_=>-1),
data: data?.inactiveRes?.map((v) => v.new_user_count)?.sort((_) => -1),
label: {
show: true,
},
formatter: function (params) {
if (!params.value) return '';
return Math.abs(params.value) + '人';
},
emphasis: {
focus: 'series'
focus: 'series',
},
xAxisIndex: 0,
yAxisIndex: 0,
@ -41,86 +53,100 @@ const ListPage: React.FC = () => {
{
name: '老客户',
type: 'bar',
data: data?.inactiveRes?.map(v=> v.old_user_count)?.sort(_=>-1),
data: data?.inactiveRes?.map((v) => v.old_user_count)?.sort((_) => -1),
label: {
show: true,
},
emphasis: {
focus: 'series'
focus: 'series',
},
xAxisIndex: 0,
yAxisIndex: 0,
},
...uniqueArr?.map(v => {
data?.res?.filter(item => item.order_month === v)
...uniqueArr?.map((v) => {
data?.res?.filter((item) => item.order_month === v);
return {
name: v,
type: "bar",
stack: "total",
type: 'bar',
stack: 'total',
label: {
"show": true,
show: true,
formatter: function (params) {
if(!params.value) return ''
return Math.abs(params.value)
if (!params.value) return '';
return Math.abs(params.value);
},
color: '#fff'
color: '#fff',
},
"data": xAxisData.map(month => {
return (data?.res?.find(item => item.order_month === month && item.first_order_month_group === v)?.order_count || 0)
data: xAxisData.map((month) => {
return (
data?.res?.find(
(item) =>
item.order_month === month &&
item.first_order_month_group === v,
)?.order_count || 0
);
}),
xAxisIndex: 0,
yAxisIndex: 0,
}
};
}),
{
name: '未复购客户',
type: 'bar',
data: data?.inactiveRes?.map(v=> -v.inactive_user_count)?.sort(_=>-1),
stack: "total",
data: data?.inactiveRes
?.map((v) => -v.inactive_user_count)
?.sort((_) => -1),
stack: 'total',
label: {
show: true,
},
emphasis: {
focus: 'series'
focus: 'series',
},
xAxisIndex: 1,
yAxisIndex: 1,
barWidth: "60%",
barWidth: '60%',
itemStyle: {
color: '#f44336'
}
color: '#f44336',
},
]
},
];
return {
grid: [
{ top: '10%', height: '70%' },
{ bottom: '10%', height: '10%' }
{ bottom: '10%', height: '10%' },
],
legend: {
selectedMode: false
selectedMode: false,
},
xAxis: [{
xAxis: [
{
type: 'category',
data: xAxisData,
gridIndex: 0,
},{
},
{
type: 'category',
data: xAxisData,
gridIndex: 1,
}],
yAxis: [{
},
],
yAxis: [
{
type: 'value',
gridIndex: 0,
},{
},
{
type: 'value',
gridIndex: 1,
}],
},
],
series,
}
}, [data])
};
}, [data]);
const [tableData, setTableData] = useState<any[]>([])
const [tableData, setTableData] = useState<any[]>([]);
const actionRef = useRef<ActionType>();
const columns: ProColumns[] = [
{
@ -228,17 +254,17 @@ const ListPage: React.FC = () => {
onEvents={{
click: async (params) => {
if (params.componentType === 'series') {
setTableData([])
const {success, data} = await statisticscontrollerGetinativeusersbymonth({
month: params.name
})
if(success) setTableData(data)
setTableData([]);
const { success, data } =
await statisticscontrollerGetinativeusersbymonth({
month: params.name,
});
if (success) setTableData(data);
}
},
}}
/>
{
tableData?.length ?
{tableData?.length ? (
<ProTable
search={false}
headerTitle="查询表格"
@ -247,11 +273,11 @@ const ListPage: React.FC = () => {
dataSource={tableData}
columns={columns}
/>
:<></>
}
) : (
<></>
)}
</PageContainer>
)
}
);
};
export default ListPage;

View File

@ -223,14 +223,17 @@ const ListPage: React.FC = () => {
(yooneTotal.yoone12Quantity || 0) +
(yooneTotal.yoone15Quantity || 0) +
(yooneTotal.yoone18Quantity || 0) +
(yooneTotal.zexQuantity || 0)
}
(yooneTotal.zexQuantity || 0)}
</div>
<div>YOONE 3MG: {yooneTotal.yoone3Quantity || 0}</div>
<div>YOONE 6MG: {yooneTotal.yoone6Quantity || 0}</div>
<div>YOONE 9MG: {yooneTotal.yoone9Quantity || 0}</div>
<div>YOONE 12MG新: {yooneTotal.yoone12QuantityNew || 0}</div>
<div>YOONE 12MG白: {(yooneTotal.yoone12Quantity || 0) - (yooneTotal.yoone12QuantityNew || 0)}</div>
<div>
YOONE 12MG白:{' '}
{(yooneTotal.yoone12Quantity || 0) -
(yooneTotal.yoone12QuantityNew || 0)}
</div>
<div>YOONE 15MG: {yooneTotal.yoone15Quantity || 0}</div>
<div>YOONE 18MG: {yooneTotal.yoone18Quantity || 0}</div>
<div>ZEX: {yooneTotal.zexQuantity || 0}</div>

View File

@ -427,9 +427,7 @@ const UpdateForm: React.FC<{
<ProFormTextArea label="备注" name="note" width={'lg'} />
<ProFormDependency name={['items']}>
{({ items }) => {
return (
'数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0)
);
return '数量:' + items?.reduce((acc, cur) => acc + cur.quantity, 0);
}}
</ProFormDependency>
<ProFormList<API.PurchaseOrderItem>

View File

@ -31,12 +31,12 @@ const ListPage: React.FC = () => {
dataIndex: 'operationType',
valueType: 'select',
valueEnum: {
'in': {
text: '入库'
in: {
text: '入库',
},
out: {
text: '出库',
},
"out": {
text: '出库'
}
},
},
{

View File

@ -272,9 +272,15 @@ const CreateForm: React.FC<{
rules={[{ required: true, message: '请选择源目标仓库' }]}
/>
<ProFormTextArea name="note" label="备注" />
<ProFormText name={'orderNumber'} addonAfter={<Button onClick={async () => {
const orderNumber = await form.getFieldValue('orderNumber')
const { data } = await stockcontrollerGetpurchaseorder({orderNumber})
<ProFormText
name={'orderNumber'}
addonAfter={
<Button
onClick={async () => {
const orderNumber = await form.getFieldValue('orderNumber');
const { data } = await stockcontrollerGetpurchaseorder({
orderNumber,
});
form.setFieldsValue({
items: data?.map(
(item: { productName: string; productSku: string }) => ({
@ -285,11 +291,19 @@ const CreateForm: React.FC<{
},
}),
),
})
}}></Button>} />
});
}}
>
</Button>
}
/>
<ProFormDependency name={['items']}>
{({ items }) => {
return '数量:' + (items?.reduce?.((acc, cur) => acc + cur.quantity, 0)||0);
return (
'数量:' +
(items?.reduce?.((acc, cur) => acc + cur.quantity, 0) || 0)
);
}}
</ProFormDependency>
<ProFormList

View File

@ -1,4 +1,8 @@
import React, { useRef, useState } from 'react';
import { sitecontrollerAll } from '@/servers/api/site';
import {
subscriptioncontrollerList,
subscriptioncontrollerSync,
} from '@/servers/api/subscription';
import {
ActionType,
DrawerForm,
@ -7,14 +11,10 @@ import {
ProFormSelect,
ProTable,
} from '@ant-design/pro-components';
import { App, Button, Tag, Drawer, List } from 'antd';
import { App, Button, Drawer, List, Tag } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import { request } from 'umi';
import {
subscriptioncontrollerList,
subscriptioncontrollerSync,
} from '@/servers/api/subscription';
import { sitecontrollerAll } from '@/servers/api/site';
/**
* ()
@ -77,7 +77,11 @@ const ListPage: React.FC = () => {
valueEnum: SUBSCRIPTION_STATUS_ENUM,
// 以 Tag 形式展示,更易辨识
render: (_, row) =>
row?.status ? <Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag> : '-',
row?.status ? (
<Tag>{SUBSCRIPTION_STATUS_ENUM[row.status]?.text || row.status}</Tag>
) : (
'-'
),
width: 120,
},
{
@ -152,9 +156,9 @@ const ListPage: React.FC = () => {
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 candidates: any[] = (
Array.isArray(data) ? data : []
).filter((c: any) => String(c?.externalOrderId) === parentNumber);
// 拉取详情,补充状态、金额、时间
const details = [] as any[];
for (const c of candidates) {
@ -230,14 +234,22 @@ const ListPage: React.FC = () => {
<List.Item>
<List.Item.Meta
title={`#${item?.externalOrderId || '-'}`}
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`}
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>
<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 ?? '-'}
{typeof item?.total === 'number'
? item.total.toFixed(2)
: item?.total ?? '-'}
</span>
</div>
</List.Item>
@ -274,7 +286,9 @@ const SyncForm: React.FC<{
*/
onFinish={async (values) => {
try {
const { success, message: errMsg } = await subscriptioncontrollerSync(values);
const { success, message: errMsg } = await subscriptioncontrollerSync(
values,
);
if (!success) {
throw new Error(errMsg);
}

View File

@ -1,31 +1,21 @@
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';
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
import { App, Button, Card, Divider, Drawer, Empty, Popconfirm } from 'antd';
import React, { useEffect, useRef } from 'react';
// 服务器 API 引用(保持与原 index.tsx 一致)
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
import {
ordercontrollerChangestatus,
ordercontrollerGetorderdetail,
ordercontrollerSyncorderbyid,
} from '@/servers/api/order';
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
import { sitecontrollerAll } from '@/servers/api/site';
// 工具与子组件
import { ORDER_STATUS_ENUM } from '@/constants';
import { formatShipmentState, formatSource } from '@/utils/format';
import RelatedOrders from './RelatedOrders';
import { ORDER_STATUS_ENUM } from '@/constants';
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
@ -59,14 +49,18 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
// 加载详情数据(与 index.tsx 中完全保持一致)
const initRequest = async () => {
const { data, success }: API.OrderDetailRes = await ordercontrollerGetorderdetail({ orderId });
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) => {
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;
};
@ -220,65 +214,178 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
: []),
]}
>
<ProDescriptions labelStyle={{ width: '100px' }} actionRef={ref} request={initRequest}>
<ProDescriptions.Item label="站点" dataIndex="siteId" valueType="select" request={async () => {
<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} />
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="联系电话"
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) => (
<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>
company:
<span>
{r?.shipping?.company || r?.billing?.company || '-'}
</span>
</div>
)} />
<ProDescriptions.Item label="原始订单" span={3} render={(_, r: any) => (
<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>
<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) => (
)}
/>
<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>
<li key={item.id}>
{item.name}:{item.quantity}
</li>
))}
</ul>
)} />
<ProDescriptions.Item label="换货" span={3} render={(_, r: any) => (
)}
/>
<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="暂无备注" />);
)}
/>
<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' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<span>{note.username}</span>
<span>{note.createdAt}</span>
</div>
@ -287,38 +394,90 @@ const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
))}
</div>
);
}} />
<ProDescriptions.Item label="物流信息" span={3} render={(_, r: any) => {
if (!r.shipment || r.shipment.length === 0) return (<Empty description="暂无物流信息" />);
}}
/>
<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%' }}>
<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={<>
<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>
] : [] }
<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);
}
}}
>
<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>
<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>
);

View File

@ -1,7 +1,7 @@
import React from 'react';
import { Empty, Tag } from 'antd';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import React from 'react';
dayjs.extend(relativeTime);
@ -12,17 +12,36 @@ dayjs.extend(relativeTime);
*/
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 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 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 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 };
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="暂无关联" />;
@ -30,7 +49,14 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
return (
<div style={{ width: '100%' }}>
{/* 表头(英文文案,符合国际化默认英文的要求) */}
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr', padding: '8px 0', fontWeight: 600 }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '1.5fr 1fr 1fr 1fr 1fr',
padding: '8px 0',
fontWeight: 600,
}}
>
<div></div>
<div></div>
<div></div>
@ -39,11 +65,23 @@ const RelatedOrders: React.FC<{ data?: any[] }> = ({ data = [] }) => {
</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
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>
<Tag color={r.color}>{r.status}</Tag>
</div>
<div>{r.totalText}</div>
</div>
))}

View File

@ -1,12 +1,16 @@
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';
import type {
ActionType,
ProColumns,
ProTableProps,
} from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { PageContainer } from '@ant-design/pro-layout';
import { App, Button, Tag } from 'antd';
import dayjs from 'dayjs';
import React, { useRef, useState } from 'react';
import OrderDetailDrawer from './OrderDetailDrawer';
interface OrderItemRow {
id: number;
@ -43,7 +47,10 @@ const OrdersPage: React.FC = () => {
valueType: 'select',
request: async () => {
const { data = [] } = await sitecontrollerAll();
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
return (data || []).map((item: any) => ({
label: item.siteName,
value: item.id,
}));
},
},
{
@ -51,7 +58,10 @@ const OrdersPage: React.FC = () => {
dataIndex: 'date_created',
width: 180,
hideInSearch: true,
render: (_, row) => (row?.date_created ? dayjs(row.date_created).format('YYYY-MM-DD HH:mm') : '-'),
render: (_, row) =>
row?.date_created
? dayjs(row.date_created).format('YYYY-MM-DD HH:mm')
: '-',
},
{
title: '邮箱',
@ -109,7 +119,14 @@ const OrdersPage: React.FC = () => {
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
try {
const { current = 1, pageSize = 10, siteId, keyword, customer_email, payment_method } = params as any;
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,
@ -119,7 +136,9 @@ const OrdersPage: React.FC = () => {
customer_email,
payment_method,
isSubscriptionOnly: true as any,
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
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;
@ -136,10 +155,10 @@ const OrdersPage: React.FC = () => {
};
return (
<PageContainer title='订阅订单'>
<PageContainer title="订阅订单">
<ProTable<OrderItemRow>
actionRef={actionRef}
rowKey='id'
rowKey="id"
columns={columns}
request={request}
pagination={{ showSizeChanger: true }}

View File

@ -1,6 +1,6 @@
import {
logisticscontrollerGetlistbyorderid,
logisticscontrollerGetorderlist,
logisticscontrollerGetlistbyorderid
} from '@/servers/api/logistics';
import { SearchOutlined } from '@ant-design/icons';
import { PageContainer, ProFormSelect } from '@ant-design/pro-components';
@ -16,8 +16,9 @@ const TrackPage: React.FC = () => {
debounceTime={500}
request={async ({ keyWords }) => {
if (!keyWords || keyWords.length < 3) return [];
const { data: trackList } =
await logisticscontrollerGetorderlist({ number: keyWords });
const { data: trackList } = await logisticscontrollerGetorderlist({
number: keyWords,
});
return trackList?.map((v) => {
return {
label: v.siteName + ' ' + v.externalOrderId,
@ -29,7 +30,7 @@ const TrackPage: React.FC = () => {
prefix: '订单号',
async onChange(value: string) {
setId(value);
setData({})
setData({});
const { data } = await logisticscontrollerGetlistbyorderid({
id,
@ -53,8 +54,7 @@ const TrackPage: React.FC = () => {
),
}}
/>
{
data?.item ?
{data?.item ? (
<div>
<div>
<h4></h4>
@ -64,10 +64,11 @@ const TrackPage: React.FC = () => {
</div>
))}
</div>
</div> : <></>
}
{
data?.saleItem ?
</div>
) : (
<></>
)}
{data?.saleItem ? (
<div>
<div>
<h4></h4>
@ -77,8 +78,10 @@ const TrackPage: React.FC = () => {
</div>
))}
</div>
</div> : <></>
}
</div>
) : (
<></>
)}
</PageContainer>
);
};

View File

@ -95,8 +95,8 @@ export function formatUniuniShipmentState(state: string) {
'230': 'RETURN TO SENDER WAREHOUSE',
'231': 'FAILED_DELIVERY_RETRY1',
'232': 'FAILED_DELIVERY_RETRY2',
'255': 'Gateway_To_Gateway_Transit'
}
'255': 'Gateway_To_Gateway_Transit',
};
if (state in UNIUNI_STATUS_ENUM) {
return UNIUNI_STATUS_ENUM[state];
} else {