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
},
],
},
{
name: '地点管理',
path: '/area',
access: 'canSeeArea',
routes: [
{
name: '地点列表',
path: '/area/list',
component: './Area/List',
},
],
},
{
name: '站点管理',

View File

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

View File

@ -1,6 +1,6 @@
// 运行时配置
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { GlobalOutlined, LogoutOutlined, UserOutlined } from '@ant-design/icons';
import {
ProLayoutProps,
ProSchemaValueEnumObj,
@ -56,12 +56,20 @@ export const layout = (): ProLayoutProps => {
menu: {
locale: false,
},
menuDataRender: (menuData) => {
menuData.unshift({
path: '/area',
name: '区域管理',
icon: <GlobalOutlined />,
});
return menuData;
},
layout: 'mix',
actionsRender: () => (
<Dropdown key="avatar" menu={{ items }}>
<div style={{ cursor: 'pointer' }}>
<Avatar size="large" icon={<UserOutlined />} />
<span style={{ marginLeft: 8 }}>{initialState?.name}</span>
<span style={{ marginLeft: 8 }}>{initialState?.user?.name}</span>
</div>
</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,
productcontrollerSetproductcomponents,
productcontrollerUpdateproduct,
productcontrollerUpdateproductnamecn,
productcontrollerUpdatenamecn,
} 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 { PlusOutlined } from '@ant-design/icons';
import {
@ -49,7 +49,7 @@ const NameCn: React.FC<{
onBlur: async (e: React.FocusEvent<HTMLInputElement>) => {
if (!e.target.value) return setEditable(false);
const { success, message: errMsg } =
await productcontrollerUpdateproductnamecn({
await productcontrollerUpdatenamecn({
id,
nameCn: e.target.value,
});
@ -444,7 +444,7 @@ const CreateForm: React.FC<{
// 如果 sku 存在
if (sku) {
// 获取库存信息
const { data } = await stockcontrollerGetstocks({
const { data } = await getStocks({
sku: sku,
} as any);
// 如果库存信息存在且不为空
@ -523,6 +523,9 @@ const CreateForm: React.FC<{
placeholder="请输入SKU"
rules={[{ required: true, message: '请输入SKU' }]}
/>
<Button style={{ marginTop: '32px' }} onClick={handleGenerateSku}>
</Button>
{stockStatus && (
<Tag
style={{ marginTop: '32px' }}
@ -570,12 +573,27 @@ const CreateForm: React.FC<{
}}
>
<ProForm.Group>
<ProFormText
<ProFormSelect
name="sku"
label="子产品SKU"
width="md"
showSearch
debounceTime={300}
placeholder="请输入子产品SKU"
rules={[{ required: true, message: '请输入子产品SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}
return data.items
.filter(item => item.sku)
.map(item => ({
label: `${item.sku} - ${item.name}`,
value: item.sku,
}));
}}
/>
<ProFormDigit
name="quantity"
@ -663,7 +681,7 @@ const EditForm: React.FC<{
React.useEffect(() => {
(async () => {
const { data: stockData } = await stockcontrollerGetstocks({
const { data: stockData } = await getStocks({
sku: record.sku,
} as any);
if (stockData && stockData.items && stockData.items.length > 0) {
@ -719,7 +737,7 @@ const EditForm: React.FC<{
// 如果 sku 存在
if (sku) {
// 获取库存信息
const { data } = await stockcontrollerGetstocks({
const { data } = await getStocks({
sku: sku,
} as any);
// 如果库存信息存在且不为空
@ -867,12 +885,27 @@ const EditForm: React.FC<{
)}
>
<ProForm.Group>
<ProFormText
<ProFormSelect
name="sku"
label="库存SKU"
width="md"
showSearch
debounceTime={300}
placeholder="请输入库存SKU"
rules={[{ required: true, message: '请输入库存SKU' }]}
request={async ({ keyWords }) => {
const params = keyWords ? { sku: keyWords, name: keyWords } : { pageSize: 9999 };
const { data } = await getStocks(params as any);
if (!data || !data.items) {
return [];
}
return data.items
.filter(item => item.sku)
.map(item => ({
label: `${item.sku} - ${item.name}`,
value: item.sku,
}));
}}
/>
<ProFormText
name="quantity"

View File

@ -31,12 +31,12 @@ const ListPage: React.FC = () => {
},
{
title: '产品名称',
dataIndex: 'productName',
dataIndex: 'name',
sorter: true,
},
{
title: '中文名',
dataIndex: 'productNameCn',
dataIndex: 'nameCn',
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 */
export async function dictcontrollerGetdicts(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)

View File

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

View File

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