From 735ff018be971e379ba1206f92103ca00f7156e8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 17 May 2026 17:48:53 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=88wms=EF=BC=89=EF=BC=9A=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20home=20=E7=BB=9F=E8=AE=A1=E7=9A=84=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/views/wms/home/index.vue | 72 +++++++++++ .../wms-home-inventory-chart-options.ts | 112 ++++++++++++++++++ .../modules/wms-home-inventory-charts.vue | 105 ++++++++++++++++ .../modules/wms-home-order-summary-cards.vue | 78 ++++++++++++ .../wms-home-order-trend-chart-options.ts | 91 ++++++++++++++ .../modules/wms-home-order-trend-chart.vue | 73 ++++++++++++ apps/web-ele/src/views/wms/home/index.vue | 72 +++++++++++ .../wms-home-inventory-chart-options.ts | 112 ++++++++++++++++++ .../modules/wms-home-inventory-charts.vue | 105 ++++++++++++++++ .../modules/wms-home-order-summary-cards.vue | 78 ++++++++++++ .../wms-home-order-trend-chart-options.ts | 91 ++++++++++++++ .../modules/wms-home-order-trend-chart.vue | 73 ++++++++++++ apps/web-ele/src/views/wms/utils/constants.ts | 35 ++++++ packages/constants/src/dict-enum.ts | 10 ++ 14 files changed, 1107 insertions(+) create mode 100644 apps/web-antd/src/views/wms/home/index.vue create mode 100644 apps/web-antd/src/views/wms/home/modules/wms-home-inventory-chart-options.ts create mode 100644 apps/web-antd/src/views/wms/home/modules/wms-home-inventory-charts.vue create mode 100644 apps/web-antd/src/views/wms/home/modules/wms-home-order-summary-cards.vue create mode 100644 apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts create mode 100644 apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart.vue create mode 100644 apps/web-ele/src/views/wms/home/index.vue create mode 100644 apps/web-ele/src/views/wms/home/modules/wms-home-inventory-chart-options.ts create mode 100644 apps/web-ele/src/views/wms/home/modules/wms-home-inventory-charts.vue create mode 100644 apps/web-ele/src/views/wms/home/modules/wms-home-order-summary-cards.vue create mode 100644 apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts create mode 100644 apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart.vue diff --git a/apps/web-antd/src/views/wms/home/index.vue b/apps/web-antd/src/views/wms/home/index.vue new file mode 100644 index 000000000..85c81d497 --- /dev/null +++ b/apps/web-antd/src/views/wms/home/index.vue @@ -0,0 +1,72 @@ + + + diff --git a/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-chart-options.ts b/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-chart-options.ts new file mode 100644 index 000000000..3998f40b5 --- /dev/null +++ b/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-chart-options.ts @@ -0,0 +1,112 @@ +import type { WmsHomeStatisticsApi } from '#/api/wms/home'; + +import { formatQuantity } from '#/views/wms/utils/format'; + +export interface InventoryChartItem { + name: string; + value: number; +} + +/** 格式化库存数量展示文本 */ +export function formatQuantityText(value?: number) { + return formatQuantity(value || 0) || '0.00'; +} + +/** 转换库存排行接口数据为 ECharts 可消费的 name/value 数据 */ +export function buildInventoryChartItemList( + list: undefined | WmsHomeStatisticsApi.InventoryRankItem[], + emptyName: string, +) { + return (list || []) + .map((item) => ({ + name: item.name || emptyName, + value: Number(item.quantity || 0), + })) + .filter((item) => item.value > 0); +} + +/** 格式化货物占比图例,补充当前商品库存占比 */ +function formatGoodsLegend(name: string, goodsShareList: InventoryChartItem[]) { + const total = goodsShareList.reduce((sum, item) => sum + item.value, 0); + const item = goodsShareList.find((goods) => goods.name === name); + if (!total || !item) { + return name; + } + return `${name} ${((item.value / total) * 100).toFixed(1)}%`; +} + +/** 货物占比图表配置 */ +export function getGoodsShareChartOptions(goodsShareList: InventoryChartItem[]): any { + return { + color: ['#2f7df6', '#18a058', '#f59e0b', '#7c3aed', '#14b8a6'], + legend: { + formatter: (name: string) => formatGoodsLegend(name, goodsShareList), + itemHeight: 10, + itemWidth: 10, + orient: 'vertical', + right: 10, + top: 'middle', + type: 'scroll', + }, + series: [ + { + avoidLabelOverlap: true, + center: ['34%', '52%'], + data: goodsShareList, + label: { show: false }, + labelLine: { show: false }, + name: '货物占比', + radius: ['48%', '70%'], + type: 'pie', + }, + ], + tooltip: { + formatter: '{b}
库存:{c} ({d}%)', + trigger: 'item', + }, + }; +} + +/** 库存分布图表配置 */ +export function getWarehouseDistributionChartOptions( + warehouseDistributionList: InventoryChartItem[], +): any { + const sortedList = warehouseDistributionList.toReversed(); + return { + color: ['#2f7df6'], + grid: { bottom: 16, containLabel: true, left: 24, right: 40, top: 12 }, + series: [ + { + barMaxWidth: 16, + data: sortedList.map((item) => item.value), + label: { + formatter: ({ value }: { value?: number }) => formatQuantityText(Number(value || 0)), + position: 'right', + show: true, + }, + name: '库存', + type: 'bar', + }, + ], + tooltip: { + formatter: (params: unknown) => { + const item = (Array.isArray(params) ? params[0] : params) as { + name?: string; + value?: number; + }; + return `${item.name || '-'}
库存:${formatQuantityText(item.value)}`; + }, + trigger: 'axis', + }, + xAxis: { + splitLine: { lineStyle: { color: '#eef2f7' } }, + type: 'value', + }, + yAxis: { + axisLine: { show: false }, + axisTick: { show: false }, + data: sortedList.map((item) => item.name), + type: 'category', + }, + }; +} diff --git a/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-charts.vue b/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-charts.vue new file mode 100644 index 000000000..6f2d333c3 --- /dev/null +++ b/apps/web-antd/src/views/wms/home/modules/wms-home-inventory-charts.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/web-antd/src/views/wms/home/modules/wms-home-order-summary-cards.vue b/apps/web-antd/src/views/wms/home/modules/wms-home-order-summary-cards.vue new file mode 100644 index 000000000..2f2ae0bec --- /dev/null +++ b/apps/web-antd/src/views/wms/home/modules/wms-home-order-summary-cards.vue @@ -0,0 +1,78 @@ + + + diff --git a/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts b/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts new file mode 100644 index 000000000..474e6d648 --- /dev/null +++ b/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts @@ -0,0 +1,91 @@ +import type { WmsHomeStatisticsApi } from '#/api/wms/home'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictLabel } from '@vben/hooks'; +import { formatDate } from '@vben/utils'; + +interface OrderDefinition { + color: string; + title: string; + trendField: keyof Pick< + WmsHomeStatisticsApi.OrderTrend, + 'checkCount' | 'movementCount' | 'receiptCount' | 'shipmentCount' + >; + type: number; +} + +const OrderTypeEnum = { + CHECK: 4, + MOVEMENT: 3, + RECEIPT: 1, + SHIPMENT: 2, +} as const; + +/** 获取 WMS 单据类型标题,用于消除字典里的“单”后缀 */ +function getOrderTypeTitle(type: number, defaultTitle: string) { + const label = getDictLabel(DICT_TYPE.WMS_ORDER_TYPE, type) || defaultTitle; + return label.endsWith('单') ? label.slice(0, -1) : label; +} + +const orderDefinitions: OrderDefinition[] = [ + { + color: '#2f7df6', + title: getOrderTypeTitle(OrderTypeEnum.RECEIPT, '入库'), + trendField: 'receiptCount', + type: OrderTypeEnum.RECEIPT, + }, + { + color: '#18a058', + title: getOrderTypeTitle(OrderTypeEnum.SHIPMENT, '出库'), + trendField: 'shipmentCount', + type: OrderTypeEnum.SHIPMENT, + }, + { + color: '#f59e0b', + title: getOrderTypeTitle(OrderTypeEnum.MOVEMENT, '移库'), + trendField: 'movementCount', + type: OrderTypeEnum.MOVEMENT, + }, + { + color: '#7c3aed', + title: getOrderTypeTitle(OrderTypeEnum.CHECK, '盘库'), + trendField: 'checkCount', + type: OrderTypeEnum.CHECK, + }, +]; + +/** 格式化趋势接口返回的时间戳为图表横轴日期 */ +function formatTrendTime(time: number | string) { + const date = new Date(time); + return Number.isNaN(date.getTime()) ? `${time}` : (formatDate(date, 'MM-DD') as string); +} + +/** 单据趋势图表配置 */ +export function getOrderTrendChartOptions(list: WmsHomeStatisticsApi.OrderTrend[]): any { + const labels = list.map((item) => formatTrendTime(item.time)); + return { + color: orderDefinitions.map((item) => item.color), + grid: { bottom: 24, containLabel: true, left: 28, right: 24, top: 48 }, + legend: { itemHeight: 10, itemWidth: 10, top: 6 }, + series: orderDefinitions.map((item) => ({ + barMaxWidth: 18, + data: list.map((row) => Number(row[item.trendField] || 0)), + emphasis: { focus: 'series' }, + name: item.title, + type: 'bar', + })), + tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' }, + xAxis: { + axisLine: { lineStyle: { color: '#dcdfe6' } }, + axisTick: { show: false }, + data: labels, + type: 'category', + }, + yAxis: { + minInterval: 1, + name: '单据数', + splitLine: { lineStyle: { color: '#eef2f7' } }, + type: 'value', + }, + }; +} diff --git a/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart.vue b/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart.vue new file mode 100644 index 000000000..b24608233 --- /dev/null +++ b/apps/web-antd/src/views/wms/home/modules/wms-home-order-trend-chart.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/web-ele/src/views/wms/home/index.vue b/apps/web-ele/src/views/wms/home/index.vue new file mode 100644 index 000000000..eab446b6a --- /dev/null +++ b/apps/web-ele/src/views/wms/home/index.vue @@ -0,0 +1,72 @@ + + + diff --git a/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-chart-options.ts b/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-chart-options.ts new file mode 100644 index 000000000..3998f40b5 --- /dev/null +++ b/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-chart-options.ts @@ -0,0 +1,112 @@ +import type { WmsHomeStatisticsApi } from '#/api/wms/home'; + +import { formatQuantity } from '#/views/wms/utils/format'; + +export interface InventoryChartItem { + name: string; + value: number; +} + +/** 格式化库存数量展示文本 */ +export function formatQuantityText(value?: number) { + return formatQuantity(value || 0) || '0.00'; +} + +/** 转换库存排行接口数据为 ECharts 可消费的 name/value 数据 */ +export function buildInventoryChartItemList( + list: undefined | WmsHomeStatisticsApi.InventoryRankItem[], + emptyName: string, +) { + return (list || []) + .map((item) => ({ + name: item.name || emptyName, + value: Number(item.quantity || 0), + })) + .filter((item) => item.value > 0); +} + +/** 格式化货物占比图例,补充当前商品库存占比 */ +function formatGoodsLegend(name: string, goodsShareList: InventoryChartItem[]) { + const total = goodsShareList.reduce((sum, item) => sum + item.value, 0); + const item = goodsShareList.find((goods) => goods.name === name); + if (!total || !item) { + return name; + } + return `${name} ${((item.value / total) * 100).toFixed(1)}%`; +} + +/** 货物占比图表配置 */ +export function getGoodsShareChartOptions(goodsShareList: InventoryChartItem[]): any { + return { + color: ['#2f7df6', '#18a058', '#f59e0b', '#7c3aed', '#14b8a6'], + legend: { + formatter: (name: string) => formatGoodsLegend(name, goodsShareList), + itemHeight: 10, + itemWidth: 10, + orient: 'vertical', + right: 10, + top: 'middle', + type: 'scroll', + }, + series: [ + { + avoidLabelOverlap: true, + center: ['34%', '52%'], + data: goodsShareList, + label: { show: false }, + labelLine: { show: false }, + name: '货物占比', + radius: ['48%', '70%'], + type: 'pie', + }, + ], + tooltip: { + formatter: '{b}
库存:{c} ({d}%)', + trigger: 'item', + }, + }; +} + +/** 库存分布图表配置 */ +export function getWarehouseDistributionChartOptions( + warehouseDistributionList: InventoryChartItem[], +): any { + const sortedList = warehouseDistributionList.toReversed(); + return { + color: ['#2f7df6'], + grid: { bottom: 16, containLabel: true, left: 24, right: 40, top: 12 }, + series: [ + { + barMaxWidth: 16, + data: sortedList.map((item) => item.value), + label: { + formatter: ({ value }: { value?: number }) => formatQuantityText(Number(value || 0)), + position: 'right', + show: true, + }, + name: '库存', + type: 'bar', + }, + ], + tooltip: { + formatter: (params: unknown) => { + const item = (Array.isArray(params) ? params[0] : params) as { + name?: string; + value?: number; + }; + return `${item.name || '-'}
库存:${formatQuantityText(item.value)}`; + }, + trigger: 'axis', + }, + xAxis: { + splitLine: { lineStyle: { color: '#eef2f7' } }, + type: 'value', + }, + yAxis: { + axisLine: { show: false }, + axisTick: { show: false }, + data: sortedList.map((item) => item.name), + type: 'category', + }, + }; +} diff --git a/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-charts.vue b/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-charts.vue new file mode 100644 index 000000000..6f2d333c3 --- /dev/null +++ b/apps/web-ele/src/views/wms/home/modules/wms-home-inventory-charts.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/web-ele/src/views/wms/home/modules/wms-home-order-summary-cards.vue b/apps/web-ele/src/views/wms/home/modules/wms-home-order-summary-cards.vue new file mode 100644 index 000000000..2f2ae0bec --- /dev/null +++ b/apps/web-ele/src/views/wms/home/modules/wms-home-order-summary-cards.vue @@ -0,0 +1,78 @@ + + + diff --git a/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts b/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts new file mode 100644 index 000000000..474e6d648 --- /dev/null +++ b/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart-options.ts @@ -0,0 +1,91 @@ +import type { WmsHomeStatisticsApi } from '#/api/wms/home'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictLabel } from '@vben/hooks'; +import { formatDate } from '@vben/utils'; + +interface OrderDefinition { + color: string; + title: string; + trendField: keyof Pick< + WmsHomeStatisticsApi.OrderTrend, + 'checkCount' | 'movementCount' | 'receiptCount' | 'shipmentCount' + >; + type: number; +} + +const OrderTypeEnum = { + CHECK: 4, + MOVEMENT: 3, + RECEIPT: 1, + SHIPMENT: 2, +} as const; + +/** 获取 WMS 单据类型标题,用于消除字典里的“单”后缀 */ +function getOrderTypeTitle(type: number, defaultTitle: string) { + const label = getDictLabel(DICT_TYPE.WMS_ORDER_TYPE, type) || defaultTitle; + return label.endsWith('单') ? label.slice(0, -1) : label; +} + +const orderDefinitions: OrderDefinition[] = [ + { + color: '#2f7df6', + title: getOrderTypeTitle(OrderTypeEnum.RECEIPT, '入库'), + trendField: 'receiptCount', + type: OrderTypeEnum.RECEIPT, + }, + { + color: '#18a058', + title: getOrderTypeTitle(OrderTypeEnum.SHIPMENT, '出库'), + trendField: 'shipmentCount', + type: OrderTypeEnum.SHIPMENT, + }, + { + color: '#f59e0b', + title: getOrderTypeTitle(OrderTypeEnum.MOVEMENT, '移库'), + trendField: 'movementCount', + type: OrderTypeEnum.MOVEMENT, + }, + { + color: '#7c3aed', + title: getOrderTypeTitle(OrderTypeEnum.CHECK, '盘库'), + trendField: 'checkCount', + type: OrderTypeEnum.CHECK, + }, +]; + +/** 格式化趋势接口返回的时间戳为图表横轴日期 */ +function formatTrendTime(time: number | string) { + const date = new Date(time); + return Number.isNaN(date.getTime()) ? `${time}` : (formatDate(date, 'MM-DD') as string); +} + +/** 单据趋势图表配置 */ +export function getOrderTrendChartOptions(list: WmsHomeStatisticsApi.OrderTrend[]): any { + const labels = list.map((item) => formatTrendTime(item.time)); + return { + color: orderDefinitions.map((item) => item.color), + grid: { bottom: 24, containLabel: true, left: 28, right: 24, top: 48 }, + legend: { itemHeight: 10, itemWidth: 10, top: 6 }, + series: orderDefinitions.map((item) => ({ + barMaxWidth: 18, + data: list.map((row) => Number(row[item.trendField] || 0)), + emphasis: { focus: 'series' }, + name: item.title, + type: 'bar', + })), + tooltip: { axisPointer: { type: 'shadow' }, trigger: 'axis' }, + xAxis: { + axisLine: { lineStyle: { color: '#dcdfe6' } }, + axisTick: { show: false }, + data: labels, + type: 'category', + }, + yAxis: { + minInterval: 1, + name: '单据数', + splitLine: { lineStyle: { color: '#eef2f7' } }, + type: 'value', + }, + }; +} diff --git a/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart.vue b/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart.vue new file mode 100644 index 000000000..b24608233 --- /dev/null +++ b/apps/web-ele/src/views/wms/home/modules/wms-home-order-trend-chart.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/web-ele/src/views/wms/utils/constants.ts b/apps/web-ele/src/views/wms/utils/constants.ts index b7d8166cc..a72929881 100644 --- a/apps/web-ele/src/views/wms/utils/constants.ts +++ b/apps/web-ele/src/views/wms/utils/constants.ts @@ -1,3 +1,38 @@ +/** 单据状态枚举 */ +export const OrderStatusEnum = { + PREPARE: 0, // 草稿 + FINISHED: 4, // 已完成 + CANCELED: 5, // 已作废 +} as const; + +/** 可修改的单据状态 */ +export const OrderUpdateStatusList: number[] = [OrderStatusEnum.PREPARE]; + +/** 可删除的单据状态 */ +export const OrderDeleteStatusList: number[] = [ + OrderStatusEnum.PREPARE, + OrderStatusEnum.CANCELED, +]; + +/** 往来企业类型枚举 */ +export const MerchantTypeEnum = { + CUSTOMER: 1, // 客户 + SUPPLIER: 2, // 供应商 + CUSTOMER_SUPPLIER: 3, // 客户/供应商 +} as const; + +/** 供应商类型的往来企业 */ +export const SupplierMerchantTypeList = [ + MerchantTypeEnum.SUPPLIER, + MerchantTypeEnum.CUSTOMER_SUPPLIER, +]; + +/** 客户类型的往来企业 */ +export const CustomerMerchantTypeList = [ + MerchantTypeEnum.CUSTOMER, + MerchantTypeEnum.CUSTOMER_SUPPLIER, +]; + /** * 生成 WMS 编号 / 条码。 * diff --git a/packages/constants/src/dict-enum.ts b/packages/constants/src/dict-enum.ts index e9477615b..ed93cf9fb 100644 --- a/packages/constants/src/dict-enum.ts +++ b/packages/constants/src/dict-enum.ts @@ -175,6 +175,15 @@ const IOT_DICT = { IOT_MODBUS_FRAME_FORMAT: 'iot_modbus_frame_format', // IoT Modbus 帧格式 } as const; +/** ========== WMS - 仓储管理模块 ========== */ +const WMS_DICT = { + WMS_MERCHANT_TYPE: 'merchant_type', // WMS 往来企业类型 + WMS_ORDER_TYPE: 'wms_order_type', // WMS 单据类型 + WMS_ORDER_STATUS: 'wms_order_status', // WMS 单据状态 + WMS_RECEIPT_ORDER_TYPE: 'wms_receipt_order_type', // WMS 入库单类型 + WMS_SHIPMENT_ORDER_TYPE: 'wms_shipment_order_type', // WMS 出库单类型 +} as const; + /** 字典类型枚举 - 统一导出 */ const DICT_TYPE = { ...AI_DICT, @@ -183,6 +192,7 @@ const DICT_TYPE = { ...ERP_DICT, ...INFRA_DICT, ...IOT_DICT, + ...WMS_DICT, ...MEMBER_DICT, ...MP_DICT, ...PAY_DICT,