feat(区域管理): 新增区域管理模块及相关功能

- 添加区域列表页面及增删改查功能
- 在菜单中新增区域管理入口
- 更新权限控制添加区域访问权限
- 调整产品列表字段名从productName改为name
- 优化库存SKU选择器支持搜索和展示名称
- 修复类型定义中siteId类型不一致问题
This commit is contained in:
tikkhun 2025-12-01 19:06:18 +08:00
parent 9c35ada7b1
commit 07fca92ef3
10 changed files with 317 additions and 41 deletions

View File

@ -69,6 +69,18 @@ export default defineConfig({
component: './Template', // 对应 src/pages/Template/index.tsx component: './Template', // 对应 src/pages/Template/index.tsx
}, },
], ],
},
{
name: '地点管理',
path: '/area',
access: 'canSeeArea',
routes: [
{
name: '地点列表',
path: '/area/list',
component: './Area/List',
},
],
}, },
{ {
name: '站点管理', name: '站点管理',

View File

@ -38,10 +38,14 @@ export default (initialState: any) => {
isSuper || isSuper ||
isAdmin || isAdmin ||
(initialState?.user?.permissions?.includes('dict') ?? false); (initialState?.user?.permissions?.includes('dict') ?? false);
const canSeeTemplate = const canSeeTemplate =
isSuper || isSuper ||
isAdmin || isAdmin ||
(initialState?.user?.permissions?.includes('template') ?? false); (initialState?.user?.permissions?.includes('template') ?? false);
const canSeeArea =
isSuper ||
isAdmin ||
(initialState?.user?.permissions?.includes('area') ?? false);
return { return {
canSeeOrganiza, canSeeOrganiza,
canSeeProduct, canSeeProduct,
@ -53,5 +57,6 @@ export default (initialState: any) => {
canSeeSite, canSeeSite,
canSeeDict, canSeeDict,
canSeeTemplate, canSeeTemplate,
canSeeArea,
}; };
}; };

View File

@ -1,6 +1,6 @@
// 运行时配置 // 运行时配置
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { GlobalOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { import {
ProLayoutProps, ProLayoutProps,
ProSchemaValueEnumObj, ProSchemaValueEnumObj,
@ -56,12 +56,20 @@ export const layout = (): ProLayoutProps => {
menu: { menu: {
locale: false, locale: false,
}, },
menuDataRender: (menuData) => {
menuData.unshift({
path: '/area',
name: '区域管理',
icon: <GlobalOutlined />,
});
return menuData;
},
layout: 'mix', layout: 'mix',
actionsRender: () => ( actionsRender: () => (
<Dropdown key="avatar" menu={{ items }}> <Dropdown key="avatar" menu={{ items }}>
<div style={{ cursor: 'pointer' }}> <div style={{ cursor: 'pointer' }}>
<Avatar size="large" icon={<UserOutlined />} /> <Avatar size="large" icon={<UserOutlined />} />
<span style={{ marginLeft: 8 }}>{initialState?.name}</span> <span style={{ marginLeft: 8 }}>{initialState?.user?.name}</span>
</div> </div>
</Dropdown> </Dropdown>
), ),

View File

@ -0,0 +1,194 @@
import {
ActionType,
DrawerForm,
ProColumns,
ProFormInstance,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, message, Popconfirm, Space } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
interface AreaItem {
id: number;
name: string;
latitude?: number;
longitude?: number;
}
const AreaList: React.FC = () => {
const actionRef = useRef<ActionType>();
const formRef = useRef<ProFormInstance>();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<AreaItem | null>(null);
useEffect(() => {
if (!open) return;
if (editing) {
formRef.current?.setFieldsValue(editing);
} else {
formRef.current?.resetFields();
}
}, [open, editing]);
const columns: ProColumns<AreaItem>[] = [
{
title: 'ID',
dataIndex: 'id',
width: 80,
sorter: true,
hideInSearch: true,
},
{ title: '名称', dataIndex: 'name', width: 220 },
{
title: '纬度',
dataIndex: 'latitude',
width: 160,
hideInSearch: true,
},
{
title: '经度',
dataIndex: 'longitude',
width: 160,
hideInSearch: true,
},
{
title: '操作',
dataIndex: 'actions',
width: 240,
hideInSearch: true,
render: (_, row) => (
<Space>
<Button
size="small"
onClick={() => {
setEditing(row);
setOpen(true);
}}
>
</Button>
<Popconfirm
title="删除区域"
description="确认删除该区域?"
onConfirm={async () => {
try {
await request(`/area/${row.id}`, {
method: 'DELETE',
});
message.success('删除成功');
actionRef.current?.reload();
} catch (e: any) {
message.error(e?.message || '删除失败');
}
}}
>
<Button size="small" type="primary" danger>
</Button>
</Popconfirm>
</Space>
),
},
];
const tableRequest = async (params: Record<string, any>) => {
try {
const { current = 1, pageSize = 10, name } = params;
const resp = await request('/area', {
method: 'GET',
params: {
currentPage: current,
pageSize,
name: name || undefined,
},
});
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取失败');
return {
data: (data?.list ?? []) as AreaItem[],
total: data?.total ?? 0,
success: true,
};
} catch (e: any) {
message.error(e?.message || '获取失败');
return { data: [], total: 0, success: false };
}
};
const handleSubmit = async (values: AreaItem) => {
try {
if (editing) {
await request(`/area/${editing.id}`, {
method: 'PUT',
data: values,
});
} else {
await request('/area', {
method: 'POST',
data: values,
});
}
message.success('提交成功');
setOpen(false);
setEditing(null);
actionRef.current?.reload();
return true;
} catch (e: any) {
message.error(e?.message || '提交失败');
return false;
}
};
return (
<>
<ProTable<AreaItem>
actionRef={actionRef}
rowKey="id"
columns={columns}
request={tableRequest}
toolBarRender={() => [
<Button
key="new"
type="primary"
onClick={() => {
setEditing(null);
setOpen(true);
}}
>
</Button>,
]}
/>
<DrawerForm<AreaItem>
title={editing ? '编辑区域' : '新增区域'}
open={open}
onOpenChange={setOpen}
formRef={formRef}
onFinish={handleSubmit}
>
<ProFormText
name="name"
label="区域名称"
placeholder="例如Australia"
rules={[{ required: true, message: '区域名称为必填项' }]}
/>
<ProFormText
name="latitude"
label="纬度"
placeholder="例如:-33.8688"
/>
<ProFormText
name="longitude"
label="经度"
placeholder="例如151.2093"
/>
</DrawerForm>
</>
);
};
export default AreaList;

View File

View File

@ -5,9 +5,9 @@ import {
productcontrollerGetproductlist, productcontrollerGetproductlist,
productcontrollerSetproductcomponents, productcontrollerSetproductcomponents,
productcontrollerUpdateproduct, productcontrollerUpdateproduct,
productcontrollerUpdateproductnamecn, productcontrollerUpdatenamecn,
} from '@/servers/api/product'; } from '@/servers/api/product';
import { stockcontrollerGetstocks } from '@/servers/api/stock'; import { stockcontrollerGetstocks as getStocks } from '@/servers/api/stock';
import { templatecontrollerRendertemplate } from '@/servers/api/template'; import { templatecontrollerRendertemplate } from '@/servers/api/template';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { import {
@ -49,7 +49,7 @@ const NameCn: React.FC<{
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => { onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) return setEditable(false); if (!e.target.value) return setEditable(false);
const { success, message: errMsg } = const { success, message: errMsg } =
await productcontrollerUpdateproductnamecn({ await productcontrollerUpdatenamecn({
id, id,
nameCn: e.target.value, nameCn: e.target.value,
}); });
@ -444,7 +444,7 @@ const CreateForm: React.FC<{
// 如果 sku 存在 // 如果 sku 存在
if (sku) { if (sku) {
// 获取库存信息 // 获取库存信息
const { data } = await stockcontrollerGetstocks({ const { data } = await getStocks({
sku: sku, sku: sku,
} as any); } as any);
// 如果库存信息存在且不为空 // 如果库存信息存在且不为空
@ -523,6 +523,9 @@ const CreateForm: React.FC<{
placeholder="请输入SKU" placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]} rules={[{ required: true, message: '请输入SKU' }]}
/> />
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button>
{stockStatus && ( {stockStatus && (
<Tag <Tag
style={{ marginTop: '32px' }} style={{ marginTop: '32px' }}
@ -570,12 +573,27 @@ const CreateForm: React.FC<{
}} }}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText <ProFormSelect
name="sku" name="sku"
label="子产品SKU" label="子产品SKU"
width="md" width="md"
showSearch
debounceTime={300}
placeholder="请输入子产品SKU" placeholder="请输入子产品SKU"
rules={[{ required: true, message: '请输入子产品SKU' }]} rules={[{ required: true, message: '请输入子产品SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}
return data.items
.filter(item => item.sku)
.map(item => ({
label: `${item.sku} - ${item.name}`,
value: item.sku,
}));
}}
/> />
<ProFormDigit <ProFormDigit
name="quantity" name="quantity"
@ -663,7 +681,7 @@ const EditForm: React.FC<{
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
const { data: stockData } = await stockcontrollerGetstocks({ const { data: stockData } = await getStocks({
sku: record.sku, sku: record.sku,
} as any); } as any);
if (stockData && stockData.items && stockData.items.length > 0) { if (stockData && stockData.items && stockData.items.length > 0) {
@ -719,7 +737,7 @@ const EditForm: React.FC<{
// 如果 sku 存在 // 如果 sku 存在
if (sku) { if (sku) {
// 获取库存信息 // 获取库存信息
const { data } = await stockcontrollerGetstocks({ const { data } = await getStocks({
sku: sku, sku: sku,
} as any); } as any);
// 如果库存信息存在且不为空 // 如果库存信息存在且不为空
@ -867,12 +885,27 @@ const EditForm: React.FC<{
)} )}
> >
<ProForm.Group> <ProForm.Group>
<ProFormText <ProFormSelect
name="sku" name="sku"
label="库存SKU" label="库存SKU"
width="md" width="md"
showSearch
debounceTime={300}
placeholder="请输入库存SKU" placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]} rules={[{ required: true, message: '请输入库存SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}
return data.items
.filter(item => item.sku)
.map(item => ({
label: `${item.sku} - ${item.name}`,
value: item.sku,
}));
}}
/> />
<ProFormText <ProFormText
name="quantity" name="quantity"

View File

@ -31,12 +31,12 @@ const ListPage: React.FC = () => {
}, },
{ {
title: '产品名称', title: '产品名称',
dataIndex: 'productName', dataIndex: 'name',
sorter: true, sorter: true,
}, },
{ {
title: '中文名', title: '中文名',
dataIndex: 'productNameCn', dataIndex: 'nameCn',
hideInSearch: true, hideInSearch: true,
}, },

View File

@ -191,6 +191,21 @@ export async function dictcontrollerGetdictitems(
}); });
} }
/** 此处后端没有提供注释 GET /dict/items-by-name */
export async function dictcontrollerGetdictitemsbydictname(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.dictcontrollerGetdictitemsbydictnameParams,
options?: { [key: string]: any },
) {
return request<any>('/dict/items-by-name', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /dict/list */ /** 此处后端没有提供注释 GET /dict/list */
export async function dictcontrollerGetdicts( export async function dictcontrollerGetdicts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

@ -588,9 +588,9 @@ export async function productcontrollerCompatstrengthall(options?: {
} }
/** 此处后端没有提供注释 PUT /productupdateNameCn/${param1}/${param0} */ /** 此处后端没有提供注释 PUT /productupdateNameCn/${param1}/${param0} */
export async function productcontrollerUpdateproductnamecn( export async function productcontrollerUpdatenamecn(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象) // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.productcontrollerUpdateproductnamecnParams, params: API.productcontrollerUpdatenamecnParams,
options?: { [key: string]: any }, options?: { [key: string]: any },
) { ) {
const { nameCn: param0, id: param1, ...queryParams } = params; const { nameCn: param0, id: param1, ...queryParams } = params;

View File

@ -19,7 +19,6 @@ declare namespace API {
createdAt: string; createdAt: string;
/** 更新时间 */ /** 更新时间 */
updatedAt: string; updatedAt: string;
attributes?: any[];
}; };
type areacontrollerDeleteareaParams = { type areacontrollerDeleteareaParams = {
@ -87,6 +86,8 @@ declare namespace API {
promotionPrice?: number; promotionPrice?: number;
/** 商品类型 */ /** 商品类型 */
type?: 'simple' | 'bundle'; type?: 'simple' | 'bundle';
/** 产品组成 */
components?: any[];
}; };
type CreatePurchaseOrderDTO = { type CreatePurchaseOrderDTO = {
@ -161,7 +162,13 @@ declare namespace API {
id: number; id: number;
}; };
type dictcontrollerGetdictitemsbydictnameParams = {
name?: string;
};
type dictcontrollerGetdictitemsParams = { type dictcontrollerGetdictitemsParams = {
title?: string;
name?: string;
dictId?: number; dictId?: number;
}; };
@ -270,7 +277,7 @@ declare namespace API {
type Order = { type Order = {
id?: number; id?: number;
siteId?: string; siteId?: number;
externalOrderId?: string; externalOrderId?: string;
status?: any; status?: any;
orderStatus?: any; orderStatus?: any;
@ -358,7 +365,7 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
siteId?: string; siteId?: number;
name?: string; name?: string;
externalProductId?: string; externalProductId?: string;
externalVariationId?: string; externalVariationId?: string;
@ -373,7 +380,7 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
siteId?: string; siteId?: number;
name?: string; name?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
@ -386,7 +393,7 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
siteId?: string; siteId?: number;
name?: string; name?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
@ -398,7 +405,7 @@ declare namespace API {
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
externalOrderId?: string; externalOrderId?: string;
siteId?: string; siteId?: number;
customer_email?: string; customer_email?: string;
billing_phone?: string; billing_phone?: string;
keyword?: string; keyword?: string;
@ -445,7 +452,7 @@ declare namespace API {
type OrderDetail = { type OrderDetail = {
id?: number; id?: number;
siteId?: string; siteId?: number;
externalOrderId?: string; externalOrderId?: string;
status?: any; status?: any;
orderStatus?: any; orderStatus?: any;
@ -512,7 +519,7 @@ declare namespace API {
type OrderItem = { type OrderItem = {
id?: number; id?: number;
name?: string; name?: string;
siteId?: string; siteId?: number;
orderId?: number; orderId?: number;
externalOrderId?: string; externalOrderId?: string;
externalOrderItemId?: string; externalOrderItemId?: string;
@ -576,7 +583,7 @@ declare namespace API {
type OrderRefundItem = { type OrderRefundItem = {
id?: number; id?: number;
refundId?: number; refundId?: number;
siteId?: string; siteId?: number;
externalRefundId?: string; externalRefundId?: string;
externalRefundItemId?: string; externalRefundItemId?: string;
externalProductId?: string; externalProductId?: string;
@ -599,7 +606,7 @@ declare namespace API {
type OrderSale = { type OrderSale = {
id?: number; id?: number;
orderId?: number; orderId?: number;
siteId?: string; siteId?: number;
externalOrderItemId?: string; externalOrderItemId?: string;
productId?: number; productId?: number;
name?: string; name?: string;
@ -620,7 +627,7 @@ declare namespace API {
type OrderSaleDTO = { type OrderSaleDTO = {
id?: number; id?: number;
orderId?: number; orderId?: number;
siteId?: string; siteId?: number;
externalOrderItemId?: string; externalOrderItemId?: string;
productId?: number; productId?: number;
name?: string; name?: string;
@ -693,6 +700,8 @@ declare namespace API {
type Product = { type Product = {
/** ID */ /** ID */
id: number; id: number;
/** 类型 */
type?: string;
/** 产品名称 */ /** 产品名称 */
name: string; name: string;
/** 产品中文名称 */ /** 产品中文名称 */
@ -703,8 +712,6 @@ declare namespace API {
sku?: string; sku?: string;
/** 价格 */ /** 价格 */
price?: number; price?: number;
/** 类型 */
type?: string;
/** 促销价格 */ /** 促销价格 */
promotionPrice?: number; promotionPrice?: number;
/** 库存 */ /** 库存 */
@ -841,7 +848,7 @@ declare namespace API {
id: number; id: number;
}; };
type productcontrollerUpdateproductnamecnParams = { type productcontrollerUpdatenamecnParams = {
nameCn: string; nameCn: string;
id: number; id: number;
}; };
@ -925,7 +932,7 @@ declare namespace API {
type PurchaseOrderItem = { type PurchaseOrderItem = {
id?: number; id?: number;
sku?: string; sku?: string;
productName?: string; name?: string;
quantity?: number; quantity?: number;
price?: number; price?: number;
purchaseOrderId?: number; purchaseOrderId?: number;
@ -980,7 +987,7 @@ declare namespace API {
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
externalOrderId?: string; externalOrderId?: string;
siteId?: string; siteId?: number;
customer_email?: string; customer_email?: string;
billing_phone?: string; billing_phone?: string;
keyword?: string; keyword?: string;
@ -1009,7 +1016,7 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
siteId?: string; siteId?: number;
name?: string; name?: string;
externalProductId?: string; externalProductId?: string;
externalVariationId?: string; externalVariationId?: string;
@ -1024,7 +1031,7 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
siteId?: string; siteId?: number;
name?: string; name?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
@ -1073,7 +1080,8 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
productName?: string; name?: string;
sku?: string;
/** 按库存点ID排序 */ /** 按库存点ID排序 */
sortPointId?: number; sortPointId?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */ /** 排序对象,格式如 { productName: "asc", sku: "desc" } */
@ -1087,7 +1095,7 @@ declare namespace API {
pageSize?: number; pageSize?: number;
stockPointId?: number; stockPointId?: number;
sku?: string; sku?: string;
productName?: string; name?: string;
operationType?: string; operationType?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
@ -1329,7 +1337,7 @@ declare namespace API {
pageSize?: number; pageSize?: number;
stockPointId?: number; stockPointId?: number;
sku?: string; sku?: string;
productName?: string; name?: string;
operationType?: string; operationType?: string;
startDate?: string; startDate?: string;
endDate?: string; endDate?: string;
@ -1340,7 +1348,8 @@ declare namespace API {
current?: number; current?: number;
/** 每页大小 */ /** 每页大小 */
pageSize?: number; pageSize?: number;
productName?: string; name?: string;
sku?: string;
/** 按库存点ID排序 */ /** 按库存点ID排序 */
sortPointId?: number; sortPointId?: number;
/** 排序对象,格式如 { productName: "asc", sku: "desc" } */ /** 排序对象,格式如 { productName: "asc", sku: "desc" } */
@ -1384,7 +1393,7 @@ declare namespace API {
createdAt: string; createdAt: string;
/** 更新时间 */ /** 更新时间 */
updatedAt: string; updatedAt: string;
productName?: string; name?: string;
stockPoint?: Record<string, any>[]; stockPoint?: Record<string, any>[];
}; };
@ -1496,7 +1505,7 @@ declare namespace API {
type Subscription = { type Subscription = {
id?: number; id?: number;
/** 来源站点唯一标识 */ /** 来源站点唯一标识 */
siteId?: string; siteId?: number;
/** WooCommerce 订阅 ID */ /** WooCommerce 订阅 ID */
externalSubscriptionId?: string; externalSubscriptionId?: string;
status?: any; status?: any;
@ -1704,8 +1713,8 @@ declare namespace API {
type VariationDTO = { type VariationDTO = {
/** ID */ /** ID */
id: number; id: number;
/** wp网站ID */ /** 站点 id */
siteId: number; siteId?: number;
/** wp产品ID */ /** wp产品ID */
externalProductId: string; externalProductId: string;
/** wp变体ID */ /** wp变体ID */