feat(web-antdv-next): migrate WMS module
parent
e6e4d8ce1e
commit
82b22173c0
|
|
@ -0,0 +1,66 @@
|
|||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsHomeStatisticsApi {
|
||||
export interface StatisticsReq {
|
||||
goodsLimit?: number;
|
||||
warehouseId?: number;
|
||||
warehouseLimit?: number;
|
||||
}
|
||||
|
||||
export interface OrderStatus {
|
||||
count: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface OrderSummary {
|
||||
statuses: OrderStatus[];
|
||||
total: number;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface OrderTrend {
|
||||
checkCount: number;
|
||||
movementCount: number;
|
||||
receiptCount: number;
|
||||
shipmentCount: number;
|
||||
time: number | string;
|
||||
}
|
||||
|
||||
export interface InventoryRankItem {
|
||||
id: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface InventorySummary {
|
||||
goodsShareList: InventoryRankItem[];
|
||||
totalQuantity: number;
|
||||
warehouseDistributionList: InventoryRankItem[];
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrderSummary(params?: WmsHomeStatisticsApi.StatisticsReq) {
|
||||
return requestClient.get<WmsHomeStatisticsApi.OrderSummary[]>(
|
||||
'/wms/home-statistics/order-summary',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getOrderTrend(
|
||||
days?: number,
|
||||
params?: WmsHomeStatisticsApi.StatisticsReq,
|
||||
) {
|
||||
return requestClient.get<WmsHomeStatisticsApi.OrderTrend[]>(
|
||||
'/wms/home-statistics/order-trend',
|
||||
{ params: { ...params, days } },
|
||||
);
|
||||
}
|
||||
|
||||
export function getInventorySummary(
|
||||
params?: WmsHomeStatisticsApi.StatisticsReq,
|
||||
) {
|
||||
return requestClient.get<WmsHomeStatisticsApi.InventorySummary>(
|
||||
'/wms/home-statistics/inventory-summary',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsInventoryHistoryApi {
|
||||
/** WMS 库存记录 */
|
||||
export interface InventoryHistory {
|
||||
id?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
quantity?: number;
|
||||
beforeQuantity?: number;
|
||||
afterQuantity?: number;
|
||||
price?: number;
|
||||
totalPrice?: number;
|
||||
remark?: string;
|
||||
orderId?: number;
|
||||
orderNo?: string;
|
||||
orderType?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询库存记录分页 */
|
||||
export function getInventoryHistoryPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsInventoryHistoryApi.InventoryHistory>>(
|
||||
'/wms/inventory-history/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsInventoryApi {
|
||||
/** WMS 库存统计 */
|
||||
export interface Inventory {
|
||||
id?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
quantity?: number;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** WMS 库存统计列表请求 */
|
||||
export interface InventoryListReq {
|
||||
warehouseId: number;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询库存统计分页 */
|
||||
export function getInventoryPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsInventoryApi.Inventory>>(
|
||||
'/wms/inventory/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询库存统计列表 */
|
||||
export function getInventoryList(params: WmsInventoryApi.InventoryListReq) {
|
||||
return requestClient.get<WmsInventoryApi.Inventory[]>('/wms/inventory/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsItemBrandApi {
|
||||
/** WMS 商品品牌 */
|
||||
export interface ItemBrand {
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询商品品牌分页 */
|
||||
export function getItemBrandPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsItemBrandApi.ItemBrand>>(
|
||||
'/wms/item-brand/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询商品品牌精简列表 */
|
||||
export function getItemBrandSimpleList() {
|
||||
return requestClient.get<WmsItemBrandApi.ItemBrand[]>(
|
||||
'/wms/item-brand/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询商品品牌详情 */
|
||||
export function getItemBrand(id: number) {
|
||||
return requestClient.get<WmsItemBrandApi.ItemBrand>(
|
||||
`/wms/item-brand/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增商品品牌 */
|
||||
export function createItemBrand(data: WmsItemBrandApi.ItemBrand) {
|
||||
return requestClient.post('/wms/item-brand/create', data);
|
||||
}
|
||||
|
||||
/** 修改商品品牌 */
|
||||
export function updateItemBrand(data: WmsItemBrandApi.ItemBrand) {
|
||||
return requestClient.put('/wms/item-brand/update', data);
|
||||
}
|
||||
|
||||
/** 删除商品品牌 */
|
||||
export function deleteItemBrand(id: number) {
|
||||
return requestClient.delete(`/wms/item-brand/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 导出商品品牌 */
|
||||
export function exportItemBrand(params: any) {
|
||||
return requestClient.download('/wms/item-brand/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsItemCategoryApi {
|
||||
/** WMS 商品分类 */
|
||||
export interface ItemCategory {
|
||||
id?: number;
|
||||
parentId?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
createTime?: Date;
|
||||
children?: ItemCategory[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询商品分类列表 */
|
||||
export function getItemCategoryList(params?: any) {
|
||||
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
|
||||
'/wms/item-category/list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询商品分类精简列表 */
|
||||
export function getItemCategorySimpleList() {
|
||||
return requestClient.get<WmsItemCategoryApi.ItemCategory[]>(
|
||||
'/wms/item-category/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询商品分类详情 */
|
||||
export function getItemCategory(id: number) {
|
||||
return requestClient.get<WmsItemCategoryApi.ItemCategory>(
|
||||
`/wms/item-category/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增商品分类 */
|
||||
export function createItemCategory(data: WmsItemCategoryApi.ItemCategory) {
|
||||
return requestClient.post('/wms/item-category/create', data);
|
||||
}
|
||||
|
||||
/** 修改商品分类 */
|
||||
export function updateItemCategory(data: WmsItemCategoryApi.ItemCategory) {
|
||||
return requestClient.put('/wms/item-category/update', data);
|
||||
}
|
||||
|
||||
/** 删除商品分类 */
|
||||
export function deleteItemCategory(id: number) {
|
||||
return requestClient.delete(`/wms/item-category/delete?id=${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { WmsItemSkuApi } from './sku';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsItemApi {
|
||||
/** WMS 商品 */
|
||||
export interface Item {
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
categoryId?: number;
|
||||
categoryName?: string;
|
||||
unit?: string;
|
||||
brandId?: number;
|
||||
brandName?: string;
|
||||
remark?: string;
|
||||
skus?: WmsItemSkuApi.ItemSku[];
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询商品分页 */
|
||||
export function getItemPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsItemApi.Item>>('/wms/item/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询商品精简列表 */
|
||||
export function getItemSimpleList(params?: any) {
|
||||
return requestClient.get<WmsItemApi.Item[]>('/wms/item/simple-list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询商品详情 */
|
||||
export function getItem(id: number) {
|
||||
return requestClient.get<WmsItemApi.Item>(`/wms/item/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增商品 */
|
||||
export function createItem(data: WmsItemApi.Item) {
|
||||
return requestClient.post('/wms/item/create', data);
|
||||
}
|
||||
|
||||
/** 修改商品 */
|
||||
export function updateItem(data: WmsItemApi.Item) {
|
||||
return requestClient.put('/wms/item/update', data);
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
export function deleteItem(id: number) {
|
||||
return requestClient.delete(`/wms/item/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 导出商品 */
|
||||
export function exportItem(params: any) {
|
||||
return requestClient.download('/wms/item/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsItemSkuApi {
|
||||
/** WMS 商品 SKU */
|
||||
export interface ItemSku {
|
||||
id?: number;
|
||||
name?: string;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
categoryId?: number;
|
||||
categoryName?: string;
|
||||
unit?: string;
|
||||
brandId?: number;
|
||||
brandName?: string;
|
||||
barCode?: string;
|
||||
code?: string;
|
||||
length?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
grossWeight?: number;
|
||||
netWeight?: number;
|
||||
costPrice?: number;
|
||||
sellingPrice?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 SKU 维度分页(支持商品 / 品牌 / 分类多表联查筛选) */
|
||||
export function getItemSkuPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsItemSkuApi.ItemSku>>(
|
||||
'/wms/item-sku/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsMerchantApi {
|
||||
/** WMS 往来企业 */
|
||||
export interface Merchant {
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
type?: number;
|
||||
level?: string;
|
||||
bankName?: string;
|
||||
bankAccount?: string;
|
||||
address?: string;
|
||||
mobile?: string;
|
||||
telephone?: string;
|
||||
contact?: string;
|
||||
email?: string;
|
||||
remark?: string;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
/** WMS 往来企业精简列表请求 */
|
||||
export interface MerchantSimpleListReq {
|
||||
types?: number[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询往来企业分页 */
|
||||
export function getMerchantPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsMerchantApi.Merchant>>(
|
||||
'/wms/merchant/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询往来企业精简列表 */
|
||||
export function getMerchantSimpleList(
|
||||
params?: WmsMerchantApi.MerchantSimpleListReq,
|
||||
) {
|
||||
return requestClient.get<WmsMerchantApi.Merchant[]>(
|
||||
'/wms/merchant/simple-list',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询往来企业详情 */
|
||||
export function getMerchant(id: number) {
|
||||
return requestClient.get<WmsMerchantApi.Merchant>(
|
||||
`/wms/merchant/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增往来企业 */
|
||||
export function createMerchant(data: WmsMerchantApi.Merchant) {
|
||||
return requestClient.post('/wms/merchant/create', data);
|
||||
}
|
||||
|
||||
/** 修改往来企业 */
|
||||
export function updateMerchant(data: WmsMerchantApi.Merchant) {
|
||||
return requestClient.put('/wms/merchant/update', data);
|
||||
}
|
||||
|
||||
/** 删除往来企业 */
|
||||
export function deleteMerchant(id: number) {
|
||||
return requestClient.delete(`/wms/merchant/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 导出往来企业 */
|
||||
export function exportMerchant(params: any) {
|
||||
return requestClient.download('/wms/merchant/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsWarehouseApi {
|
||||
/** WMS 仓库 */
|
||||
export interface Warehouse {
|
||||
id?: number;
|
||||
code?: string;
|
||||
name?: string;
|
||||
remark?: string;
|
||||
sort?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询仓库分页 */
|
||||
export function getWarehousePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsWarehouseApi.Warehouse>>(
|
||||
'/wms/warehouse/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询仓库精简列表 */
|
||||
export function getWarehouseSimpleList() {
|
||||
return requestClient.get<WmsWarehouseApi.Warehouse[]>(
|
||||
'/wms/warehouse/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询仓库详情 */
|
||||
export function getWarehouse(id: number) {
|
||||
return requestClient.get<WmsWarehouseApi.Warehouse>(
|
||||
`/wms/warehouse/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增仓库 */
|
||||
export function createWarehouse(data: WmsWarehouseApi.Warehouse) {
|
||||
return requestClient.post('/wms/warehouse/create', data);
|
||||
}
|
||||
|
||||
/** 修改仓库 */
|
||||
export function updateWarehouse(data: WmsWarehouseApi.Warehouse) {
|
||||
return requestClient.put('/wms/warehouse/update', data);
|
||||
}
|
||||
|
||||
/** 删除仓库 */
|
||||
export function deleteWarehouse(id: number) {
|
||||
return requestClient.delete(`/wms/warehouse/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 导出仓库 */
|
||||
export function exportWarehouse(params: any) {
|
||||
return requestClient.download('/wms/warehouse/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export namespace WmsCheckOrderDetailApi {
|
||||
/** WMS 盘库单明细 */
|
||||
export interface CheckOrderDetail {
|
||||
id?: number;
|
||||
orderId?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
inventoryId?: number;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
receiptTime?: Date;
|
||||
quantity?: number;
|
||||
checkQuantity?: number;
|
||||
availableQuantity?: number;
|
||||
price?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { WmsCheckOrderDetailApi } from './detail';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsCheckOrderApi {
|
||||
/** WMS 盘库单 */
|
||||
export interface CheckOrder {
|
||||
id?: number;
|
||||
no?: string;
|
||||
orderTime?: string;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
totalQuantity?: number;
|
||||
totalPrice?: number;
|
||||
actualPrice?: number;
|
||||
details?: WmsCheckOrderDetailApi.CheckOrderDetail[];
|
||||
createTime?: Date;
|
||||
creator?: string;
|
||||
creatorName?: string;
|
||||
updateTime?: Date;
|
||||
updater?: string;
|
||||
updaterName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCheckOrderPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsCheckOrderApi.CheckOrder>>(
|
||||
'/wms/check-order/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getCheckOrder(id: number) {
|
||||
return requestClient.get<WmsCheckOrderApi.CheckOrder>(
|
||||
`/wms/check-order/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getCheckOrderDetailListByOrderId(orderId: number) {
|
||||
return requestClient.get<WmsCheckOrderDetailApi.CheckOrderDetail[]>(
|
||||
`/wms/check-order-detail/list-by-order-id?orderId=${orderId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
|
||||
return requestClient.post('/wms/check-order/create', data);
|
||||
}
|
||||
|
||||
export function updateCheckOrder(data: WmsCheckOrderApi.CheckOrder) {
|
||||
return requestClient.put('/wms/check-order/update', data);
|
||||
}
|
||||
|
||||
export function completeCheckOrder(id: number) {
|
||||
return requestClient.put(`/wms/check-order/complete?id=${id}`);
|
||||
}
|
||||
|
||||
export function cancelCheckOrder(id: number) {
|
||||
return requestClient.put(`/wms/check-order/cancel?id=${id}`);
|
||||
}
|
||||
|
||||
export function deleteCheckOrder(id: number) {
|
||||
return requestClient.delete(`/wms/check-order/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function exportCheckOrder(params: any) {
|
||||
return requestClient.download('/wms/check-order/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
export namespace WmsMovementOrderDetailApi {
|
||||
/** WMS 移库单明细 */
|
||||
export interface MovementOrderDetail {
|
||||
id?: number;
|
||||
orderId?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
sourceWarehouseId?: number;
|
||||
sourceWarehouseName?: string;
|
||||
targetWarehouseId?: number;
|
||||
targetWarehouseName?: string;
|
||||
quantity?: number;
|
||||
availableQuantity?: number;
|
||||
price?: number;
|
||||
totalPrice?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { WmsMovementOrderDetailApi } from './detail';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsMovementOrderApi {
|
||||
/** WMS 移库单 */
|
||||
export interface MovementOrder {
|
||||
id?: number;
|
||||
no?: string;
|
||||
orderTime?: string;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
sourceWarehouseId?: number;
|
||||
sourceWarehouseName?: string;
|
||||
targetWarehouseId?: number;
|
||||
targetWarehouseName?: string;
|
||||
totalQuantity?: number;
|
||||
totalPrice?: number;
|
||||
details?: WmsMovementOrderDetailApi.MovementOrderDetail[];
|
||||
createTime?: Date;
|
||||
creator?: string;
|
||||
creatorName?: string;
|
||||
updateTime?: Date;
|
||||
updater?: string;
|
||||
updaterName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMovementOrderPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsMovementOrderApi.MovementOrder>>(
|
||||
'/wms/movement-order/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getMovementOrder(id: number) {
|
||||
return requestClient.get<WmsMovementOrderApi.MovementOrder>(
|
||||
`/wms/movement-order/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getMovementOrderDetailListByOrderId(orderId: number) {
|
||||
return requestClient.get<WmsMovementOrderDetailApi.MovementOrderDetail[]>(
|
||||
`/wms/movement-order-detail/list-by-order-id?orderId=${orderId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
|
||||
return requestClient.post('/wms/movement-order/create', data);
|
||||
}
|
||||
|
||||
export function updateMovementOrder(data: WmsMovementOrderApi.MovementOrder) {
|
||||
return requestClient.put('/wms/movement-order/update', data);
|
||||
}
|
||||
|
||||
export function completeMovementOrder(id: number) {
|
||||
return requestClient.put(`/wms/movement-order/complete?id=${id}`);
|
||||
}
|
||||
|
||||
export function cancelMovementOrder(id: number) {
|
||||
return requestClient.put(`/wms/movement-order/cancel?id=${id}`);
|
||||
}
|
||||
|
||||
export function deleteMovementOrder(id: number) {
|
||||
return requestClient.delete(`/wms/movement-order/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function exportMovementOrder(params: any) {
|
||||
return requestClient.download('/wms/movement-order/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export namespace WmsReceiptOrderDetailApi {
|
||||
/** WMS 入库单明细 */
|
||||
export interface ReceiptOrderDetail {
|
||||
id?: number;
|
||||
orderId?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
quantity?: number;
|
||||
price?: number;
|
||||
totalPrice?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { WmsReceiptOrderDetailApi } from './detail';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsReceiptOrderApi {
|
||||
/** WMS 入库单 */
|
||||
export interface ReceiptOrder {
|
||||
id?: number;
|
||||
no?: string;
|
||||
type?: number;
|
||||
orderTime?: string;
|
||||
status?: number;
|
||||
bizOrderNo?: string;
|
||||
merchantId?: number;
|
||||
merchantName?: string;
|
||||
remark?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
totalQuantity?: number;
|
||||
totalPrice?: number;
|
||||
details?: WmsReceiptOrderDetailApi.ReceiptOrderDetail[];
|
||||
createTime?: Date;
|
||||
creator?: string;
|
||||
creatorName?: string;
|
||||
updateTime?: Date;
|
||||
updater?: string;
|
||||
updaterName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getReceiptOrderPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsReceiptOrderApi.ReceiptOrder>>(
|
||||
'/wms/receipt-order/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getReceiptOrder(id: number) {
|
||||
return requestClient.get<WmsReceiptOrderApi.ReceiptOrder>(
|
||||
`/wms/receipt-order/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getReceiptOrderDetailListByOrderId(orderId: number) {
|
||||
return requestClient.get<WmsReceiptOrderDetailApi.ReceiptOrderDetail[]>(
|
||||
`/wms/receipt-order-detail/list-by-order-id?orderId=${orderId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
return requestClient.post('/wms/receipt-order/create', data);
|
||||
}
|
||||
|
||||
export function updateReceiptOrder(data: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
return requestClient.put('/wms/receipt-order/update', data);
|
||||
}
|
||||
|
||||
export function completeReceiptOrder(id: number) {
|
||||
return requestClient.put(`/wms/receipt-order/complete?id=${id}`);
|
||||
}
|
||||
|
||||
export function cancelReceiptOrder(id: number) {
|
||||
return requestClient.put(`/wms/receipt-order/cancel?id=${id}`);
|
||||
}
|
||||
|
||||
export function deleteReceiptOrder(id: number) {
|
||||
return requestClient.delete(`/wms/receipt-order/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function exportReceiptOrder(params: any) {
|
||||
return requestClient.download('/wms/receipt-order/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
export namespace WmsShipmentOrderDetailApi {
|
||||
/** WMS 出库单明细 */
|
||||
export interface ShipmentOrderDetail {
|
||||
id?: number;
|
||||
orderId?: number;
|
||||
itemId?: number;
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
unit?: string;
|
||||
skuId?: number;
|
||||
skuCode?: string;
|
||||
skuName?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
quantity?: number;
|
||||
availableQuantity?: number;
|
||||
price?: number;
|
||||
totalPrice?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { WmsShipmentOrderDetailApi } from './detail';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace WmsShipmentOrderApi {
|
||||
/** WMS 出库单 */
|
||||
export interface ShipmentOrder {
|
||||
id?: number;
|
||||
no?: string;
|
||||
type?: number;
|
||||
orderTime?: string;
|
||||
status?: number;
|
||||
bizOrderNo?: string;
|
||||
merchantId?: number;
|
||||
merchantName?: string;
|
||||
remark?: string;
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
totalQuantity?: number;
|
||||
totalPrice?: number;
|
||||
details?: WmsShipmentOrderDetailApi.ShipmentOrderDetail[];
|
||||
createTime?: Date;
|
||||
creator?: string;
|
||||
creatorName?: string;
|
||||
updateTime?: Date;
|
||||
updater?: string;
|
||||
updaterName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function getShipmentOrderPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<WmsShipmentOrderApi.ShipmentOrder>>(
|
||||
'/wms/shipment-order/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
export function getShipmentOrder(id: number) {
|
||||
return requestClient.get<WmsShipmentOrderApi.ShipmentOrder>(
|
||||
`/wms/shipment-order/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getShipmentOrderDetailListByOrderId(orderId: number) {
|
||||
return requestClient.get<WmsShipmentOrderDetailApi.ShipmentOrderDetail[]>(
|
||||
`/wms/shipment-order-detail/list-by-order-id?orderId=${orderId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
return requestClient.post('/wms/shipment-order/create', data);
|
||||
}
|
||||
|
||||
export function updateShipmentOrder(data: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
return requestClient.put('/wms/shipment-order/update', data);
|
||||
}
|
||||
|
||||
export function completeShipmentOrder(id: number) {
|
||||
return requestClient.put(`/wms/shipment-order/complete?id=${id}`);
|
||||
}
|
||||
|
||||
export function cancelShipmentOrder(id: number) {
|
||||
return requestClient.put(`/wms/shipment-order/cancel?id=${id}`);
|
||||
}
|
||||
|
||||
export function deleteShipmentOrder(id: number) {
|
||||
return requestClient.delete(`/wms/shipment-order/delete?id=${id}`);
|
||||
}
|
||||
|
||||
export function exportShipmentOrder(params: any) {
|
||||
return requestClient.download('/wms/shipment-order/export-excel', { params });
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import NumberRangeInput from './number-range-input.vue';
|
||||
|
||||
export { default as NumberRangeInput } from './number-range-input.vue';
|
||||
|
||||
export type NumberRangeValue = [number | undefined, number | undefined];
|
||||
|
||||
function splitNumberRange(minFieldName: string, maxFieldName: string) {
|
||||
return (
|
||||
value: NumberRangeValue | undefined,
|
||||
setValue: (fieldName: string, value: number | undefined) => void,
|
||||
) => {
|
||||
setValue(minFieldName, value?.[0]);
|
||||
setValue(maxFieldName, value?.[1]);
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function buildNumberRangeSchema(
|
||||
label: string,
|
||||
fieldName: string,
|
||||
minFieldName: string,
|
||||
maxFieldName: string,
|
||||
precision: number,
|
||||
): VbenFormSchema {
|
||||
return {
|
||||
component: markRaw(NumberRangeInput),
|
||||
componentProps: {
|
||||
min: 0,
|
||||
precision,
|
||||
},
|
||||
fieldName,
|
||||
label,
|
||||
valueFormat: splitNumberRange(minFieldName, maxFieldName),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts" setup>
|
||||
import { InputNumber } from 'antdv-next';
|
||||
|
||||
type NumberRangeValue = [number | undefined, number | undefined];
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
maxPlaceholder?: string;
|
||||
min?: number;
|
||||
minPlaceholder?: string;
|
||||
precision?: number;
|
||||
value?: NumberRangeValue;
|
||||
}>(),
|
||||
{
|
||||
maxPlaceholder: '最大值',
|
||||
min: undefined,
|
||||
minPlaceholder: '最小值',
|
||||
precision: 2,
|
||||
value: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:value': [value: NumberRangeValue | undefined];
|
||||
}>();
|
||||
|
||||
function normalizeValue(value: unknown) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() !== '') {
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) ? numberValue : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function updateValue(index: 0 | 1, value: unknown) {
|
||||
const next: NumberRangeValue = [
|
||||
props.value?.[0] ?? undefined,
|
||||
props.value?.[1] ?? undefined,
|
||||
];
|
||||
next[index] = normalizeValue(value);
|
||||
emit(
|
||||
'update:value',
|
||||
next[0] === undefined && next[1] === undefined ? undefined : next,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<InputNumber
|
||||
:controls="false"
|
||||
:min="min"
|
||||
:placeholder="minPlaceholder"
|
||||
:precision="precision"
|
||||
:value="value?.[0]"
|
||||
class="min-w-0 flex-1"
|
||||
@update:value="updateValue(0, $event)"
|
||||
/>
|
||||
<span class="shrink-0 text-muted-foreground">至</span>
|
||||
<InputNumber
|
||||
:controls="false"
|
||||
:min="min"
|
||||
:placeholder="maxPlaceholder"
|
||||
:precision="precision"
|
||||
:value="value?.[1]"
|
||||
class="min-w-0 flex-1"
|
||||
@update:value="updateValue(1, $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Button, Card } from 'antdv-next';
|
||||
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
|
||||
import WmsHomeInventoryCharts from './modules/wms-home-inventory-charts.vue';
|
||||
import WmsHomeOrderSummaryCards from './modules/wms-home-order-summary-cards.vue';
|
||||
import WmsHomeOrderTrendChart from './modules/wms-home-order-trend-chart.vue';
|
||||
|
||||
defineOptions({ name: 'WmsHome' });
|
||||
|
||||
const loading = ref(false);
|
||||
const warehouseId = ref<number>();
|
||||
const statTime = ref(formatDateTime(new Date()));
|
||||
const orderSummaryCardsRef =
|
||||
ref<InstanceType<typeof WmsHomeOrderSummaryCards>>();
|
||||
const orderTrendChartRef = ref<InstanceType<typeof WmsHomeOrderTrendChart>>();
|
||||
const inventoryChartsRef = ref<InstanceType<typeof WmsHomeInventoryCharts>>();
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
orderSummaryCardsRef.value?.load(warehouseId.value),
|
||||
orderTrendChartRef.value?.load(warehouseId.value),
|
||||
inventoryChartsRef.value?.load(warehouseId.value),
|
||||
]);
|
||||
statTime.value = formatDateTime(new Date());
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="WMS 手册(功能开启)"
|
||||
url="https://doc.iocoder.cn/wms/build/"
|
||||
/>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Card :body-style="{ padding: '16px' }">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-xl font-semibold">WMS 首页</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
单据工作台 / 库存概览
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<WmsWarehouseSelect
|
||||
v-model="warehouseId"
|
||||
class="!w-[220px]"
|
||||
placeholder="全部仓库"
|
||||
@change="refresh"
|
||||
/>
|
||||
<Button :loading="loading" @click="refresh">刷新</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<WmsHomeOrderSummaryCards ref="orderSummaryCardsRef" />
|
||||
<WmsHomeOrderTrendChart ref="orderTrendChartRef" />
|
||||
<WmsHomeInventoryCharts ref="inventoryChartsRef" />
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
统计时间:{{ statTime }}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import type { EChartsOption } from '@vben/plugins/echarts';
|
||||
|
||||
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[],
|
||||
): EChartsOption {
|
||||
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}<br/>库存:{c} ({d}%)',
|
||||
trigger: 'item',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 库存分布图表配置 */
|
||||
export function getWarehouseDistributionChartOptions(
|
||||
warehouseDistributionList: InventoryChartItem[],
|
||||
): EChartsOption {
|
||||
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 }) => 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 || '-'}<br/>库存:${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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import type { InventoryChartItem } from './wms-home-inventory-chart-options';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Card } from 'antdv-next';
|
||||
|
||||
import { getInventorySummary } from '#/api/wms/home';
|
||||
|
||||
import {
|
||||
buildInventoryChartItemList,
|
||||
formatQuantityText,
|
||||
getGoodsShareChartOptions,
|
||||
getWarehouseDistributionChartOptions,
|
||||
} from './wms-home-inventory-chart-options';
|
||||
|
||||
defineOptions({ name: 'WmsHomeInventoryCharts' });
|
||||
|
||||
const GOODS_SHARE_LIMIT = 5;
|
||||
const WAREHOUSE_DISTRIBUTION_LIMIT = 8;
|
||||
|
||||
const loading = ref(false);
|
||||
const totalQuantity = ref(0);
|
||||
const goodsShareList = ref<InventoryChartItem[]>([]);
|
||||
const warehouseDistributionList = ref<InventoryChartItem[]>([]);
|
||||
const goodsShareChartRef = ref<EchartsUIType>();
|
||||
const warehouseDistributionChartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts: renderGoodsShareEcharts } =
|
||||
useEcharts(goodsShareChartRef);
|
||||
const { renderEcharts: renderWarehouseDistributionEcharts } = useEcharts(
|
||||
warehouseDistributionChartRef,
|
||||
);
|
||||
|
||||
/** 使用最新库存汇总数据渲染首页库存图表 */
|
||||
async function renderCharts() {
|
||||
await nextTick();
|
||||
await Promise.all([
|
||||
renderGoodsShareEcharts(getGoodsShareChartOptions(goodsShareList.value)),
|
||||
renderWarehouseDistributionEcharts(
|
||||
getWarehouseDistributionChartOptions(warehouseDistributionList.value),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/** 加载指定仓库的库存汇总和排行数据 */
|
||||
async function load(warehouseId?: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getInventorySummary({
|
||||
...(warehouseId ? { warehouseId } : {}),
|
||||
goodsLimit: GOODS_SHARE_LIMIT,
|
||||
warehouseLimit: WAREHOUSE_DISTRIBUTION_LIMIT,
|
||||
});
|
||||
totalQuantity.value = Number(data.totalQuantity || 0);
|
||||
goodsShareList.value = buildInventoryChartItemList(
|
||||
data.goodsShareList,
|
||||
'未命名商品',
|
||||
);
|
||||
warehouseDistributionList.value = buildInventoryChartItemList(
|
||||
data.warehouseDistributionList,
|
||||
'未指定仓库',
|
||||
);
|
||||
await renderCharts();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ load });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-4 max-lg:grid-cols-1">
|
||||
<Card :body-style="{ padding: '12px 16px 16px' }">
|
||||
<div class="mb-3">
|
||||
<div class="font-semibold">货物占比</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
按商品库存数量汇总 Top 5
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative min-h-[300px]">
|
||||
<EchartsUI ref="goodsShareChartRef" height="300px" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex items-center justify-center bg-card/70 text-sm text-muted-foreground"
|
||||
>
|
||||
加载中
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card :body-style="{ padding: '12px 16px 16px' }">
|
||||
<div class="mb-3 flex justify-between">
|
||||
<div>
|
||||
<div class="font-semibold">库存分布</div>
|
||||
<div class="text-sm text-muted-foreground">按仓库库存数量汇总</div>
|
||||
</div>
|
||||
<span class="font-semibold">
|
||||
总库存 {{ formatQuantityText(totalQuantity) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative min-h-[300px]">
|
||||
<EchartsUI ref="warehouseDistributionChartRef" height="300px" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex items-center justify-center bg-card/70 text-sm text-muted-foreground"
|
||||
>
|
||||
加载中
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsHomeStatisticsApi } from '#/api/wms/home';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DICT_TYPE, OrderStatusEnum, OrderTypeEnum } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
import { Button, Card, message } from 'antdv-next';
|
||||
|
||||
import { getOrderSummary } from '#/api/wms/home';
|
||||
|
||||
defineOptions({ name: 'WmsHomeOrderSummaryCards' });
|
||||
|
||||
interface StatusItem {
|
||||
color: string;
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface OrderSummaryItem {
|
||||
color: string;
|
||||
routeName: string;
|
||||
statuses?: WmsHomeStatisticsApi.OrderStatus[];
|
||||
title: string;
|
||||
total?: number;
|
||||
type: number;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const orderDefinitions: OrderSummaryItem[] = [
|
||||
{
|
||||
color: '#2f7df6',
|
||||
routeName: 'WmsReceiptOrder',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.RECEIPT,
|
||||
).replace(/单$/, ''),
|
||||
type: OrderTypeEnum.RECEIPT,
|
||||
},
|
||||
{
|
||||
color: '#18a058',
|
||||
routeName: 'WmsShipmentOrder',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.SHIPMENT,
|
||||
).replace(/单$/, ''),
|
||||
type: OrderTypeEnum.SHIPMENT,
|
||||
},
|
||||
{
|
||||
color: '#f59e0b',
|
||||
routeName: 'WmsMovementOrder',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.MOVEMENT,
|
||||
).replace(/单$/, ''),
|
||||
type: OrderTypeEnum.MOVEMENT,
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
routeName: 'WmsCheckOrder',
|
||||
title: getDictLabel(DICT_TYPE.WMS_ORDER_TYPE, OrderTypeEnum.CHECK).replace(
|
||||
/单$/,
|
||||
'',
|
||||
),
|
||||
type: OrderTypeEnum.CHECK,
|
||||
},
|
||||
];
|
||||
|
||||
const statusList: StatusItem[] = [
|
||||
{
|
||||
color: '#409eff',
|
||||
label:
|
||||
getDictLabel(DICT_TYPE.WMS_ORDER_STATUS, OrderStatusEnum.PREPARE) ||
|
||||
'草稿',
|
||||
value: OrderStatusEnum.PREPARE,
|
||||
},
|
||||
{
|
||||
color: '#67c23a',
|
||||
label:
|
||||
getDictLabel(DICT_TYPE.WMS_ORDER_STATUS, OrderStatusEnum.FINISHED) ||
|
||||
'已完成',
|
||||
value: OrderStatusEnum.FINISHED,
|
||||
},
|
||||
{
|
||||
color: '#909399',
|
||||
label:
|
||||
getDictLabel(DICT_TYPE.WMS_ORDER_STATUS, OrderStatusEnum.CANCELED) ||
|
||||
'已作废',
|
||||
value: OrderStatusEnum.CANCELED,
|
||||
},
|
||||
];
|
||||
|
||||
const loading = ref(false);
|
||||
const summaryList = ref<OrderSummaryItem[]>(orderDefinitions);
|
||||
|
||||
function getStatusCount(item: OrderSummaryItem, status: number) {
|
||||
return item.statuses?.find((row) => row.status === status)?.count ?? 0;
|
||||
}
|
||||
|
||||
function getStatusPercent(item: OrderSummaryItem, status: number) {
|
||||
const total = item.total ?? 0;
|
||||
if (total <= 0) {
|
||||
return '0%';
|
||||
}
|
||||
return `${(getStatusCount(item, status) / total) * 100}%`;
|
||||
}
|
||||
|
||||
async function handleNavigate(routeName: string) {
|
||||
try {
|
||||
await router.push({ name: routeName });
|
||||
} catch {
|
||||
message.warning('当前菜单尚未加载,请从左侧菜单进入对应页面');
|
||||
}
|
||||
}
|
||||
|
||||
async function load(warehouseId?: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getOrderSummary(warehouseId ? { warehouseId } : {});
|
||||
summaryList.value = orderDefinitions.map((definition) => {
|
||||
const summary = data.find((item) => item.type === definition.type);
|
||||
return {
|
||||
...definition,
|
||||
statuses: summary?.statuses,
|
||||
total: summary?.total,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ load });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-4 gap-4 max-xl:grid-cols-2 max-sm:grid-cols-1">
|
||||
<Card
|
||||
v-for="item in summaryList"
|
||||
:key="item.type"
|
||||
:body-style="{ padding: '12px 16px 16px' }"
|
||||
class="h-full shadow-sm"
|
||||
:loading="loading"
|
||||
:style="{ borderTop: `3px solid ${item.color}` }"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 font-semibold">
|
||||
<span
|
||||
class="h-2 w-2 rounded-full"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
></span>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleNavigate(item.routeName)"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mb-1 flex items-baseline gap-2">
|
||||
<span class="text-muted-foreground">合计</span>
|
||||
<span class="text-3xl font-bold leading-9">
|
||||
{{ item.total?.toLocaleString() ?? 0 }}
|
||||
</span>
|
||||
<span class="text-muted-foreground">单</span>
|
||||
</div>
|
||||
<div class="my-3 flex h-2 overflow-hidden rounded-full bg-muted">
|
||||
<span
|
||||
v-for="status in statusList"
|
||||
:key="status.value"
|
||||
class="h-full"
|
||||
:style="{
|
||||
backgroundColor: status.color,
|
||||
width: getStatusPercent(item, status.value),
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
||||
<div v-for="status in statusList" :key="status.value">
|
||||
<div class="truncate text-muted-foreground">{{ status.label }}</div>
|
||||
<div class="font-semibold" :style="{ color: status.color }">
|
||||
{{ getStatusCount(item, status.value).toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import type { EChartsOption } from '@vben/plugins/echarts';
|
||||
|
||||
import type { WmsHomeStatisticsApi } from '#/api/wms/home';
|
||||
|
||||
import { DICT_TYPE, OrderTypeEnum } 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 orderDefinitions: OrderDefinition[] = [
|
||||
{
|
||||
color: '#2f7df6',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.RECEIPT,
|
||||
).replace(/单$/, ''),
|
||||
trendField: 'receiptCount',
|
||||
type: OrderTypeEnum.RECEIPT,
|
||||
},
|
||||
{
|
||||
color: '#18a058',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.SHIPMENT,
|
||||
).replace(/单$/, ''),
|
||||
trendField: 'shipmentCount',
|
||||
type: OrderTypeEnum.SHIPMENT,
|
||||
},
|
||||
{
|
||||
color: '#f59e0b',
|
||||
title: getDictLabel(
|
||||
DICT_TYPE.WMS_ORDER_TYPE,
|
||||
OrderTypeEnum.MOVEMENT,
|
||||
).replace(/单$/, ''),
|
||||
trendField: 'movementCount',
|
||||
type: OrderTypeEnum.MOVEMENT,
|
||||
},
|
||||
{
|
||||
color: '#7c3aed',
|
||||
title: getDictLabel(DICT_TYPE.WMS_ORDER_TYPE, OrderTypeEnum.CHECK).replace(
|
||||
/单$/,
|
||||
'',
|
||||
),
|
||||
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[],
|
||||
): EChartsOption {
|
||||
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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import type { WmsHomeStatisticsApi } from '#/api/wms/home';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import { Card, Segmented } from 'antdv-next';
|
||||
|
||||
import { getOrderTrend } from '#/api/wms/home';
|
||||
|
||||
import { getOrderTrendChartOptions } from './wms-home-order-trend-chart-options';
|
||||
|
||||
defineOptions({ name: 'WmsHomeOrderTrendChart' });
|
||||
|
||||
const loading = ref(false);
|
||||
const warehouseId = ref<number>();
|
||||
const trendDays = ref(7);
|
||||
const trendList = ref<WmsHomeStatisticsApi.OrderTrend[]>([]);
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
const trendDayOptions = [
|
||||
{ label: '近7天', value: 7 },
|
||||
{ label: '近30天', value: 30 },
|
||||
];
|
||||
|
||||
/** 使用最新趋势数据渲染单据趋势图 */
|
||||
async function renderChart() {
|
||||
await nextTick();
|
||||
await renderEcharts(getOrderTrendChartOptions(trendList.value));
|
||||
}
|
||||
|
||||
/** 加载指定仓库的单据趋势数据 */
|
||||
async function load(selectedWarehouseId?: number) {
|
||||
warehouseId.value = selectedWarehouseId;
|
||||
loading.value = true;
|
||||
try {
|
||||
trendList.value = await getOrderTrend(
|
||||
trendDays.value,
|
||||
selectedWarehouseId ? { warehouseId: selectedWarehouseId } : {},
|
||||
);
|
||||
await renderChart();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换趋势统计时间范围并刷新图表 */
|
||||
async function handleTrendDaysChange(value: number | string) {
|
||||
trendDays.value = Number(value);
|
||||
await load(warehouseId.value);
|
||||
}
|
||||
|
||||
defineExpose({ load });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :body-style="{ padding: '12px 16px 16px' }">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="font-semibold">单据趋势</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
入库、出库、移库、盘库单据数量
|
||||
</div>
|
||||
</div>
|
||||
<Segmented
|
||||
:options="trendDayOptions"
|
||||
:value="trendDays"
|
||||
@change="handleTrendDaysChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative min-h-[330px]">
|
||||
<EchartsUI ref="chartRef" height="330px" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute inset-0 flex items-center justify-center bg-card/70 text-sm text-muted-foreground"
|
||||
>
|
||||
加载中
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default as WmsInventorySelect } from './select.vue';
|
||||
export type { InventorySelectRow } from './select.vue';
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsInventoryApi } from '#/api/wms/inventory';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { message, Modal } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getInventoryPage } from '#/api/wms/inventory';
|
||||
import { formatQuantity } from '#/views/wms/utils/format';
|
||||
|
||||
import {
|
||||
useInventorySelectGridColumns,
|
||||
useInventorySelectGridFormSchema,
|
||||
} from '../index/data';
|
||||
|
||||
export interface InventorySelectRow extends WmsInventoryApi.Inventory {
|
||||
availableQuantity?: number;
|
||||
price?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsInventorySelect' });
|
||||
|
||||
const props = defineProps<{
|
||||
warehouseId?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [list: InventorySelectRow[]];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
const disabledInventoryKeys = ref<Set<string>>(new Set());
|
||||
|
||||
interface CellDblclickEvent {
|
||||
row: InventorySelectRow;
|
||||
}
|
||||
|
||||
/** 获得行唯一标识 */
|
||||
function getRowKey(row: InventorySelectRow) {
|
||||
return `inventory-${row.id || `${row.skuId}-${row.warehouseId}`}`;
|
||||
}
|
||||
|
||||
/** 获得业务库存标识 */
|
||||
function getInventoryKey(
|
||||
row: Pick<InventorySelectRow, 'skuId' | 'warehouseId'>,
|
||||
) {
|
||||
return row.skuId && row.warehouseId
|
||||
? `${row.skuId}-${row.warehouseId}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** 判断库存是否可选 */
|
||||
function isInventorySelectable(row: InventorySelectRow) {
|
||||
const key = getInventoryKey(row);
|
||||
return !key || !disabledInventoryKeys.value.has(key);
|
||||
}
|
||||
|
||||
/** 合并当前页和跨页保留的选择记录 */
|
||||
function mergeSelectedRows(rows: InventorySelectRow[]) {
|
||||
const selectedMap = new Map<string, InventorySelectRow>();
|
||||
rows
|
||||
.filter((row) => isInventorySelectable(row))
|
||||
.forEach((row) => selectedMap.set(getRowKey(row), row));
|
||||
return [...selectedMap.values()];
|
||||
}
|
||||
|
||||
/** 获取 VXE 当前页和 reserve 中的完整选择 */
|
||||
function getSelectedRows() {
|
||||
const records =
|
||||
(gridApi.grid.getCheckboxRecords?.() as InventorySelectRow[] | undefined) ||
|
||||
[];
|
||||
const reserves =
|
||||
(gridApi.grid.getCheckboxReserveRecords?.() as
|
||||
| InventorySelectRow[]
|
||||
| undefined) || [];
|
||||
return mergeSelectedRows([...reserves, ...records]);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useInventorySelectGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useInventorySelectGridColumns(),
|
||||
height: 560,
|
||||
keepSource: true,
|
||||
showOverflow: false,
|
||||
checkboxConfig: {
|
||||
checkMethod: ({ row }: { row: InventorySelectRow }) =>
|
||||
isInventorySelectable(row),
|
||||
highlight: true,
|
||||
range: true,
|
||||
reserve: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const data = await getInventoryPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
type: 'warehouse',
|
||||
warehouseId: props.warehouseId,
|
||||
onlyPositiveQuantity: true,
|
||||
...formValues,
|
||||
});
|
||||
return {
|
||||
...data,
|
||||
list: (data.list || []).map((inventory) => ({
|
||||
...inventory,
|
||||
availableQuantity: inventory.quantity,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InventorySelectRow>,
|
||||
gridEvents: {
|
||||
cellDblclick: handleCellDblclick,
|
||||
},
|
||||
});
|
||||
|
||||
async function openModal(selectedInventoryKeys: string[] = []) {
|
||||
if (!props.warehouseId) {
|
||||
message.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
open.value = true;
|
||||
disabledInventoryKeys.value = new Set(selectedInventoryKeys);
|
||||
await nextTick();
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
await gridApi.grid.clearCheckboxReserve();
|
||||
await gridApi.formApi.resetForm();
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
async function closeModal() {
|
||||
open.value = false;
|
||||
disabledInventoryKeys.value = new Set();
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
await gridApi.grid.clearCheckboxReserve();
|
||||
}
|
||||
|
||||
function confirmSelectedRows(rows: InventorySelectRow[]) {
|
||||
const selectedList = mergeSelectedRows(rows);
|
||||
if (selectedList.length === 0) {
|
||||
message.warning('请选择库存');
|
||||
return;
|
||||
}
|
||||
emit('change', selectedList);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
confirmSelectedRows(getSelectedRows());
|
||||
}
|
||||
|
||||
/** 双击行直接选择并确认 */
|
||||
function handleCellDblclick({ row }: CellDblclickEvent) {
|
||||
if (!isInventorySelectable(row)) {
|
||||
message.warning('该库存已添加');
|
||||
return;
|
||||
}
|
||||
confirmSelectedRows(mergeSelectedRows([...getSelectedRows(), row]));
|
||||
}
|
||||
|
||||
defineExpose({ open: openModal });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="open"
|
||||
title="库存选择"
|
||||
width="80%"
|
||||
:destroy-on-close="true"
|
||||
@ok="handleConfirm"
|
||||
@cancel="closeModal"
|
||||
>
|
||||
<Grid table-title="库存列表">
|
||||
<template #itemInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #skuInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #availableQuantity="{ row }">
|
||||
{{ formatQuantity(row.availableQuantity) }}
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getWarehouseSimpleList } from '#/api/wms/md/warehouse';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'orderType',
|
||||
label: '单据类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择单据类型',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'orderNo',
|
||||
label: '单据号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入单据号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getWarehouseSimpleList,
|
||||
labelField: 'name',
|
||||
placeholder: '请选择仓库',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemCode',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuCode',
|
||||
label: '规格编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuName',
|
||||
label: '规格名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '操作时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 库存流水列表字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'orderNo',
|
||||
title: '单据号',
|
||||
width: 180,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
field: 'orderType',
|
||||
title: '单据类型',
|
||||
width: 110,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'itemInfo',
|
||||
title: '商品信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuInfo',
|
||||
title: '规格信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
title: '仓库',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'beforeQuantity',
|
||||
title: '操作前',
|
||||
align: 'right',
|
||||
minWidth: 110,
|
||||
slots: { default: 'beforeQuantity' },
|
||||
},
|
||||
{
|
||||
field: 'afterQuantity',
|
||||
title: '操作后',
|
||||
align: 'right',
|
||||
minWidth: 110,
|
||||
slots: { default: 'afterQuantity' },
|
||||
},
|
||||
{
|
||||
field: 'quantityPrice',
|
||||
title: '数量/金额(元)',
|
||||
minWidth: 180,
|
||||
slots: { default: 'quantityPrice' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '操作时间',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsInventoryHistoryApi } from '#/api/wms/inventory/history';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getInventoryHistoryPage } from '#/api/wms/inventory/history';
|
||||
import { formatPrice, formatQuantity } from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
defineOptions({ name: 'WmsInventoryHistory' });
|
||||
|
||||
function hasValue(value: unknown) {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
showOverflow: false,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getInventoryHistoryPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsInventoryHistoryApi.InventoryHistory>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【库存】库存记录、流水、统计"
|
||||
url="https://doc.iocoder.cn/wms/inventory/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="库存流水">
|
||||
<template #itemInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div class="text-sm">{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="break-all text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #skuInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div class="text-sm">{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="break-all text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #beforeQuantity="{ row }">
|
||||
{{ formatQuantity(row.beforeQuantity) || '-' }}
|
||||
</template>
|
||||
<template #afterQuantity="{ row }">
|
||||
{{ formatQuantity(row.afterQuantity) || '-' }}
|
||||
</template>
|
||||
<template #quantityPrice="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div class="flex justify-between gap-2">
|
||||
<span class="shrink-0">数量:</span>
|
||||
<span>{{ formatQuantity(row.quantity) }}</span>
|
||||
</div>
|
||||
<div v-if="hasValue(row.price)" class="flex justify-between gap-2">
|
||||
<span class="shrink-0">单价:</span>
|
||||
<span>{{ formatPrice(row.price) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasValue(row.totalPrice)"
|
||||
class="flex justify-between gap-2"
|
||||
>
|
||||
<span class="shrink-0">金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getWarehouseSimpleList } from '#/api/wms/md/warehouse';
|
||||
|
||||
export const INVENTORY_DIMENSION = {
|
||||
ITEM: 'item',
|
||||
WAREHOUSE: 'warehouse',
|
||||
} as const;
|
||||
|
||||
export type InventoryDimension =
|
||||
(typeof INVENTORY_DIMENSION)[keyof typeof INVENTORY_DIMENSION];
|
||||
|
||||
const dimensionOptions = [
|
||||
{ label: '仓库', value: INVENTORY_DIMENSION.WAREHOUSE },
|
||||
{ label: '商品', value: INVENTORY_DIMENSION.ITEM },
|
||||
];
|
||||
|
||||
interface DimensionChangeEvent {
|
||||
target?: {
|
||||
value?: InventoryDimension;
|
||||
};
|
||||
}
|
||||
|
||||
type DimensionChangeHandler = (
|
||||
dimension: InventoryDimension,
|
||||
) => Promise<void> | void;
|
||||
|
||||
/** 统一解析 antd 事件对象和原始维度值 */
|
||||
function getDimensionChangeValue(
|
||||
value?: DimensionChangeEvent | InventoryDimension,
|
||||
): InventoryDimension {
|
||||
if (
|
||||
value === INVENTORY_DIMENSION.ITEM ||
|
||||
value === INVENTORY_DIMENSION.WAREHOUSE
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
return value?.target?.value ?? INVENTORY_DIMENSION.WAREHOUSE;
|
||||
}
|
||||
|
||||
/** 搜索表单 */
|
||||
export function useGridFormSchema(
|
||||
onDimensionChange: DimensionChangeHandler,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '统计维度',
|
||||
component: 'RadioGroup',
|
||||
defaultValue: INVENTORY_DIMENSION.WAREHOUSE,
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
options: dimensionOptions,
|
||||
onChange: (value: DimensionChangeEvent | InventoryDimension) => {
|
||||
void onDimensionChange(getDimensionChangeValue(value));
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getWarehouseSimpleList,
|
||||
labelField: 'name',
|
||||
placeholder: '请选择仓库',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemCode',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuName',
|
||||
label: '规格名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuCode',
|
||||
label: '规格编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const warehouseDimensionColumns: VxeTableGridOptions['columns'] = [
|
||||
{
|
||||
field: 'warehouseId',
|
||||
title: '仓库',
|
||||
minWidth: 160,
|
||||
slots: { default: 'warehouseName' },
|
||||
},
|
||||
{
|
||||
field: 'warehouseItemId',
|
||||
title: '商品信息',
|
||||
minWidth: 240,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuId',
|
||||
title: '规格信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '库存',
|
||||
align: 'right',
|
||||
minWidth: 130,
|
||||
slots: { default: 'quantity' },
|
||||
},
|
||||
];
|
||||
|
||||
const itemDimensionColumns: VxeTableGridOptions['columns'] = [
|
||||
{
|
||||
field: 'itemId',
|
||||
title: '商品信息',
|
||||
minWidth: 240,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuId',
|
||||
title: '规格信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuWarehouseId',
|
||||
title: '仓库',
|
||||
minWidth: 160,
|
||||
slots: { default: 'warehouseName' },
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '库存',
|
||||
align: 'right',
|
||||
minWidth: 130,
|
||||
slots: { default: 'quantity' },
|
||||
},
|
||||
];
|
||||
|
||||
/** 库存统计列表字段 */
|
||||
export function useGridColumns(
|
||||
dimension: InventoryDimension = INVENTORY_DIMENSION.WAREHOUSE,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return dimension === INVENTORY_DIMENSION.ITEM
|
||||
? itemDimensionColumns
|
||||
: warehouseDimensionColumns;
|
||||
}
|
||||
|
||||
/** 库存选择弹窗搜索表单 */
|
||||
export function useInventorySelectGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'itemName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemCode',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuName',
|
||||
label: '规格名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'skuCode',
|
||||
label: '规格编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 库存选择弹窗列表字段 */
|
||||
export function useInventorySelectGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{
|
||||
field: 'itemInfo',
|
||||
title: '商品信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuInfo',
|
||||
title: '规格信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
title: '仓库',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'availableQuantity',
|
||||
title: '可用库存',
|
||||
align: 'right',
|
||||
width: 130,
|
||||
slots: { default: 'availableQuantity' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<script lang="ts" setup>
|
||||
import type { InventoryDimension } from './data';
|
||||
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsInventoryApi } from '#/api/wms/inventory';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page } from '@vben/common-ui';
|
||||
|
||||
import { Checkbox } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getInventoryPage } from '#/api/wms/inventory';
|
||||
import { formatQuantity } from '#/views/wms/utils/format';
|
||||
|
||||
import { INVENTORY_DIMENSION, useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
interface InventoryRow extends WmsInventoryApi.Inventory {
|
||||
skuWarehouseId?: string;
|
||||
warehouseItemId?: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsInventory' });
|
||||
|
||||
const currentRows = ref<InventoryRow[]>([]);
|
||||
const currentDimension = ref<InventoryDimension>(INVENTORY_DIMENSION.WAREHOUSE);
|
||||
const filterZero = ref(false);
|
||||
|
||||
/** 补充合并单元格需要的复合 key */
|
||||
function buildRows(items: WmsInventoryApi.Inventory[]) {
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
skuWarehouseId: `${item.skuId || 0}-${item.warehouseId || 0}`,
|
||||
warehouseItemId: `${item.warehouseId || 0}-${item.itemId || 0}`,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 切换统计维度时同步表单、表头和查询参数 */
|
||||
async function handleDimensionChange(dimension: InventoryDimension) {
|
||||
// spanMethod 依赖当前维度判断合并字段,先更新本地状态。
|
||||
currentDimension.value = dimension;
|
||||
// 表头结构需要立即跟随维度切换,避免请求完成前仍显示旧列。
|
||||
await gridApi.grid.reloadColumn(useGridColumns(dimension) || []);
|
||||
// VXE proxy 查询会合并最近提交值,这里显式写入新 type 并刷新提交快照。
|
||||
await gridApi.formApi.setFieldValue('type', dimension);
|
||||
const formValues = await gridApi.formApi.getValues();
|
||||
gridApi.formApi.setLatestSubmissionValues(formValues);
|
||||
// 带上最新表单值重新加载,确保本次请求使用切换后的统计维度。
|
||||
await gridApi.reload(formValues);
|
||||
}
|
||||
|
||||
/** 切换零库存过滤时按当前搜索条件重新查询 */
|
||||
function handleFilterZeroChange() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 按列字段读取行值,兼容 VXE 的 field/property */
|
||||
function getRowPropertyValue(row: InventoryRow | undefined, property: string) {
|
||||
return row?.[property as keyof InventoryRow];
|
||||
}
|
||||
|
||||
/** 根据统计维度返回需要合并单元格的字段 */
|
||||
function getRowSpanProperties() {
|
||||
if (currentDimension.value === INVENTORY_DIMENSION.ITEM) {
|
||||
return ['itemId', 'skuId', 'skuWarehouseId'];
|
||||
}
|
||||
return ['warehouseId', 'warehouseItemId'];
|
||||
}
|
||||
|
||||
/** 合并库存统计的维度单元格 */
|
||||
function handleSpanMethod({
|
||||
column,
|
||||
rowIndex,
|
||||
}: {
|
||||
column: { field?: string; property?: string };
|
||||
rowIndex: number;
|
||||
}) {
|
||||
const property = column.field || column.property;
|
||||
if (!property || !getRowSpanProperties().includes(property)) {
|
||||
return { colspan: 1, rowspan: 1 };
|
||||
}
|
||||
const row = currentRows.value[rowIndex];
|
||||
if (
|
||||
rowIndex > 0 &&
|
||||
getRowPropertyValue(currentRows.value[rowIndex - 1], property) ===
|
||||
getRowPropertyValue(row, property)
|
||||
) {
|
||||
return { colspan: 0, rowspan: 0 };
|
||||
}
|
||||
let rowspan = 1;
|
||||
for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) {
|
||||
if (
|
||||
getRowPropertyValue(currentRows.value[index], property) !==
|
||||
getRowPropertyValue(row, property)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
rowspan += 1;
|
||||
}
|
||||
return { colspan: 1, rowspan };
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(handleDimensionChange),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
// 维度切换会通过 reload(formValues) 传入新 type,刷新场景用当前维度兜底。
|
||||
const nextDimension = (formValues.type ||
|
||||
currentDimension.value ||
|
||||
INVENTORY_DIMENSION.WAREHOUSE) as InventoryDimension;
|
||||
currentDimension.value = nextDimension;
|
||||
await gridApi.grid.reloadColumn(useGridColumns(nextDimension) || []);
|
||||
|
||||
// type 是后端聚合维度参数,普通搜索参数里移除,避免重复透传旧值。
|
||||
const queryParams = { ...formValues };
|
||||
Reflect.deleteProperty(queryParams, 'type');
|
||||
const data = await getInventoryPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...queryParams,
|
||||
type: nextDimension,
|
||||
onlyPositiveQuantity: filterZero.value ? true : undefined,
|
||||
});
|
||||
|
||||
// spanMethod 依赖当前页数据计算 rowspan,需要保存构造后的行。
|
||||
currentRows.value = buildRows(data.list || []);
|
||||
return {
|
||||
...data,
|
||||
list: currentRows.value,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
spanMethod: handleSpanMethod,
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<InventoryRow>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【库存】库存记录、流水、统计"
|
||||
url="https://doc.iocoder.cn/wms/inventory/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Grid table-title="库存统计">
|
||||
<template #toolbar-tools>
|
||||
<Checkbox v-model:checked="filterZero" @change="handleFilterZeroChange">
|
||||
过滤掉库存为 0 的商品
|
||||
</Checkbox>
|
||||
</template>
|
||||
<template #warehouseName="{ row }">
|
||||
{{ row.warehouseName || '-' }}
|
||||
</template>
|
||||
<template #itemInfo="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
<template #skuInfo="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
<template #quantity="{ row }">
|
||||
{{ formatQuantity(row.quantity) }}
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as WmsItemBrandSelect } from './select.vue';
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SelectValue } from 'antdv-next';
|
||||
|
||||
import type { WmsItemBrandApi } from '#/api/wms/md/item/brand';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Select } from 'antdv-next';
|
||||
|
||||
import { getItemBrandSimpleList } from '#/api/wms/md/item/brand';
|
||||
|
||||
defineOptions({ name: 'WmsItemBrandSelect', inheritAttrs: false });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
allowClear?: boolean;
|
||||
disabled?: boolean;
|
||||
modelValue?: number;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
allowClear: true,
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择商品品牌',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [brand: undefined | WmsItemBrandApi.ItemBrand];
|
||||
'update:modelValue': [value: number | undefined];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const brandList = ref<WmsItemBrandApi.ItemBrand[]>([]);
|
||||
|
||||
const options = computed(() =>
|
||||
brandList.value
|
||||
.filter((brand) => brand.id !== undefined)
|
||||
.map((brand) => ({
|
||||
label: brand.name,
|
||||
value: brand.id,
|
||||
})),
|
||||
);
|
||||
|
||||
/** 选中变化 */
|
||||
function handleChange(value: SelectValue) {
|
||||
const brandId = typeof value === 'number' ? value : undefined;
|
||||
emit('update:modelValue', brandId);
|
||||
emit(
|
||||
'change',
|
||||
brandList.value.find((brand) => brand.id === brandId),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询商品品牌精简列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
brandList.value = await getItemBrandSimpleList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
v-bind="$attrs"
|
||||
:allow-clear="allowClear"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:options="options"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
class="w-full"
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { generateWmsCode } from '@vben/constants';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
/** 新增/修改商品品牌的表单 */
|
||||
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '品牌编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入品牌编号',
|
||||
},
|
||||
rules: z.string().min(1, '品牌编号不能为空').max(20),
|
||||
suffix: () => {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('code', generateWmsCode('B'));
|
||||
},
|
||||
},
|
||||
{ default: () => '生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '品牌名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 30,
|
||||
placeholder: '请输入品牌名称',
|
||||
},
|
||||
rules: z.string().min(1, '品牌名称不能为空').max(30),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '品牌编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入品牌编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '品牌名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入品牌名称',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'code',
|
||||
title: '品牌编号',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '品牌名称',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemBrandApi } from '#/api/wms/md/item/brand';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteItemBrand,
|
||||
exportItemBrand,
|
||||
getItemBrandPage,
|
||||
} from '#/api/wms/md/item/brand';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'WmsItemBrand' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建商品品牌 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑商品品牌 */
|
||||
function handleEdit(row: WmsItemBrandApi.ItemBrand) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除商品品牌 */
|
||||
async function handleDelete(row: WmsItemBrandApi.ItemBrand) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteItemBrand(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出商品品牌 */
|
||||
async function handleExport() {
|
||||
const data = await exportItemBrand(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '商品品牌.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getItemBrandPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsItemBrandApi.ItemBrand>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="商品品牌列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商品品牌']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:item-brand:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:item-brand:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['wms:item-brand:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['wms:item-brand:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsItemBrandApi } from '#/api/wms/md/item/brand';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createItemBrand,
|
||||
getItemBrand,
|
||||
updateItemBrand,
|
||||
} from '#/api/wms/md/item/brand';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'WmsItemBrandForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<WmsItemBrandApi.ItemBrand>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['商品品牌'])
|
||||
: $t('ui.actionTitle.create', ['商品品牌']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
const data = (await formApi.getValues()) as WmsItemBrandApi.ItemBrand;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateItemBrand(data)
|
||||
: createItemBrand(data));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(formApi) });
|
||||
const data = modalApi.getData<WmsItemBrandApi.ItemBrand>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getItemBrand(data.id);
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/3">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default as WmsItemCategorySelect } from './select.vue';
|
||||
export { default as WmsItemCategoryTree } from './tree.vue';
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { TreeSelect } from 'antdv-next';
|
||||
|
||||
import { getItemCategorySimpleList } from '#/api/wms/md/item/category';
|
||||
|
||||
/** WMS 商品分类选择器 */
|
||||
defineOptions({ name: 'WmsItemCategorySelect', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
allowClear?: boolean;
|
||||
disabled?: boolean;
|
||||
modelValue?: number;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
allowClear: true,
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择商品分类',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number | undefined];
|
||||
}>();
|
||||
|
||||
const categoryTree = ref<WmsItemCategoryApi.ItemCategory[]>([]);
|
||||
|
||||
const selectValue = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: number | undefined) => {
|
||||
emit('update:modelValue', value);
|
||||
},
|
||||
});
|
||||
|
||||
/** 查询商品分类树 */
|
||||
async function getCategoryTree() {
|
||||
const data = await getItemCategorySimpleList();
|
||||
categoryTree.value = handleTree(data, 'id', 'parentId');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCategoryTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TreeSelect
|
||||
v-bind="$attrs"
|
||||
v-model:value="selectValue"
|
||||
:allow-clear="allowClear"
|
||||
:disabled="disabled"
|
||||
:field-names="{ children: 'children', label: 'name', value: 'id' }"
|
||||
:placeholder="placeholder"
|
||||
:tree-data="categoryTree"
|
||||
class="w-full"
|
||||
tree-default-expand-all
|
||||
tree-node-filter-prop="name"
|
||||
show-search
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { Input, Spin, Tree } from 'antdv-next';
|
||||
|
||||
import { getItemCategorySimpleList } from '#/api/wms/md/item/category';
|
||||
|
||||
/** WMS 商品分类树组件 */
|
||||
defineOptions({ name: 'WmsItemCategoryTree' });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
filterPlaceholder?: string;
|
||||
}>(),
|
||||
{
|
||||
filterPlaceholder: '请输入分类名称',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
nodeClick: [categoryId: number | undefined];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const filterText = ref('');
|
||||
const currentNodeId = ref<number>();
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const categoryList = ref<WmsItemCategoryApi.ItemCategory[]>([]);
|
||||
const categoryTree = ref<any[]>([]);
|
||||
|
||||
/** 加载分类树 */
|
||||
async function loadTree() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getItemCategorySimpleList();
|
||||
categoryList.value = data;
|
||||
categoryTree.value = handleTree(data, 'id', 'parentId');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理搜索逻辑 */
|
||||
function handleSearch(value: string) {
|
||||
filterText.value = value;
|
||||
const filteredList = value
|
||||
? categoryList.value.filter((item) => item.name?.includes(value))
|
||||
: categoryList.value;
|
||||
categoryTree.value = handleTree(filteredList, 'id', 'parentId');
|
||||
}
|
||||
|
||||
/** 处理节点点击:支持点击同一节点取消选中 */
|
||||
function handleSelect(_selectedKeys: any[], info: any) {
|
||||
const row = info.node.dataRef as WmsItemCategoryApi.ItemCategory;
|
||||
if (currentNodeId.value === row.id) {
|
||||
currentNodeId.value = undefined;
|
||||
selectedKeys.value = [];
|
||||
emit('nodeClick', undefined);
|
||||
return;
|
||||
}
|
||||
currentNodeId.value = row.id;
|
||||
selectedKeys.value = row.id === undefined ? [] : [row.id];
|
||||
emit('nodeClick', row.id);
|
||||
}
|
||||
|
||||
/** 清空选中状态 */
|
||||
function reset() {
|
||||
currentNodeId.value = undefined;
|
||||
filterText.value = '';
|
||||
selectedKeys.value = [];
|
||||
categoryTree.value = handleTree(categoryList.value, 'id', 'parentId');
|
||||
}
|
||||
|
||||
/** 设置当前选中分类 */
|
||||
function setCurrent(categoryId: number) {
|
||||
currentNodeId.value = categoryId;
|
||||
selectedKeys.value = [categoryId];
|
||||
}
|
||||
|
||||
watch(filterText, (value) => {
|
||||
handleSearch(value);
|
||||
});
|
||||
|
||||
defineExpose({ reset, setCurrent });
|
||||
|
||||
onMounted(() => {
|
||||
loadTree();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<Input
|
||||
v-model:value="filterText"
|
||||
:placeholder="filterPlaceholder"
|
||||
allow-clear
|
||||
class="w-full"
|
||||
>
|
||||
<template #prefix>
|
||||
<IconifyIcon class="size-4" icon="lucide:search" />
|
||||
</template>
|
||||
</Input>
|
||||
<Spin :spinning="loading" wrapper-class-name="w-full">
|
||||
<Tree
|
||||
v-if="categoryTree.length > 0"
|
||||
:default-expand-all="true"
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:selected-keys="selectedKeys"
|
||||
:tree-data="categoryTree"
|
||||
class="pt-2"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
<div v-else-if="!loading" class="py-4 text-center text-gray-500">
|
||||
暂无数据
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { CommonStatusEnum, DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { handleTree } from '@vben/utils';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getItemCategorySimpleList } from '#/api/wms/md/item/category';
|
||||
|
||||
/** 新增/修改商品分类的表单 */
|
||||
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
label: '上级分类',
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: async () => {
|
||||
const data = await getItemCategorySimpleList();
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: '顶级分类',
|
||||
children: handleTree(data),
|
||||
},
|
||||
];
|
||||
},
|
||||
childrenField: 'children',
|
||||
labelField: 'name',
|
||||
placeholder: '请选择上级分类',
|
||||
treeDefaultExpandAll: true,
|
||||
treeNodeFilterProp: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '分类编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入分类编号',
|
||||
},
|
||||
rules: z.string().min(1, '分类编号不能为空').max(20),
|
||||
suffix: () => {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('code', generateWmsCode('C'));
|
||||
},
|
||||
},
|
||||
{ default: () => '生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '显示排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
class: '!w-full',
|
||||
min: 0,
|
||||
},
|
||||
rules: z.number().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
optionType: 'button',
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
},
|
||||
rules: z.number().default(CommonStatusEnum.ENABLE),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '分类编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入分类编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '分类名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入分类名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '分类状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||
placeholder: '请选择分类状态',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions<WmsItemCategoryApi.ItemCategory>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称',
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '分类编号',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteItemCategory,
|
||||
getItemCategoryList,
|
||||
} from '#/api/wms/md/item/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'WmsItemCategory' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
function handleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
gridApi.grid.setAllTreeExpand(isExpanded.value);
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建分类 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 添加下级分类 */
|
||||
function handleAppend(row: WmsItemCategoryApi.ItemCategory) {
|
||||
formModalApi.setData({ parentId: row.id }).open();
|
||||
}
|
||||
|
||||
/** 编辑分类 */
|
||||
function handleEdit(row: WmsItemCategoryApi.ItemCategory) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除分类 */
|
||||
async function handleDelete(row: WmsItemCategoryApi.ItemCategory) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteItemCategory(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_, formValues) => {
|
||||
return await getItemCategoryList(formValues);
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: true,
|
||||
expandAll: true,
|
||||
reserve: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsItemCategoryApi.ItemCategory>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="商品分类列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['分类']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:item-category:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: isExpanded ? '收缩' : '展开',
|
||||
type: 'primary',
|
||||
onClick: handleExpand,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增下级',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:item-category:create'],
|
||||
onClick: handleAppend.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['wms:item-category:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['wms:item-category:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsItemCategoryApi } from '#/api/wms/md/item/category';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { CommonStatusEnum } from '@vben/constants';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createItemCategory,
|
||||
getItemCategory,
|
||||
updateItemCategory,
|
||||
} from '#/api/wms/md/item/category';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'WmsItemCategoryForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<WmsItemCategoryApi.ItemCategory>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['商品分类'])
|
||||
: $t('ui.actionTitle.create', ['商品分类']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as WmsItemCategoryApi.ItemCategory;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateItemCategory(data)
|
||||
: createItemCategory(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(formApi) });
|
||||
const data = modalApi.getData<WmsItemCategoryApi.ItemCategory>();
|
||||
if (!data || !data.id) {
|
||||
formData.value = data;
|
||||
await formApi.setValues({
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
...data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getItemCategory(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/4">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { generateWmsCode } from '@vben/constants';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getItemBrandSimpleList } from '#/api/wms/md/item/brand';
|
||||
|
||||
import { WmsItemBrandSelect } from './brand/components';
|
||||
import { WmsItemCategorySelect } from './category/components';
|
||||
|
||||
/** 新增/修改商品的表单 */
|
||||
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 60,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
rules: z.string().min(1, '商品名称不能为空').max(60),
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryId',
|
||||
label: '商品分类',
|
||||
component: markRaw(WmsItemCategorySelect),
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
rules: z.string().min(1, '商品编号不能为空').max(20),
|
||||
suffix: () => {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('code', generateWmsCode('I'));
|
||||
},
|
||||
},
|
||||
{ default: () => '生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'unit',
|
||||
label: '商品单位',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入单位',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'brandId',
|
||||
label: '商品品牌',
|
||||
component: markRaw(WmsItemBrandSelect),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
rows: 3,
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'brandId',
|
||||
label: '商品品牌',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getItemBrandSimpleList,
|
||||
labelField: 'name',
|
||||
placeholder: '请选择商品品牌',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'itemInfo',
|
||||
title: '商品信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuInfo',
|
||||
title: '规格信息',
|
||||
minWidth: 180,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'priceInfo',
|
||||
title: '金额(元)',
|
||||
minWidth: 140,
|
||||
slots: { default: 'priceInfo' },
|
||||
},
|
||||
{
|
||||
field: 'weightInfo',
|
||||
title: '重量(kg)',
|
||||
minWidth: 140,
|
||||
slots: { default: 'weightInfo' },
|
||||
},
|
||||
{
|
||||
field: 'dimensionInfo',
|
||||
title: '长宽高(cm)',
|
||||
minWidth: 180,
|
||||
align: 'right',
|
||||
slots: { default: 'dimensionInfo' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** SKU 选择弹窗搜索表单 */
|
||||
export function useSkuSelectGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'itemName',
|
||||
label: '商品名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'itemCode',
|
||||
label: '商品编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入商品编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '规格名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '规格编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入规格编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'barCode',
|
||||
label: '条码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入条码',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** SKU 选择弹窗列表字段 */
|
||||
export function useSkuSelectGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 50 },
|
||||
{
|
||||
field: 'itemInfo',
|
||||
title: '商品信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'itemInfo' },
|
||||
},
|
||||
{
|
||||
field: 'skuInfo',
|
||||
title: '规格信息',
|
||||
minWidth: 220,
|
||||
slots: { default: 'skuInfo' },
|
||||
},
|
||||
{
|
||||
field: 'priceInfo',
|
||||
title: '金额(元)',
|
||||
minWidth: 160,
|
||||
slots: { default: 'priceInfo' },
|
||||
},
|
||||
{
|
||||
field: 'weightInfo',
|
||||
title: '重量(kg)',
|
||||
minWidth: 160,
|
||||
slots: { default: 'weightInfo' },
|
||||
},
|
||||
{
|
||||
field: 'dimensionInfo',
|
||||
title: '长宽高(cm)',
|
||||
minWidth: 180,
|
||||
align: 'right',
|
||||
slots: { default: 'dimensionInfo' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemApi } from '#/api/wms/md/item';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Card, message } from 'antdv-next';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteItem, exportItem, getItemPage } from '#/api/wms/md/item';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsItemCategoryTree } from '#/views/wms/md/item/category/components';
|
||||
import {
|
||||
formatDimensionText,
|
||||
formatPrice,
|
||||
formatWeight,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
type ItemSkuRow = WmsItemSkuApi.ItemSku;
|
||||
|
||||
defineOptions({ name: 'WmsItem' });
|
||||
|
||||
const currentRows = ref<ItemSkuRow[]>([]);
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 展开商品 SKU 列表 */
|
||||
function buildItemSkuRows(items: WmsItemApi.Item[]) {
|
||||
return items.flatMap((item) => {
|
||||
const skus = item.skus?.length ? item.skus : [{}];
|
||||
return skus.map((sku) => ({
|
||||
...sku,
|
||||
itemId: item.id,
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
categoryId: item.categoryId,
|
||||
categoryName: item.categoryName,
|
||||
unit: item.unit,
|
||||
brandId: item.brandId,
|
||||
brandName: item.brandName,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建商品 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑商品 */
|
||||
function handleEdit(row: ItemSkuRow) {
|
||||
formModalApi.setData({ id: row.itemId }).open();
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
async function handleDelete(row: ItemSkuRow) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.itemName]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteItem(row.itemId!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出商品 */
|
||||
async function handleExport() {
|
||||
const data = await exportItem(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '商品.xls', source: data });
|
||||
}
|
||||
|
||||
/** 分类树点击 */
|
||||
const searchCategoryId = ref<number | undefined>(undefined);
|
||||
function handleCategoryNodeClick(categoryId: number | undefined) {
|
||||
searchCategoryId.value = categoryId;
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
/** 合并商品维度的重复单元格 */
|
||||
function handleSpanMethod({
|
||||
column,
|
||||
rowIndex,
|
||||
}: {
|
||||
column: { field?: string; property?: string };
|
||||
rowIndex: number;
|
||||
}) {
|
||||
const field = column.field || column.property;
|
||||
if (!['actions', 'itemInfo'].includes(field || '')) {
|
||||
return { colspan: 1, rowspan: 1 };
|
||||
}
|
||||
const row = currentRows.value[rowIndex];
|
||||
if (rowIndex > 0 && currentRows.value[rowIndex - 1]?.itemId === row?.itemId) {
|
||||
return { colspan: 0, rowspan: 0 };
|
||||
}
|
||||
let rowspan = 1;
|
||||
for (let index = rowIndex + 1; index < currentRows.value.length; index += 1) {
|
||||
if (currentRows.value[index]?.itemId !== row?.itemId) {
|
||||
break;
|
||||
}
|
||||
rowspan += 1;
|
||||
}
|
||||
return { colspan: 1, rowspan };
|
||||
}
|
||||
|
||||
function hasValue(value: unknown) {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
showOverflow: false,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const data = await getItemPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
categoryId: searchCategoryId.value,
|
||||
});
|
||||
currentRows.value = buildItemSkuRows(data.list || []);
|
||||
return {
|
||||
...data,
|
||||
list: currentRows.value,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
spanMethod: handleSpanMethod,
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<ItemSkuRow>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【基础】商品、SKU、分类、品牌"
|
||||
url="https://doc.iocoder.cn/wms/md/item/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
|
||||
<div class="flex h-full w-full">
|
||||
<Card class="mr-4 h-full w-1/6">
|
||||
<WmsItemCategoryTree @node-click="handleCategoryNodeClick" />
|
||||
</Card>
|
||||
<div class="w-5/6">
|
||||
<Grid table-title="商品列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['商品']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:item:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:item:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #itemInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div class="text-sm">{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
{{ row.itemCode }}
|
||||
</div>
|
||||
<div v-if="row.brandName" class="text-xs text-gray-500">
|
||||
品牌:{{ row.brandName }}
|
||||
</div>
|
||||
<div v-if="row.categoryName" class="text-xs text-gray-500">
|
||||
分类:{{ row.categoryName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #skuInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div class="text-sm">{{ row.name || '-' }}</div>
|
||||
<div v-if="row.code" class="text-xs text-gray-500">
|
||||
编号:{{ row.code }}
|
||||
</div>
|
||||
<div v-if="row.barCode" class="text-xs text-gray-500">
|
||||
条码:{{ row.barCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #priceInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div v-if="hasValue(row.costPrice)">
|
||||
成本价:{{ formatPrice(row.costPrice) }}
|
||||
</div>
|
||||
<div v-if="hasValue(row.sellingPrice)">
|
||||
销售价:{{ formatPrice(row.sellingPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #weightInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div v-if="hasValue(row.netWeight)">
|
||||
净重:{{ formatWeight(row.netWeight) }}
|
||||
</div>
|
||||
<div v-if="hasValue(row.grossWeight)">
|
||||
毛重:{{ formatWeight(row.grossWeight) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #dimensionInfo="{ row }">
|
||||
{{ formatDimensionText(row.length, row.width, row.height) || '-' }}
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['wms:item:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['wms:item:delete'],
|
||||
popConfirm: {
|
||||
title: `确认删除商品【${row.itemName}】吗?`,
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsItemApi } from '#/api/wms/md/item';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createItem, getItem, updateItem } from '#/api/wms/md/item';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
import SkuForm from './sku-form.vue';
|
||||
|
||||
defineOptions({ name: 'WmsItemForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<WmsItemApi.Item>();
|
||||
const skuFormRef = ref<any>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['商品'])
|
||||
: $t('ui.actionTitle.create', ['商品']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 88,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
async function resetSkuForm(item?: WmsItemApi.Item) {
|
||||
await nextTick();
|
||||
await skuFormRef.value?.setRows(item?.skus);
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
skuFormRef.value?.validate();
|
||||
} catch (error) {
|
||||
message.warning((error as Error).message);
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as WmsItemApi.Item;
|
||||
data.skus = skuFormRef.value?.getRows() || [];
|
||||
try {
|
||||
await (formData.value?.id ? updateItem(data) : createItem(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(formApi) });
|
||||
await resetSkuForm();
|
||||
// 加载数据
|
||||
const data = modalApi.getData<WmsItemApi.Item>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getItem(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
await resetSkuForm(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/4">
|
||||
<Form class="mx-4" />
|
||||
<SkuForm ref="skuFormRef" class="mx-4 mt-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { generateWmsCode } from '@vben/constants';
|
||||
|
||||
import { Button, Input, InputNumber, message } from 'antdv-next';
|
||||
|
||||
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
DIMENSION_PRECISION,
|
||||
PRICE_PRECISION,
|
||||
WEIGHT_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
interface SkuRow extends WmsItemSkuApi.ItemSku {
|
||||
seq: number;
|
||||
}
|
||||
|
||||
let skuSeq = 0; // SKU 行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
const tableData = ref<SkuRow[]>([]);
|
||||
|
||||
function nextSkuSeq() {
|
||||
skuSeq += 1;
|
||||
return skuSeq;
|
||||
}
|
||||
|
||||
function buildEmptySku(): SkuRow {
|
||||
return {
|
||||
seq: nextSkuSeq(),
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
barCode: undefined,
|
||||
code: undefined,
|
||||
length: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
grossWeight: undefined,
|
||||
netWeight: undefined,
|
||||
costPrice: undefined,
|
||||
sellingPrice: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function toSkuRow(sku: WmsItemSkuApi.ItemSku): SkuRow {
|
||||
return {
|
||||
...sku,
|
||||
seq: nextSkuSeq(),
|
||||
};
|
||||
}
|
||||
|
||||
function toSku(row: SkuRow): WmsItemSkuApi.ItemSku {
|
||||
const { seq: _seq, ...sku } = row;
|
||||
return sku;
|
||||
}
|
||||
|
||||
async function reloadGrid() {
|
||||
await nextTick();
|
||||
await gridApi.grid.reloadData(tableData.value);
|
||||
}
|
||||
|
||||
async function setRows(skus?: WmsItemSkuApi.ItemSku[]) {
|
||||
skuSeq = 0;
|
||||
tableData.value = skus?.length
|
||||
? skus.map((sku) => toSkuRow(sku))
|
||||
: [buildEmptySku()];
|
||||
await reloadGrid();
|
||||
}
|
||||
|
||||
function getRows() {
|
||||
return tableData.value.map((row) => toSku(row));
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if (tableData.value.length === 0) {
|
||||
throw new Error('至少包含一个商品规格');
|
||||
}
|
||||
for (let index = 0; index < tableData.value.length; index += 1) {
|
||||
const row = tableData.value[index];
|
||||
if (!row?.name) {
|
||||
throw new Error(`第 ${index + 1} 行:规格名称不能为空`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddSku() {
|
||||
tableData.value.push(buildEmptySku());
|
||||
await reloadGrid();
|
||||
}
|
||||
|
||||
async function handleDeleteSku(row: SkuRow) {
|
||||
if (tableData.value.length <= 1) {
|
||||
message.error('至少包含一个商品规格');
|
||||
return;
|
||||
}
|
||||
const index = tableData.value.findIndex((item) => item.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
tableData.value.splice(index, 1);
|
||||
}
|
||||
await reloadGrid();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: [
|
||||
{
|
||||
field: 'name',
|
||||
title: '规格名称',
|
||||
minWidth: 180,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'codeBarCode',
|
||||
title: '编号/条码',
|
||||
width: 300,
|
||||
slots: { default: 'codeBarCode' },
|
||||
},
|
||||
{
|
||||
field: 'dimension',
|
||||
title: '长/宽/高(cm)',
|
||||
width: 300,
|
||||
slots: { default: 'dimension' },
|
||||
},
|
||||
{
|
||||
field: 'weight',
|
||||
title: '净重/毛重(kg)',
|
||||
width: 200,
|
||||
slots: { default: 'weight' },
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '成本价/销售价',
|
||||
width: 200,
|
||||
slots: { default: 'price' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
],
|
||||
data: tableData.value,
|
||||
minHeight: 260,
|
||||
autoResize: true,
|
||||
border: true,
|
||||
showOverflow: false,
|
||||
rowConfig: {
|
||||
keyField: 'seq',
|
||||
isHover: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
} as VxeTableGridOptions<SkuRow>,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getRows,
|
||||
setRows,
|
||||
validate,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">规格</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增规格',
|
||||
type: 'primary',
|
||||
onClick: handleAddSku,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<Grid class="w-full">
|
||||
<template #name="{ row }">
|
||||
<Input
|
||||
v-model:value="row.name"
|
||||
class="w-full"
|
||||
:maxlength="255"
|
||||
placeholder="请输入规格名称"
|
||||
/>
|
||||
</template>
|
||||
<template #codeBarCode="{ row }">
|
||||
<div class="flex flex-col gap-2 py-1">
|
||||
<Input
|
||||
v-model:value="row.code"
|
||||
class="w-full"
|
||||
:maxlength="64"
|
||||
placeholder="编号"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<Button @click="row.code = generateWmsCode('S')">生成</Button>
|
||||
</template>
|
||||
</Input>
|
||||
<Input
|
||||
v-model:value="row.barCode"
|
||||
class="w-full"
|
||||
:maxlength="64"
|
||||
placeholder="条码"
|
||||
>
|
||||
<template #addonAfter>
|
||||
<Button @click="row.barCode = generateWmsCode()">生成</Button>
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</template>
|
||||
<template #dimension="{ row }">
|
||||
<div class="flex w-full gap-1">
|
||||
<InputNumber
|
||||
v-model:value="row.length"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="DIMENSION_PRECISION"
|
||||
class="!w-1/3"
|
||||
placeholder="长"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="row.width"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="DIMENSION_PRECISION"
|
||||
class="!w-1/3"
|
||||
placeholder="宽"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="row.height"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="DIMENSION_PRECISION"
|
||||
class="!w-1/3"
|
||||
placeholder="高"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #weight="{ row }">
|
||||
<div class="flex flex-col gap-2 py-1">
|
||||
<InputNumber
|
||||
v-model:value="row.netWeight"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="WEIGHT_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="净重"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="row.grossWeight"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="WEIGHT_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="毛重"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #price="{ row }">
|
||||
<div class="flex flex-col gap-2 py-1">
|
||||
<InputNumber
|
||||
v-model:value="row.costPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="成本价"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model:value="row.sellingPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="销售价"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: handleDeleteSku.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as WmsItemSkuSelect } from './select.vue';
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { message, Modal } from 'antdv-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getItemSkuPage } from '#/api/wms/md/item/sku';
|
||||
import {
|
||||
formatDimensionText,
|
||||
formatPrice,
|
||||
formatWeight,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import {
|
||||
useSkuSelectGridColumns,
|
||||
useSkuSelectGridFormSchema,
|
||||
} from '../../data';
|
||||
|
||||
defineOptions({ name: 'WmsItemSkuSelect' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [list: WmsItemSkuApi.ItemSku[]];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
const multiple = ref(true);
|
||||
const syncingSingleSelection = ref(false);
|
||||
const selectedRows = ref<WmsItemSkuApi.ItemSku[]>([]);
|
||||
const disabledSelectedIds = ref<Set<number>>(new Set());
|
||||
|
||||
/** 判断 SKU 是否可勾选,已传入的业务明细 SKU 需要禁选 */
|
||||
function isRowSelectable({ row }: { row: WmsItemSkuApi.ItemSku }) {
|
||||
return row.id === undefined || !disabledSelectedIds.value.has(row.id);
|
||||
}
|
||||
|
||||
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
|
||||
async function syncSingleSelection(row?: WmsItemSkuApi.ItemSku) {
|
||||
syncingSingleSelection.value = true;
|
||||
await nextTick();
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
if (row) {
|
||||
await gridApi.grid.setCheckboxRow(row, true);
|
||||
}
|
||||
await nextTick();
|
||||
syncingSingleSelection.value = false;
|
||||
}
|
||||
|
||||
/** 处理勾选变化,单选模式只保留最后一条可选 SKU */
|
||||
async function handleCheckboxChange({
|
||||
checked,
|
||||
records,
|
||||
row,
|
||||
}: {
|
||||
checked: boolean;
|
||||
records: WmsItemSkuApi.ItemSku[];
|
||||
row?: WmsItemSkuApi.ItemSku;
|
||||
}) {
|
||||
if (syncingSingleSelection.value) {
|
||||
return;
|
||||
}
|
||||
if (!multiple.value) {
|
||||
const selected = checked && row ? [row] : [];
|
||||
selectedRows.value = selected;
|
||||
await syncSingleSelection(selected[0]);
|
||||
return;
|
||||
}
|
||||
selectedRows.value = records.filter(
|
||||
(item) => item.id === undefined || !disabledSelectedIds.value.has(item.id),
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理全选变化,过滤掉已禁选的 SKU */
|
||||
function handleCheckboxAll({ records }: { records: WmsItemSkuApi.ItemSku[] }) {
|
||||
if (syncingSingleSelection.value) {
|
||||
return;
|
||||
}
|
||||
selectedRows.value = records.filter(
|
||||
(item) => item.id === undefined || !disabledSelectedIds.value.has(item.id),
|
||||
);
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useSkuSelectGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useSkuSelectGridColumns(),
|
||||
height: 560,
|
||||
keepSource: true,
|
||||
showOverflow: false,
|
||||
checkboxConfig: {
|
||||
checkMethod: isRowSelectable,
|
||||
highlight: true,
|
||||
range: true,
|
||||
reserve: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getItemSkuPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsItemSkuApi.ItemSku>,
|
||||
gridEvents: {
|
||||
checkboxAll: handleCheckboxAll,
|
||||
checkboxChange: handleCheckboxChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 打开 SKU 选择弹窗 */
|
||||
async function openModal(
|
||||
selectedIds?: number[],
|
||||
options?: { multiple?: boolean; preselectDisabled?: boolean },
|
||||
) {
|
||||
open.value = true;
|
||||
multiple.value = options?.multiple ?? true;
|
||||
selectedRows.value = [];
|
||||
disabledSelectedIds.value =
|
||||
options?.preselectDisabled === false
|
||||
? new Set()
|
||||
: new Set(selectedIds || []);
|
||||
await nextTick();
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
await gridApi.formApi.resetForm();
|
||||
await gridApi.query();
|
||||
}
|
||||
|
||||
/** 关闭 SKU 选择弹窗并清空临时选择 */
|
||||
async function closeModal() {
|
||||
open.value = false;
|
||||
selectedRows.value = [];
|
||||
await gridApi.grid.clearCheckboxRow();
|
||||
}
|
||||
|
||||
/** 确认选择并返回新增的 SKU 列表 */
|
||||
function handleConfirm() {
|
||||
const list = selectedRows.value.filter(
|
||||
(sku) => sku.id !== undefined && !disabledSelectedIds.value.has(sku.id),
|
||||
);
|
||||
if (list.length === 0) {
|
||||
message.warning('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
emit('change', list);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
defineExpose({ open: openModal });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="open"
|
||||
title="商品选择"
|
||||
width="80%"
|
||||
:destroy-on-close="true"
|
||||
@ok="handleConfirm"
|
||||
@cancel="closeModal"
|
||||
>
|
||||
<Grid table-title="商品 SKU 列表">
|
||||
<template #itemInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
<div v-if="row.brandName" class="text-xs text-gray-500">
|
||||
品牌:{{ row.brandName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #skuInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div>{{ row.name || '-' }}</div>
|
||||
<div v-if="row.code" class="text-xs text-gray-500">
|
||||
编号:{{ row.code }}
|
||||
</div>
|
||||
<div v-if="row.barCode" class="text-xs text-gray-500">
|
||||
条码:{{ row.barCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #priceInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div v-if="row.costPrice !== undefined">
|
||||
成本价:{{ formatPrice(row.costPrice) }}
|
||||
</div>
|
||||
<div v-if="row.sellingPrice !== undefined">
|
||||
销售价:{{ formatPrice(row.sellingPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #weightInfo="{ row }">
|
||||
<div class="flex flex-col gap-1 py-1 leading-5">
|
||||
<div v-if="row.netWeight !== undefined">
|
||||
净重:{{ formatWeight(row.netWeight) }}
|
||||
</div>
|
||||
<div v-if="row.grossWeight !== undefined">
|
||||
毛重:{{ formatWeight(row.grossWeight) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #dimensionInfo="{ row }">
|
||||
{{ formatDimensionText(row.length, row.width, row.height) || '-' }}
|
||||
</template>
|
||||
</Grid>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as WmsMerchantSelect } from './select.vue';
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SelectValue } from 'antdv-next';
|
||||
|
||||
import type { WmsMerchantApi } from '#/api/wms/md/merchant';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
CustomerMerchantTypeList,
|
||||
SupplierMerchantTypeList,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { Select } from 'antdv-next';
|
||||
|
||||
import { getMerchantSimpleList } from '#/api/wms/md/merchant';
|
||||
|
||||
defineOptions({ name: 'WmsMerchantSelect', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
allowClear?: boolean;
|
||||
customer?: boolean;
|
||||
disabled?: boolean;
|
||||
modelValue?: number;
|
||||
placeholder?: string;
|
||||
supplier?: boolean;
|
||||
}>(),
|
||||
{
|
||||
allowClear: true,
|
||||
customer: false,
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择往来企业',
|
||||
supplier: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [merchant: undefined | WmsMerchantApi.Merchant];
|
||||
'update:modelValue': [value: number | undefined];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const merchantList = ref<WmsMerchantApi.Merchant[]>([]);
|
||||
|
||||
const options = computed(() =>
|
||||
merchantList.value
|
||||
.filter((merchant) => merchant.id !== undefined)
|
||||
.map((merchant) => ({
|
||||
label: merchant.name,
|
||||
value: merchant.id,
|
||||
})),
|
||||
);
|
||||
|
||||
/** 选中变化 */
|
||||
function handleChange(value: SelectValue) {
|
||||
const merchantId = typeof value === 'number' ? value : undefined;
|
||||
emit('update:modelValue', merchantId);
|
||||
emit(
|
||||
'change',
|
||||
merchantList.value.find((merchant) => merchant.id === merchantId),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询往来企业精简列表 */
|
||||
async function getList() {
|
||||
let types: number[] | undefined;
|
||||
if (props.supplier) {
|
||||
types = SupplierMerchantTypeList;
|
||||
} else if (props.customer) {
|
||||
types = CustomerMerchantTypeList;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
merchantList.value = await getMerchantSimpleList(
|
||||
types ? { types } : undefined,
|
||||
);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.supplier, props.customer],
|
||||
() => {
|
||||
getList();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
v-bind="$attrs"
|
||||
:allow-clear="allowClear"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:options="options"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
class="w-full"
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DICT_TYPE, generateWmsCode } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
/** 新增/修改往来企业的表单 */
|
||||
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '往来企业编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入往来企业编号',
|
||||
},
|
||||
rules: z.string().min(1, '往来企业编号不能为空').max(20),
|
||||
suffix: () => {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('code', generateWmsCode('M'));
|
||||
},
|
||||
},
|
||||
{ default: () => '生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '往来企业名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 60,
|
||||
placeholder: '请输入往来企业名称',
|
||||
},
|
||||
rules: z.string().min(1, '往来企业名称不能为空').max(60),
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '往来企业类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_MERCHANT_TYPE, 'number'),
|
||||
placeholder: '请选择往来企业类型',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'level',
|
||||
label: '级别',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 10,
|
||||
placeholder: '请输入级别',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bankName',
|
||||
label: '开户行',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入开户行',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'bankAccount',
|
||||
label: '银行账户',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 40,
|
||||
placeholder: '请输入银行账户',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'address',
|
||||
label: '地址',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 200,
|
||||
placeholder: '请输入地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'contact',
|
||||
label: '联系人',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 30,
|
||||
placeholder: '请输入联系人',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'mobile',
|
||||
label: '手机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 13,
|
||||
placeholder: '请输入手机号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'telephone',
|
||||
label: '座机号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 13,
|
||||
placeholder: '请输入座机号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'email',
|
||||
label: 'Email',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
placeholder: '请输入 Email',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '往来企业编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入往来企业编号',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '往来企业名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入往来企业名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
label: '往来企业类型',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_MERCHANT_TYPE, 'number'),
|
||||
placeholder: '请选择往来企业类型',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'code',
|
||||
title: '往来企业编号',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '往来企业名称',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '往来企业类型',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_MERCHANT_TYPE },
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'level',
|
||||
title: '级别',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'contact',
|
||||
title: '联系人',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsMerchantApi } from '#/api/wms/md/merchant';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteMerchant,
|
||||
exportMerchant,
|
||||
getMerchantPage,
|
||||
} from '#/api/wms/md/merchant';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'WmsMerchant' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建往来企业 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑往来企业 */
|
||||
function handleEdit(row: WmsMerchantApi.Merchant) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除往来企业 */
|
||||
async function handleDelete(row: WmsMerchantApi.Merchant) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteMerchant(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出往来企业 */
|
||||
async function handleExport() {
|
||||
const data = await exportMerchant(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '往来企业.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMerchantPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsMerchantApi.Merchant>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【基础】往来企业(供应商、客户)"
|
||||
url="https://doc.iocoder.cn/wms/md/merchant/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="往来企业列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['往来企业']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:merchant:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:merchant:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['wms:merchant:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['wms:merchant:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsMerchantApi } from '#/api/wms/md/merchant';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createMerchant,
|
||||
getMerchant,
|
||||
updateMerchant,
|
||||
} from '#/api/wms/md/merchant';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'WmsMerchantForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<WmsMerchantApi.Merchant>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['往来企业'])
|
||||
: $t('ui.actionTitle.create', ['往来企业']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 120,
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as WmsMerchantApi.Merchant;
|
||||
try {
|
||||
await (formData.value?.id ? updateMerchant(data) : createMerchant(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(formApi) });
|
||||
const data = modalApi.getData<WmsMerchantApi.Merchant>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getMerchant(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/2">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as WmsWarehouseSelect } from './select.vue';
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SelectValue } from 'antdv-next';
|
||||
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Select } from 'antdv-next';
|
||||
|
||||
import { getWarehouseSimpleList } from '#/api/wms/md/warehouse';
|
||||
|
||||
defineOptions({ name: 'WmsWarehouseSelect', inheritAttrs: false });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
allowClear?: boolean;
|
||||
disabled?: boolean;
|
||||
modelValue?: number;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
allowClear: true,
|
||||
disabled: false,
|
||||
modelValue: undefined,
|
||||
placeholder: '请选择仓库',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [warehouse: undefined | WmsWarehouseApi.Warehouse];
|
||||
'update:modelValue': [value: number | undefined];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const warehouseList = ref<WmsWarehouseApi.Warehouse[]>([]);
|
||||
|
||||
const options = computed(() =>
|
||||
warehouseList.value
|
||||
.filter((warehouse) => warehouse.id !== undefined)
|
||||
.map((warehouse) => ({
|
||||
label: warehouse.name,
|
||||
value: warehouse.id,
|
||||
})),
|
||||
);
|
||||
|
||||
/** 选中变化 */
|
||||
function handleChange(value: SelectValue) {
|
||||
const warehouseId = typeof value === 'number' ? value : undefined;
|
||||
emit('update:modelValue', warehouseId);
|
||||
emit(
|
||||
'change',
|
||||
warehouseList.value.find((warehouse) => warehouse.id === warehouseId),
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询仓库精简列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
warehouseList.value = await getWarehouseSimpleList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
v-bind="$attrs"
|
||||
:allow-clear="allowClear"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
:options="options"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
class="w-full"
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { generateWmsCode } from '@vben/constants';
|
||||
|
||||
import { Button } from 'antdv-next';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
/** 新增/修改仓库的表单 */
|
||||
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '仓库名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
placeholder: '请输入仓库名称',
|
||||
},
|
||||
rules: z.string().min(1, '仓库名称不能为空').max(50),
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '仓库编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 20,
|
||||
placeholder: '请输入仓库编号',
|
||||
},
|
||||
rules: z.string().min(1, '仓库编号不能为空').max(20),
|
||||
suffix: () => {
|
||||
return h(
|
||||
Button,
|
||||
{
|
||||
type: 'default',
|
||||
onClick: () => {
|
||||
formApi?.setFieldValue('code', generateWmsCode('W'));
|
||||
},
|
||||
},
|
||||
{ default: () => '生成' },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
class: '!w-full',
|
||||
min: 0,
|
||||
},
|
||||
rules: z.number().default(0),
|
||||
},
|
||||
{
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'name',
|
||||
label: '仓库名称',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入仓库名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'code',
|
||||
label: '仓库编号',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入仓库编号',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: '仓库名称',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
title: '仓库编号',
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteWarehouse,
|
||||
exportWarehouse,
|
||||
getWarehousePage,
|
||||
} from '#/api/wms/md/warehouse';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
defineOptions({ name: 'WmsWarehouse' });
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建仓库 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑仓库 */
|
||||
function handleEdit(row: WmsWarehouseApi.Warehouse) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除仓库 */
|
||||
async function handleDelete(row: WmsWarehouseApi.Warehouse) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteWarehouse(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出仓库 */
|
||||
async function handleExport() {
|
||||
const data = await exportWarehouse(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '仓库.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getWarehousePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsWarehouseApi.Warehouse>,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【基础】仓库"
|
||||
url="https://doc.iocoder.cn/wms/md/warehouse/"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormModal @success="handleRefresh" />
|
||||
<Grid table-title="仓库列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['仓库']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:warehouse:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:warehouse:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['wms:warehouse:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['wms:warehouse:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
createWarehouse,
|
||||
getWarehouse,
|
||||
updateWarehouse,
|
||||
} from '#/api/wms/md/warehouse';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
defineOptions({ name: 'WmsWarehouseForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<WmsWarehouseApi.Warehouse>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['仓库'])
|
||||
: $t('ui.actionTitle.create', ['仓库']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as WmsWarehouseApi.Warehouse;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateWarehouse(data)
|
||||
: createWarehouse(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
formApi.setState({ schema: useFormSchema(formApi) });
|
||||
const data = modalApi.getData<WmsWarehouseApi.Warehouse>();
|
||||
if (!data || !data.id) {
|
||||
await formApi.setValues({ sort: 0 });
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getWarehouse(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-1/3">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,488 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
import type { NumberRangeValue } from '#/components/number-range-input';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import {
|
||||
buildNumberRangeSchema,
|
||||
NumberRangeInput,
|
||||
} from '#/components/number-range-input';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
getLossClass,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
roundPrice,
|
||||
sumPrice,
|
||||
sumQuantity,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
/** 表单类型 */
|
||||
export type FormType = 'create' | 'update';
|
||||
|
||||
/** 拆分数量/金额区间字段,适配后端 Min/Max 查询参数 */
|
||||
function splitNumberRange(minFieldName: string, maxFieldName: string) {
|
||||
return (
|
||||
value: NumberRangeValue | undefined,
|
||||
setValue: (fieldName: string, value: number | undefined) => void,
|
||||
) => {
|
||||
setValue(minFieldName, value?.[0]);
|
||||
setValue(maxFieldName, value?.[1]);
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/** 构建允许负数的区间搜索项,盘库盈亏数量需要支持盘亏 */
|
||||
function buildSignedNumberRangeSchema(
|
||||
label: string,
|
||||
fieldName: string,
|
||||
minFieldName: string,
|
||||
maxFieldName: string,
|
||||
precision: number,
|
||||
): VbenFormSchema {
|
||||
return {
|
||||
component: markRaw(NumberRangeInput),
|
||||
componentProps: {
|
||||
precision,
|
||||
},
|
||||
fieldName,
|
||||
label,
|
||||
valueFormat: splitNumberRange(minFieldName, maxFieldName),
|
||||
};
|
||||
}
|
||||
|
||||
/** 计算单据盈亏金额 */
|
||||
export function getOrderDifferencePrice(order: {
|
||||
actualPrice?: number;
|
||||
totalPrice?: number;
|
||||
}) {
|
||||
return roundPrice(
|
||||
Number(order.actualPrice || 0) - Number(order.totalPrice || 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** 计算明细盈亏数量 */
|
||||
export function getDetailDifferenceQuantity(detail: {
|
||||
checkQuantity?: number;
|
||||
quantity?: number;
|
||||
}) {
|
||||
return Number(detail.checkQuantity || 0) - Number(detail.quantity || 0);
|
||||
}
|
||||
|
||||
/** 计算明细实际金额 */
|
||||
export function getDetailActualPrice(detail: {
|
||||
checkQuantity?: number;
|
||||
price?: number;
|
||||
}) {
|
||||
if (
|
||||
detail.checkQuantity === undefined ||
|
||||
detail.checkQuantity === null ||
|
||||
detail.price === undefined ||
|
||||
detail.price === null
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return roundPrice(Number(detail.checkQuantity) * Number(detail.price));
|
||||
}
|
||||
|
||||
/** 计算明细盈亏金额 */
|
||||
export function getDetailDifferencePrice(detail: {
|
||||
checkQuantity?: number;
|
||||
price?: number;
|
||||
quantity?: number;
|
||||
}) {
|
||||
if (detail.price === undefined || detail.price === null) {
|
||||
return undefined;
|
||||
}
|
||||
return roundPrice(getDetailDifferenceQuantity(detail) * Number(detail.price));
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入盘库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '盘库单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||
placeholder: '请选择单据状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '单据状态',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
},
|
||||
buildSignedNumberRangeSchema(
|
||||
'盈亏数量',
|
||||
'totalQuantityRange',
|
||||
'totalQuantityMin',
|
||||
'totalQuantityMax',
|
||||
QUANTITY_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'总金额',
|
||||
'totalPriceRange',
|
||||
'totalPriceMin',
|
||||
'totalPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'实际金额',
|
||||
'actualPriceRange',
|
||||
'actualPriceMin',
|
||||
'actualPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择创建用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'creator',
|
||||
label: '创建用户',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择更新用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'updater',
|
||||
label: '更新用户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'updateTime',
|
||||
label: '更新时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表表格列 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
fixed: 'left',
|
||||
slots: { content: 'expand_content' },
|
||||
type: 'expand',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
title: '单号',
|
||||
width: 210,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||
},
|
||||
field: 'status',
|
||||
fixed: 'left',
|
||||
title: '盘库状态',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
minWidth: 180,
|
||||
title: '仓库',
|
||||
},
|
||||
{
|
||||
field: 'quantityAmount',
|
||||
minWidth: 200,
|
||||
slots: { default: 'quantityAmount' },
|
||||
title: '盈亏/金额(元)',
|
||||
},
|
||||
{
|
||||
field: 'operateInfo',
|
||||
minWidth: 280,
|
||||
slots: { default: 'operateInfo' },
|
||||
title: '操作信息',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 160,
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '盘库单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
label: '仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'orderTime',
|
||||
label: '单据日期',
|
||||
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '单据状态',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'totalQuantity',
|
||||
label: '盈亏数量',
|
||||
render: (val) =>
|
||||
h('span', { class: getLossClass(val) }, formatQuantity(val) || '-'),
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '总金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'actualPrice',
|
||||
label: '实际金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'differencePrice',
|
||||
label: '实际盈亏金额',
|
||||
render: (_val, data) => {
|
||||
const differencePrice = getOrderDifferencePrice(data || {});
|
||||
return h(
|
||||
'span',
|
||||
{ class: getLossClass(differencePrice) },
|
||||
formatPrice(differencePrice) || '-',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
render: (val, data) => val || data?.creator || '-',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'updaterName',
|
||||
label: '更新人',
|
||||
render: (val, data) => val || data?.updater || '-',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
render: (val) => val || '-',
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入盘库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '盘库单号',
|
||||
rules: z.string().min(1, '盘库单号不能为空').max(64),
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
format: 'YYYY-MM-DD',
|
||||
placeholder: '请选择单据日期',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
fieldName: 'actualPrice',
|
||||
label: '实际金额',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'col-span-2',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 选择盘库仓库表单的配置项 */
|
||||
export function useWarehouseFormSchema(
|
||||
onWarehouseChange: (warehouse: unknown) => void,
|
||||
): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
componentProps: {
|
||||
onChange: onWarehouseChange,
|
||||
},
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface CheckOrderDetailFooterRow {
|
||||
actualPrice?: number;
|
||||
checkQuantity?: number;
|
||||
differencePrice?: number;
|
||||
differenceQuantity?: number;
|
||||
quantity?: number;
|
||||
}
|
||||
type CheckOrderDetailFooterColumn = Pick<
|
||||
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||
'field'
|
||||
>;
|
||||
|
||||
/** 明细表格的合计行 */
|
||||
export function getCheckDetailFooter({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: CheckOrderDetailFooterColumn[];
|
||||
data: CheckOrderDetailFooterRow[];
|
||||
}) {
|
||||
return [
|
||||
columns.map((column, index) => {
|
||||
if (index === 0) {
|
||||
return '合计';
|
||||
}
|
||||
if (column.field === 'quantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||
}
|
||||
if (column.field === 'checkQuantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.checkQuantity);
|
||||
}
|
||||
if (column.field === 'actualPrice') {
|
||||
return formatSumPrice(data, (detail) => detail.actualPrice);
|
||||
}
|
||||
if (column.field === 'differenceQuantity') {
|
||||
return formatQuantity(
|
||||
sumQuantity(data, (detail) => detail.differenceQuantity),
|
||||
);
|
||||
}
|
||||
if (column.field === 'differencePrice') {
|
||||
return formatPrice(sumPrice(data, (detail) => detail.differencePrice));
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderDeleteStatusList,
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import {
|
||||
ACTION_ICON,
|
||||
TableAction,
|
||||
useVbenVxeGrid,
|
||||
VxeColumn,
|
||||
VxeTable,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteCheckOrder,
|
||||
exportCheckOrder,
|
||||
getCheckOrderDetailListByOrderId,
|
||||
getCheckOrderPage,
|
||||
} from '#/api/wms/order/check';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
getLossClass,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import {
|
||||
getDetailActualPrice,
|
||||
getDetailDifferencePrice,
|
||||
getDetailDifferenceQuantity,
|
||||
getOrderDifferencePrice,
|
||||
useGridColumns,
|
||||
useGridFormSchema,
|
||||
} from './data';
|
||||
import CheckOrderDetail from './modules/detail.vue';
|
||||
import CheckOrderForm from './modules/form.vue';
|
||||
import CheckOrderPrint from './modules/print.vue';
|
||||
|
||||
defineOptions({ name: 'WmsCheckOrder' });
|
||||
|
||||
const printRef = ref<InstanceType<typeof CheckOrderPrint>>();
|
||||
const detailMap = reactive<
|
||||
Record<number, WmsCheckOrderDetailApi.CheckOrderDetail[]>
|
||||
>({});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: CheckOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: CheckOrderDetail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 清空展开明细缓存 */
|
||||
function clearDetailMap() {
|
||||
for (const id of Object.keys(detailMap)) {
|
||||
delete detailMap[Number(id)];
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
clearDetailMap();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建盘库单 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ formType: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑盘库单 */
|
||||
function handleEdit(row: WmsCheckOrderApi.CheckOrder) {
|
||||
formModalApi.setData({ id: row.id!, formType: 'update' }).open();
|
||||
}
|
||||
|
||||
/** 查看盘库单详情 */
|
||||
function handleDetail(row: WmsCheckOrderApi.CheckOrder) {
|
||||
detailModalApi.setData({ id: row.id! }).open();
|
||||
}
|
||||
|
||||
/** 获取已展开行的明细 */
|
||||
function getExpandedDetails(row: WmsCheckOrderApi.CheckOrder) {
|
||||
return detailMap[row.id!] || [];
|
||||
}
|
||||
|
||||
/** 展开列表行时懒加载盘库明细 */
|
||||
async function handleExpandChange(
|
||||
row: WmsCheckOrderApi.CheckOrder,
|
||||
expanded: boolean,
|
||||
) {
|
||||
if (!expanded) {
|
||||
return;
|
||||
}
|
||||
delete detailMap[row.id!];
|
||||
detailMap[row.id!] = await getCheckOrderDetailListByOrderId(row.id!);
|
||||
}
|
||||
|
||||
/** 判断盘库单是否可修改 */
|
||||
function canUpdateCheckOrder(status?: number) {
|
||||
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 判断盘库单是否可删除 */
|
||||
function canDeleteCheckOrder(status?: number) {
|
||||
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 获取修改按钮禁用提示 */
|
||||
function getCheckOrderUpdateTip(status?: number) {
|
||||
if (canUpdateCheckOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已盘库,无法修改';
|
||||
}
|
||||
if (status === OrderStatusEnum.CANCELED) {
|
||||
return '已作废,无法修改';
|
||||
}
|
||||
return '当前状态无法修改';
|
||||
}
|
||||
|
||||
/** 获取删除按钮禁用提示 */
|
||||
function getCheckOrderDeleteTip(status?: number) {
|
||||
if (canDeleteCheckOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已盘库,无法删除';
|
||||
}
|
||||
return '当前状态无法删除';
|
||||
}
|
||||
|
||||
/** 删除盘库单 */
|
||||
async function handleDelete(row: WmsCheckOrderApi.CheckOrder) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.no]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteCheckOrder(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出盘库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportCheckOrder(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '盘库单.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
collapsed: true,
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
expandConfig: {
|
||||
padding: true,
|
||||
},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getCheckOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsCheckOrderApi.CheckOrder>,
|
||||
gridEvents: {
|
||||
toggleRowExpand: ({
|
||||
expanded,
|
||||
row,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
row: WmsCheckOrderApi.CheckOrder;
|
||||
}) => {
|
||||
handleExpandChange(row, expanded);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【单据】盘库"
|
||||
url="https://doc.iocoder.cn/wms/order/check/"
|
||||
/>
|
||||
</template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<CheckOrderPrint ref="printRef" />
|
||||
|
||||
<Grid table-title="盘库单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<VxeTable
|
||||
:data="getExpandedDetails(row)"
|
||||
border
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="账面数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.quantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="实盘数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.checkQuantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="实际金额(元)" align="right" width="140">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(getDetailActualPrice(detail)) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="盈亏数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
<span :class="getLossClass(getDetailDifferenceQuantity(detail))">
|
||||
{{ formatQuantity(getDetailDifferenceQuantity(detail)) }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="实际盈亏金额(元)" align="right" width="160">
|
||||
<template #default="{ row: detail }">
|
||||
<span :class="getLossClass(getDetailDifferencePrice(detail))">
|
||||
{{ formatPrice(getDetailDifferencePrice(detail)) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['盘库单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:check-order:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:check-order:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<div>
|
||||
单号:
|
||||
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #quantityAmount="{ row }">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>盈亏数:</span>
|
||||
<span :class="getLossClass(row.totalQuantity)">
|
||||
{{ formatQuantity(row.totalQuantity) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>总金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>实际金额:</span>
|
||||
<span>{{ formatPrice(row.actualPrice) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>盈亏金额:</span>
|
||||
<span :class="getLossClass(getOrderDifferencePrice(row))">
|
||||
{{ formatPrice(getOrderDifferencePrice(row)) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #operateInfo="{ row }">
|
||||
<div>
|
||||
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||
{{ row.updaterName || row.updater || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: !canUpdateCheckOrder(row.status),
|
||||
tooltip: getCheckOrderUpdateTip(row.status),
|
||||
auth: ['wms:check-order:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: !canDeleteCheckOrder(row.status),
|
||||
tooltip: getCheckOrderDeleteTip(row.status),
|
||||
auth: ['wms:check-order:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
type: 'link',
|
||||
auth: ['wms:check-order:query'],
|
||||
onClick: () => printRef?.print(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getCheckOrder,
|
||||
getCheckOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/check';
|
||||
import { useDescription } from '#/components/description';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
getLossClass,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import {
|
||||
getCheckDetailFooter,
|
||||
getDetailActualPrice,
|
||||
getDetailDifferencePrice,
|
||||
getDetailDifferenceQuantity,
|
||||
useDetailSchema,
|
||||
} from '../data';
|
||||
|
||||
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||
actualPrice?: number;
|
||||
differencePrice?: number;
|
||||
differenceQuantity?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsCheckOrderDetail' });
|
||||
|
||||
const detailData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||
|
||||
/** 详情明细补齐实际金额和盈亏字段,便于表格与 footer 统一渲染 */
|
||||
const detailRows = computed<DetailRow[]>(() =>
|
||||
(detailData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
actualPrice: getDetailActualPrice(detail),
|
||||
differencePrice: getDetailDifferencePrice(detail),
|
||||
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||
})),
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: true,
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
useCard: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
detailData.value = {};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const order = await getCheckOrder(data.id);
|
||||
const details =
|
||||
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||
detailData.value = { ...order, details };
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="盘库单详情"
|
||||
class="w-2/3"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<Descriptions :data="detailData" />
|
||||
<VxeTable
|
||||
:data="detailRows"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getCheckDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="账面数量" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="checkQuantity"
|
||||
title="实盘数量"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.checkQuantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="actualPrice"
|
||||
title="实际金额(元)"
|
||||
align="right"
|
||||
width="140"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.actualPrice) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="differenceQuantity"
|
||||
title="盈亏数量"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="getLossClass(row.differenceQuantity)">
|
||||
{{ formatQuantity(row.differenceQuantity) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="differencePrice"
|
||||
title="实际盈亏金额(元)"
|
||||
align="right"
|
||||
width="160"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="getLossClass(row.differencePrice)">
|
||||
{{ formatPrice(row.differencePrice) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,741 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormType } from '../data';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import type { WmsInventoryApi } from '#/api/wms/inventory';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { isEqual } from '@vben/utils';
|
||||
|
||||
import { InputNumber, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getInventoryList } from '#/api/wms/inventory';
|
||||
import {
|
||||
cancelCheckOrder,
|
||||
completeCheckOrder,
|
||||
createCheckOrder,
|
||||
getCheckOrder,
|
||||
getCheckOrderDetailListByOrderId,
|
||||
updateCheckOrder,
|
||||
} from '#/api/wms/order/check';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsItemSkuSelect } from '#/views/wms/md/item/sku/components';
|
||||
import {
|
||||
dividePrice,
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
getLossClass,
|
||||
multiplyPrice,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
sumPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||
|
||||
import {
|
||||
getCheckDetailFooter,
|
||||
getDetailDifferencePrice,
|
||||
getDetailDifferenceQuantity,
|
||||
useFormSchema,
|
||||
useWarehouseFormSchema,
|
||||
} from '../data';
|
||||
|
||||
interface CheckInventoryRow extends WmsInventoryApi.Inventory {
|
||||
availableQuantity?: number;
|
||||
price?: number;
|
||||
}
|
||||
|
||||
interface DetailRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||
actualPrice?: number;
|
||||
differencePrice?: number;
|
||||
differenceQuantity?: number;
|
||||
seq: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsCheckOrderForm' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||
const formType = ref<FormType>('create');
|
||||
const originalSubmitData = ref<WmsCheckOrderApi.CheckOrder>();
|
||||
const details = ref<DetailRow[]>([]);
|
||||
const detailTableRef = ref<VxeTableInstance>();
|
||||
const skuSelectRef = ref<InstanceType<typeof WmsItemSkuSelect>>();
|
||||
const selectingWarehouse = ref(false);
|
||||
const warehouseName = ref<string>();
|
||||
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formType.value === 'update'
|
||||
? $t('ui.actionTitle.edit', ['盘库单'])
|
||||
: $t('ui.actionTitle.create', ['盘库单']);
|
||||
});
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
return selectingWarehouse.value ? '选择盘库仓库' : getTitle.value;
|
||||
});
|
||||
const modalClass = computed(() => {
|
||||
return selectingWarehouse.value ? 'w-[420px]' : 'w-3/4';
|
||||
});
|
||||
|
||||
const isPrepareOrder = computed(() => {
|
||||
return (
|
||||
!formData.value?.id ||
|
||||
(formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status))
|
||||
);
|
||||
});
|
||||
const isSavedPrepareOrder = computed(() => {
|
||||
return (
|
||||
!!formData.value?.id &&
|
||||
formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status)
|
||||
);
|
||||
});
|
||||
|
||||
/** 表单顶部实际金额来自明细合计,保持和 vue3 的只读汇总字段一致 */
|
||||
const actualPrice = computed(() =>
|
||||
sumPrice(details.value, (detail) => getDetailActualPrice(detail)),
|
||||
);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
const [WarehouseForm, warehouseFormApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useWarehouseFormSchema(handleWarehouseSelect),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 记录新增盘库单前置选择的仓库名称 */
|
||||
function handleWarehouseSelect(warehouse: unknown) {
|
||||
warehouseName.value = (
|
||||
warehouse as undefined | WmsWarehouseApi.Warehouse
|
||||
)?.name;
|
||||
}
|
||||
|
||||
/** 初始化新增盘库单草稿 */
|
||||
async function initCreateForm(warehouse: {
|
||||
warehouseId?: number;
|
||||
warehouseName?: string;
|
||||
}) {
|
||||
formData.value = {
|
||||
details: [],
|
||||
no: generateOrderNo('PK'),
|
||||
status: OrderStatusEnum.PREPARE,
|
||||
warehouseId: warehouse.warehouseId,
|
||||
warehouseName: warehouse.warehouseName,
|
||||
};
|
||||
setDetails([]);
|
||||
await formApi.resetForm();
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
syncActualPriceField();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
}
|
||||
|
||||
/** 确认前置选择仓库,进入主表单 */
|
||||
async function handleWarehouseConfirm() {
|
||||
const { valid } = await warehouseFormApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const values = (await warehouseFormApi.getValues()) as {
|
||||
warehouseId?: number;
|
||||
};
|
||||
selectingWarehouse.value = false;
|
||||
await nextTick();
|
||||
await initCreateForm({
|
||||
warehouseId: values.warehouseId,
|
||||
warehouseName: warehouseName.value,
|
||||
});
|
||||
}
|
||||
|
||||
/** 同步表单顶部只读实际金额 */
|
||||
function syncActualPriceField() {
|
||||
void formApi.setFieldValue(
|
||||
'actualPrice',
|
||||
formatPrice(actualPrice.value) || '0.00',
|
||||
);
|
||||
}
|
||||
|
||||
watch(actualPrice, syncActualPriceField);
|
||||
|
||||
/** 计算明细实际金额 */
|
||||
function getDetailActualPrice(detail: DetailRow) {
|
||||
return (
|
||||
detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price)
|
||||
);
|
||||
}
|
||||
|
||||
/** 刷新明细行的盈亏数据 */
|
||||
function refreshDetailCalculatedFields(detail: DetailRow) {
|
||||
detail.differenceQuantity = getDetailDifferenceQuantity(detail);
|
||||
detail.differencePrice = getDetailDifferencePrice(detail);
|
||||
}
|
||||
|
||||
/** 标准化明细行,补齐本地序号和计算字段 */
|
||||
function normalizeDetail(
|
||||
detail: WmsCheckOrderDetailApi.CheckOrderDetail & { actualPrice?: number },
|
||||
): DetailRow {
|
||||
detailSeq += 1;
|
||||
const row: DetailRow = {
|
||||
...detail,
|
||||
actualPrice:
|
||||
detail.actualPrice ?? multiplyPrice(detail.checkQuantity, detail.price),
|
||||
seq: detailSeq,
|
||||
};
|
||||
refreshDetailCalculatedFields(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 根据库存构建盘库明细 */
|
||||
function buildDetail(inventory: CheckInventoryRow): DetailRow {
|
||||
return normalizeDetail({
|
||||
actualPrice: multiplyPrice(inventory.availableQuantity, inventory.price),
|
||||
availableQuantity: inventory.availableQuantity,
|
||||
checkQuantity: inventory.availableQuantity,
|
||||
id: undefined,
|
||||
inventoryId: inventory.id,
|
||||
itemCode: inventory.itemCode,
|
||||
itemId: inventory.itemId,
|
||||
itemName: inventory.itemName,
|
||||
price: inventory.price,
|
||||
quantity: inventory.availableQuantity,
|
||||
skuCode: inventory.skuCode,
|
||||
skuId: inventory.skuId,
|
||||
skuName: inventory.skuName,
|
||||
unit: inventory.unit,
|
||||
warehouseId: inventory.warehouseId,
|
||||
warehouseName: inventory.warehouseName,
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建零库存盘库明细,用于盘点仓库内暂无库存的商品 */
|
||||
function buildZeroInventoryDetail(sku: WmsItemSkuApi.ItemSku): DetailRow {
|
||||
return normalizeDetail({
|
||||
actualPrice: 0,
|
||||
availableQuantity: 0,
|
||||
checkQuantity: 0,
|
||||
id: undefined,
|
||||
inventoryId: undefined,
|
||||
itemCode: sku.itemCode,
|
||||
itemId: sku.itemId,
|
||||
itemName: sku.itemName,
|
||||
price: sku.costPrice,
|
||||
quantity: 0,
|
||||
skuCode: sku.code,
|
||||
skuId: sku.id,
|
||||
skuName: sku.name,
|
||||
unit: sku.unit,
|
||||
warehouseId: formData.value.warehouseId,
|
||||
warehouseName: formData.value.warehouseName,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置盘库明细 */
|
||||
function setDetails(list?: WmsCheckOrderDetailApi.CheckOrderDetail[]) {
|
||||
detailSeq = 0;
|
||||
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 刷新明细合计行 */
|
||||
async function refreshDetailFooter() {
|
||||
await nextTick();
|
||||
await detailTableRef.value?.updateFooter();
|
||||
}
|
||||
|
||||
/** 导入当前仓库的全部库存余额 */
|
||||
async function handleImportAllInventory() {
|
||||
if (!formData.value.warehouseId) {
|
||||
message.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
if (details.value.length > 0) {
|
||||
await confirm('导入仓库库存会覆盖当前盘库明细,是否继续?');
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const inventories = await loadWarehouseInventoryList();
|
||||
details.value = inventories.map((inventory) =>
|
||||
buildDetail({ ...inventory, availableQuantity: inventory.quantity }),
|
||||
);
|
||||
await refreshDetailFooter();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开盘点商品添加弹窗,已导入/已添加 SKU 在选择器内禁选 */
|
||||
function handleAddSkuInventory() {
|
||||
if (!formData.value.warehouseId) {
|
||||
message.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
skuSelectRef.value?.open(getSelectedSkuIds(), {
|
||||
multiple: false,
|
||||
});
|
||||
}
|
||||
|
||||
/** 选择商品 SKU */
|
||||
async function handleSelectSku(skus: WmsItemSkuApi.ItemSku[]) {
|
||||
if (skus.length === 0) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const warehouseInventoryMap = await getWarehouseInventoryMap();
|
||||
const selectedSkuIds = new Set(getSelectedSkuIds());
|
||||
let changed = false;
|
||||
for (const sku of skus) {
|
||||
if (!sku.id || selectedSkuIds.has(sku.id)) {
|
||||
continue;
|
||||
}
|
||||
const inventory = warehouseInventoryMap.get(sku.id);
|
||||
details.value.push(
|
||||
inventory
|
||||
? buildDetail({ ...inventory, availableQuantity: inventory.quantity })
|
||||
: buildZeroInventoryDetail(sku),
|
||||
);
|
||||
selectedSkuIds.add(sku.id);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
await refreshDetailFooter();
|
||||
}
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得已导入/已添加 SKU 编号,避免重复盘点同一规格 */
|
||||
function getSelectedSkuIds() {
|
||||
return details.value
|
||||
.map((detail) => detail.skuId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
}
|
||||
|
||||
/** 查询当前仓库全部库存余额 */
|
||||
async function loadWarehouseInventoryList(): Promise<CheckInventoryRow[]> {
|
||||
return (await getInventoryList({
|
||||
warehouseId: formData.value.warehouseId!,
|
||||
})) as CheckInventoryRow[];
|
||||
}
|
||||
|
||||
/** 获得当前仓库 SKU 对应库存余额,用于添加单个 SKU 时带入账面库存 */
|
||||
async function getWarehouseInventoryMap(): Promise<
|
||||
Map<number, CheckInventoryRow>
|
||||
> {
|
||||
const inventories = await loadWarehouseInventoryList();
|
||||
return new Map(
|
||||
inventories
|
||||
.filter((inventory) => !!inventory.skuId)
|
||||
.map((inventory) => [inventory.skuId!, inventory] as const),
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除商品明细 */
|
||||
function handleDeleteDetail(row: DetailRow) {
|
||||
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
details.value.splice(index, 1);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细实盘数量变化 */
|
||||
function handleDetailCheckQuantityChange(detail: DetailRow) {
|
||||
if (detail.price !== undefined && detail.price !== null) {
|
||||
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||
} else {
|
||||
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||
}
|
||||
refreshDetailCalculatedFields(detail);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细单价变化 */
|
||||
function handleDetailPriceChange(detail: DetailRow) {
|
||||
detail.actualPrice = multiplyPrice(detail.checkQuantity, detail.price);
|
||||
refreshDetailCalculatedFields(detail);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细实际金额变化 */
|
||||
function handleDetailActualPriceChange(detail: DetailRow) {
|
||||
detail.price = dividePrice(detail.actualPrice, detail.checkQuantity);
|
||||
refreshDetailCalculatedFields(detail);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 校验商品明细 */
|
||||
function validateDetails(required = false) {
|
||||
if (details.value.length === 0) {
|
||||
if (required) {
|
||||
message.error('至少包含一条盘库明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (let index = 0; index < details.value.length; index += 1) {
|
||||
const detail = details.value[index]!;
|
||||
if (
|
||||
detail.checkQuantity === undefined ||
|
||||
detail.checkQuantity === null ||
|
||||
detail.checkQuantity < 0
|
||||
) {
|
||||
message.error(`第 ${index + 1} 行明细实盘数量不能小于 0`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 构建提交用的明细数据 */
|
||||
function buildSubmitDetails() {
|
||||
return details.value.map((row) => {
|
||||
const {
|
||||
actualPrice: _actualPrice,
|
||||
availableQuantity: _availableQuantity,
|
||||
differencePrice: _differencePrice,
|
||||
differenceQuantity: _differenceQuantity,
|
||||
seq: _seq,
|
||||
...detail
|
||||
} = row;
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建提交用的单据数据 */
|
||||
async function buildSubmitData(): Promise<WmsCheckOrderApi.CheckOrder> {
|
||||
const values = (await formApi.getValues()) as WmsCheckOrderApi.CheckOrder;
|
||||
const {
|
||||
actualPrice: _formActualPrice,
|
||||
details: _formDetails,
|
||||
totalPrice: _formTotalPrice,
|
||||
totalQuantity: _formTotalQuantity,
|
||||
...submitValues
|
||||
} = values;
|
||||
const {
|
||||
actualPrice: _actualPrice,
|
||||
details: _details,
|
||||
totalPrice: _totalPrice,
|
||||
totalQuantity: _totalQuantity,
|
||||
...order
|
||||
} = formData.value;
|
||||
return {
|
||||
...order,
|
||||
...submitValues,
|
||||
details: buildSubmitDetails(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 完成盘库 */
|
||||
async function handleFormComplete() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认完成盘库?完成后将更新库存。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = await buildSubmitData();
|
||||
if (!isEqual(data, originalSubmitData.value)) {
|
||||
await updateCheckOrder(data);
|
||||
}
|
||||
await completeCheckOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('盘库成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 作废盘库单 */
|
||||
async function handleFormCancel() {
|
||||
if (!formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认作废该盘库单?作废后不可恢复。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
await cancelCheckOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('作废成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (selectingWarehouse.value) {
|
||||
await handleWarehouseConfirm();
|
||||
return;
|
||||
}
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await buildSubmitData();
|
||||
try {
|
||||
await (formType.value === 'update'
|
||||
? updateCheckOrder(data)
|
||||
: createCheckOrder(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = {};
|
||||
originalSubmitData.value = undefined;
|
||||
selectingWarehouse.value = false;
|
||||
warehouseName.value = undefined;
|
||||
setDetails([]);
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ formType: FormType; id?: number }>();
|
||||
formType.value = data.formType;
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
const order = await getCheckOrder(data.id);
|
||||
const orderDetails =
|
||||
order.details || (await getCheckOrderDetailListByOrderId(data.id));
|
||||
formData.value = { ...order, details: orderDetails };
|
||||
setDetails(orderDetails);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
syncActualPriceField();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 新增时先选择盘库仓库,再进入主表单
|
||||
selectingWarehouse.value = true;
|
||||
warehouseName.value = undefined;
|
||||
formData.value = {};
|
||||
setDetails([]);
|
||||
await warehouseFormApi.resetForm();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="modalTitle"
|
||||
:class="modalClass"
|
||||
:show-confirm-button="selectingWarehouse || isPrepareOrder"
|
||||
>
|
||||
<template v-if="selectingWarehouse" #confirmText>开始盘库</template>
|
||||
<WarehouseForm v-if="selectingWarehouse" class="mx-4" />
|
||||
<div v-else class="mx-4">
|
||||
<Form />
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">盘库明细</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '导入仓库库存',
|
||||
disabled: !formData.warehouseId,
|
||||
onClick: handleImportAllInventory,
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: '添加盘点商品',
|
||||
disabled: !formData.warehouseId,
|
||||
onClick: handleAddSkuInventory,
|
||||
type: 'primary',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<VxeTable
|
||||
ref="detailTableRef"
|
||||
:data="details"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getCheckDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="quantity"
|
||||
title="账面库存"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="price" title="单价(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.price"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="单价"
|
||||
@change="handleDetailPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="checkQuantity" title="实际库存" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.checkQuantity"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="QUANTITY_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="数量"
|
||||
@change="handleDetailCheckQuantityChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="actualPrice" title="实际金额(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.actualPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="金额"
|
||||
@change="handleDetailActualPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="differenceQuantity"
|
||||
title="盈亏数"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="getLossClass(row.differenceQuantity)">
|
||||
{{ formatQuantity(row.differenceQuantity) }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="differencePrice"
|
||||
title="实际盈亏金额(元)"
|
||||
align="right"
|
||||
width="160"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="getLossClass(row.differencePrice)">
|
||||
{{ formatPrice(row.differencePrice) || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: handleDeleteDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div
|
||||
v-if="!selectingWarehouse && isSavedPrepareOrder"
|
||||
class="flex flex-auto items-center gap-2"
|
||||
>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '完成盘库',
|
||||
type: 'primary',
|
||||
auth: ['wms:check-order:complete'],
|
||||
onClick: handleFormComplete,
|
||||
},
|
||||
{
|
||||
label: '作废',
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
auth: ['wms:check-order:cancel'],
|
||||
onClick: handleFormCancel,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<WmsItemSkuSelect ref="skuSelectRef" @change="handleSelectSku" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsCheckOrderApi } from '#/api/wms/order/check';
|
||||
import type { WmsCheckOrderDetailApi } from '#/api/wms/order/check/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
getCheckOrder,
|
||||
getCheckOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/check';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
getLossClass,
|
||||
sumPrice,
|
||||
sumQuantity,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import {
|
||||
getDetailActualPrice,
|
||||
getDetailDifferencePrice,
|
||||
getDetailDifferenceQuantity,
|
||||
getOrderDifferencePrice,
|
||||
} from '../data';
|
||||
|
||||
interface PrintRow extends WmsCheckOrderDetailApi.CheckOrderDetail {
|
||||
actualPrice?: number;
|
||||
differencePrice?: number;
|
||||
differenceQuantity?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsCheckOrderPrint' });
|
||||
|
||||
const printData = ref<WmsCheckOrderApi.CheckOrder>({});
|
||||
const tableColumnCount = 8;
|
||||
|
||||
/** 打印明细补齐实际金额和盈亏字段,避免模板重复计算 */
|
||||
const printRows = computed<PrintRow[]>(() =>
|
||||
(printData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
actualPrice: getDetailActualPrice(detail),
|
||||
differencePrice: getDetailDifferencePrice(detail),
|
||||
differenceQuantity: getDetailDifferenceQuantity(detail),
|
||||
})),
|
||||
);
|
||||
|
||||
/** 打印合计盈亏数量 */
|
||||
const totalDifferenceQuantity = computed(() =>
|
||||
sumQuantity(printRows.value, (detail) => detail.differenceQuantity),
|
||||
);
|
||||
/** 打印合计盈亏金额 */
|
||||
const totalDifferencePrice = computed(() =>
|
||||
sumPrice(printRows.value, (detail) => detail.differencePrice),
|
||||
);
|
||||
|
||||
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||
function waitForPaint() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 退出打印模式,恢复当前页面显示 */
|
||||
function removePrintMode() {
|
||||
document.body.classList.remove('wms-check-order-printing');
|
||||
}
|
||||
|
||||
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||
function getPrintDictLabel(dictType: string, value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
return getDictLabel(dictType, value) || '-';
|
||||
}
|
||||
|
||||
/** 打印盘库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||
async function print(id: number) {
|
||||
const order = await getCheckOrder(id);
|
||||
const details = order.details || (await getCheckOrderDetailListByOrderId(id));
|
||||
printData.value = { ...order, details };
|
||||
await nextTick();
|
||||
await waitForPaint();
|
||||
document.body.classList.add('wms-check-order-printing');
|
||||
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||
window.print();
|
||||
}
|
||||
|
||||
defineExpose({ print });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="wmsCheckOrderPrint"
|
||||
class="wms-check-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||
>
|
||||
<div class="relative mb-2">
|
||||
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||
盘库单
|
||||
</h2>
|
||||
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||
<Barcode
|
||||
:content="printData.no"
|
||||
:display-value="false"
|
||||
:format="BarcodeFormatEnum.CODE39"
|
||||
:height="40"
|
||||
:width="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||
<div>盘库单号:{{ printData.no || '-' }}</div>
|
||||
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||
<div>
|
||||
盘库状态:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status)
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
盈亏数量:
|
||||
<span :class="getLossClass(printData.totalQuantity)">
|
||||
{{ formatQuantity(printData.totalQuantity) || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||
<div>实际金额:{{ formatPrice(printData.actualPrice) || '-' }}</div>
|
||||
<div>
|
||||
实际盈亏金额:
|
||||
<span :class="getLossClass(getOrderDifferencePrice(printData))">
|
||||
{{ formatPrice(getOrderDifferencePrice(printData)) || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||
{{ printData.creatorName || printData.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||
{{ printData.updaterName || printData.updater || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
商品信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
规格信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
账面库存
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
单价(元)
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
实际库存
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
实际金额(元)
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
盈亏数
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
实际盈亏金额(元)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs">
|
||||
编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs">
|
||||
编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.quantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.checkQuantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.actualPrice) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
<span :class="getLossClass(detail.differenceQuantity)">
|
||||
{{ formatQuantity(detail.differenceQuantity) || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
<span :class="getLossClass(detail.differencePrice)">
|
||||
{{ formatPrice(detail.differencePrice) || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length > 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||
colspan="2"
|
||||
>
|
||||
合计
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
></td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{
|
||||
formatSumQuantity(printRows, (detail) => detail.checkQuantity)
|
||||
}}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumPrice(printRows, (detail) => detail.actualPrice) }}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
<span :class="getLossClass(totalDifferenceQuantity)">
|
||||
{{ formatQuantity(totalDifferenceQuantity) }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
<span :class="getLossClass(totalDifferencePrice)">
|
||||
{{ formatPrice(totalDifferencePrice) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length === 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||
:colspan="tableColumnCount"
|
||||
>
|
||||
暂无明细
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@page {
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body.wms-check-order-printing) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
:global(body.wms-check-order-printing *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
:global(body.wms-check-order-printing .wms-check-order-print),
|
||||
:global(body.wms-check-order-printing .wms-check-order-print *) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
:global(body.wms-check-order-printing .wms-check-order-print) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { buildNumberRangeSchema } from '#/components/number-range-input';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
/** 表单类型 */
|
||||
export type FormType = 'create' | 'update';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入移库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '移库单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||
placeholder: '请选择单据状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '单据状态',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'sourceWarehouseId',
|
||||
label: '来源仓库',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'targetWarehouseId',
|
||||
label: '目标仓库',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
},
|
||||
buildNumberRangeSchema(
|
||||
'数量',
|
||||
'totalQuantityRange',
|
||||
'totalQuantityMin',
|
||||
'totalQuantityMax',
|
||||
QUANTITY_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'总金额',
|
||||
'totalPriceRange',
|
||||
'totalPriceMin',
|
||||
'totalPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择创建用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'creator',
|
||||
label: '创建用户',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择更新用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'updater',
|
||||
label: '更新用户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'updateTime',
|
||||
label: '更新时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表表格列 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
fixed: 'left',
|
||||
slots: { content: 'expand_content' },
|
||||
type: 'expand',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
title: '单号',
|
||||
width: 210,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||
},
|
||||
field: 'status',
|
||||
fixed: 'left',
|
||||
title: '移库状态',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
field: 'sourceWarehouseName',
|
||||
minWidth: 180,
|
||||
title: '来源仓库',
|
||||
},
|
||||
{
|
||||
field: 'targetWarehouseName',
|
||||
minWidth: 180,
|
||||
title: '目标仓库',
|
||||
},
|
||||
{
|
||||
field: 'quantityAmount',
|
||||
minWidth: 180,
|
||||
slots: { default: 'quantityAmount' },
|
||||
title: '总数量/总金额(元)',
|
||||
},
|
||||
{
|
||||
field: 'operateInfo',
|
||||
minWidth: 260,
|
||||
slots: { default: 'operateInfo' },
|
||||
title: '操作信息',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 160,
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '移库单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'sourceWarehouseName',
|
||||
label: '来源仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'targetWarehouseName',
|
||||
label: '目标仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '单据状态',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'orderTime',
|
||||
label: '单据日期',
|
||||
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalQuantity',
|
||||
label: '总数量',
|
||||
render: (val) => formatQuantity(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '总金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
render: (val, data) => val || data?.creator || '-',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'updaterName',
|
||||
label: '更新人',
|
||||
render: (val, data) => val || data?.updater || '-',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
render: (val) => val || '-',
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface MovementFormSchemaOptions {
|
||||
onSourceWarehouseChange: (warehouse?: WmsWarehouseApi.Warehouse) => void;
|
||||
onTargetWarehouseChange: (warehouse?: WmsWarehouseApi.Warehouse) => void;
|
||||
}
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema({
|
||||
onSourceWarehouseChange,
|
||||
onTargetWarehouseChange,
|
||||
}: MovementFormSchemaOptions): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入移库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '移库单号',
|
||||
rules: z.string().min(1, '移库单号不能为空').max(64),
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
componentProps: {
|
||||
onChange: onSourceWarehouseChange,
|
||||
},
|
||||
fieldName: 'sourceWarehouseId',
|
||||
label: '来源仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
componentProps: {
|
||||
onChange: onTargetWarehouseChange,
|
||||
},
|
||||
fieldName: 'targetWarehouseId',
|
||||
label: '目标仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
format: 'YYYY-MM-DD',
|
||||
placeholder: '请选择单据日期',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'col-span-2',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface MovementOrderDetailFooterRow {
|
||||
quantity?: number;
|
||||
totalPrice?: number;
|
||||
}
|
||||
type MovementOrderDetailFooterColumn = Pick<
|
||||
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||
'field'
|
||||
>;
|
||||
|
||||
/** 明细表格的合计行 */
|
||||
export function getDetailFooter({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: MovementOrderDetailFooterColumn[];
|
||||
data: MovementOrderDetailFooterRow[];
|
||||
}) {
|
||||
return [
|
||||
columns.map((column, index) => {
|
||||
if (index === 0) {
|
||||
return '合计';
|
||||
}
|
||||
if (column.field === 'quantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||
}
|
||||
if (column.field === 'totalPrice') {
|
||||
return formatSumPrice(data, (detail) => detail.totalPrice);
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsMovementOrderApi } from '#/api/wms/order/movement';
|
||||
import type { WmsMovementOrderDetailApi } from '#/api/wms/order/movement/detail';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderDeleteStatusList,
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import {
|
||||
ACTION_ICON,
|
||||
TableAction,
|
||||
useVbenVxeGrid,
|
||||
VxeColumn,
|
||||
VxeTable,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteMovementOrder,
|
||||
exportMovementOrder,
|
||||
getMovementOrderDetailListByOrderId,
|
||||
getMovementOrderPage,
|
||||
} from '#/api/wms/order/movement';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import MovementOrderDetail from './modules/detail.vue';
|
||||
import MovementOrderForm from './modules/form.vue';
|
||||
import MovementOrderPrint from './modules/print.vue';
|
||||
|
||||
defineOptions({ name: 'WmsMovementOrder' });
|
||||
|
||||
const printRef = ref<InstanceType<typeof MovementOrderPrint>>();
|
||||
const detailMap = reactive<
|
||||
Record<number, WmsMovementOrderDetailApi.MovementOrderDetail[]>
|
||||
>({});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: MovementOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: MovementOrderDetail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 清空展开明细缓存 */
|
||||
function clearDetailMap() {
|
||||
for (const id of Object.keys(detailMap)) {
|
||||
delete detailMap[Number(id)];
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
clearDetailMap();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建移库单 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ formType: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑移库单 */
|
||||
function handleEdit(row: WmsMovementOrderApi.MovementOrder) {
|
||||
formModalApi.setData({ id: row.id!, formType: 'update' }).open();
|
||||
}
|
||||
|
||||
/** 查看移库单详情 */
|
||||
function handleDetail(row: WmsMovementOrderApi.MovementOrder) {
|
||||
detailModalApi.setData({ id: row.id! }).open();
|
||||
}
|
||||
|
||||
/** 计算单据明细金额 */
|
||||
function getDetailTotalPrice(
|
||||
detail: WmsMovementOrderDetailApi.MovementOrderDetail,
|
||||
) {
|
||||
return detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price);
|
||||
}
|
||||
|
||||
/** 获取已展开行的明细 */
|
||||
function getExpandedDetails(row: WmsMovementOrderApi.MovementOrder) {
|
||||
return detailMap[row.id!] || [];
|
||||
}
|
||||
|
||||
/** 展开列表行时懒加载移库明细 */
|
||||
async function handleExpandChange(
|
||||
row: WmsMovementOrderApi.MovementOrder,
|
||||
expanded: boolean,
|
||||
) {
|
||||
if (!expanded) {
|
||||
return;
|
||||
}
|
||||
delete detailMap[row.id!];
|
||||
detailMap[row.id!] = await getMovementOrderDetailListByOrderId(row.id!);
|
||||
}
|
||||
|
||||
/** 判断移库单是否可修改 */
|
||||
function canUpdateMovementOrder(status?: number) {
|
||||
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 判断移库单是否可删除 */
|
||||
function canDeleteMovementOrder(status?: number) {
|
||||
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 获取修改按钮禁用提示 */
|
||||
function getMovementOrderUpdateTip(status?: number) {
|
||||
if (canUpdateMovementOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已移库,无法修改';
|
||||
}
|
||||
if (status === OrderStatusEnum.CANCELED) {
|
||||
return '已作废,无法修改';
|
||||
}
|
||||
return '当前状态无法修改';
|
||||
}
|
||||
|
||||
/** 获取删除按钮禁用提示 */
|
||||
function getMovementOrderDeleteTip(status?: number) {
|
||||
if (canDeleteMovementOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已移库,无法删除';
|
||||
}
|
||||
return '当前状态无法删除';
|
||||
}
|
||||
|
||||
/** 删除移库单 */
|
||||
async function handleDelete(row: WmsMovementOrderApi.MovementOrder) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.no]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteMovementOrder(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出移库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportMovementOrder(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '移库单.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
collapsed: true,
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
expandConfig: {
|
||||
padding: true,
|
||||
},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getMovementOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsMovementOrderApi.MovementOrder>,
|
||||
gridEvents: {
|
||||
toggleRowExpand: ({
|
||||
expanded,
|
||||
row,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
row: WmsMovementOrderApi.MovementOrder;
|
||||
}) => {
|
||||
handleExpandChange(row, expanded);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【单据】移库"
|
||||
url="https://doc.iocoder.cn/wms/order/movement/"
|
||||
/>
|
||||
</template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<MovementOrderPrint ref="printRef" />
|
||||
|
||||
<Grid table-title="移库单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<VxeTable
|
||||
:data="getExpandedDetails(row)"
|
||||
border
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="移库数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.quantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="金额(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(getDetailTotalPrice(detail)) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['移库单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:movement-order:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:movement-order:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<div>
|
||||
单号:
|
||||
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #quantityAmount="{ row }">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>数量:</span>
|
||||
<span>{{ formatQuantity(row.totalQuantity) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #operateInfo="{ row }">
|
||||
<div>
|
||||
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||
{{ row.updaterName || row.updater || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: !canUpdateMovementOrder(row.status),
|
||||
tooltip: getMovementOrderUpdateTip(row.status),
|
||||
auth: ['wms:movement-order:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: !canDeleteMovementOrder(row.status),
|
||||
tooltip: getMovementOrderDeleteTip(row.status),
|
||||
auth: ['wms:movement-order:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
type: 'link',
|
||||
auth: ['wms:movement-order:query'],
|
||||
onClick: () => printRef?.print(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsMovementOrderApi } from '#/api/wms/order/movement';
|
||||
import type { WmsMovementOrderDetailApi } from '#/api/wms/order/movement/detail';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getMovementOrder,
|
||||
getMovementOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/movement';
|
||||
import { useDescription } from '#/components/description';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { getDetailFooter, useDetailSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsMovementOrderDetailApi.MovementOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsMovementOrderDetail' });
|
||||
|
||||
const detailData = ref<WmsMovementOrderApi.MovementOrder>({});
|
||||
|
||||
const detailRows = computed<DetailRow[]>(() =>
|
||||
(detailData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: true,
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
useCard: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
detailData.value = {};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const order = await getMovementOrder(data.id);
|
||||
const details =
|
||||
order.details || (await getMovementOrderDetailListByOrderId(data.id));
|
||||
detailData.value = { ...order, details };
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="移库单详情"
|
||||
class="w-2/3"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<Descriptions :data="detailData" />
|
||||
<VxeTable
|
||||
:data="detailRows"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="数量" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="totalPrice"
|
||||
title="金额(元)"
|
||||
align="right"
|
||||
width="140"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.totalPrice) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormType } from '../data';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
import type { WmsMovementOrderApi } from '#/api/wms/order/movement';
|
||||
import type { WmsMovementOrderDetailApi } from '#/api/wms/order/movement/detail';
|
||||
import type { InventorySelectRow } from '#/views/wms/inventory/components';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { isEqual } from '@vben/utils';
|
||||
|
||||
import { InputNumber, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelMovementOrder,
|
||||
completeMovementOrder,
|
||||
createMovementOrder,
|
||||
getMovementOrder,
|
||||
getMovementOrderDetailListByOrderId,
|
||||
updateMovementOrder,
|
||||
} from '#/api/wms/order/movement';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsInventorySelect } from '#/views/wms/inventory/components';
|
||||
import {
|
||||
dividePrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||
|
||||
import { getDetailFooter, useFormSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsMovementOrderDetailApi.MovementOrderDetail {
|
||||
seq: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsMovementOrderForm' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<WmsMovementOrderApi.MovementOrder>({});
|
||||
const formType = ref<FormType>('create');
|
||||
const originalSubmitData = ref<WmsMovementOrderApi.MovementOrder>();
|
||||
const details = ref<DetailRow[]>([]);
|
||||
const detailTableRef = ref<VxeTableInstance>();
|
||||
const inventorySelectRef = ref<InstanceType<typeof WmsInventorySelect>>();
|
||||
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formType.value === 'update'
|
||||
? $t('ui.actionTitle.edit', ['移库单'])
|
||||
: $t('ui.actionTitle.create', ['移库单']);
|
||||
});
|
||||
|
||||
const isPrepareOrder = computed(() => {
|
||||
return (
|
||||
!formData.value?.id ||
|
||||
(formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status))
|
||||
);
|
||||
});
|
||||
const isSavedPrepareOrder = computed(() => {
|
||||
return (
|
||||
!!formData.value?.id &&
|
||||
formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status)
|
||||
);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema({
|
||||
onSourceWarehouseChange: handleSourceWarehouseChange,
|
||||
onTargetWarehouseChange: handleTargetWarehouseChange,
|
||||
}),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 标准化明细行,补齐本地序号和金额 */
|
||||
function normalizeDetail(
|
||||
detail: WmsMovementOrderDetailApi.MovementOrderDetail,
|
||||
): DetailRow {
|
||||
detailSeq += 1;
|
||||
return {
|
||||
...detail,
|
||||
seq: detailSeq,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据库存构建新的移库明细 */
|
||||
function buildDetail(inventory: InventorySelectRow): DetailRow {
|
||||
return normalizeDetail({
|
||||
availableQuantity: inventory.availableQuantity,
|
||||
id: undefined,
|
||||
itemCode: inventory.itemCode,
|
||||
itemId: inventory.itemId,
|
||||
itemName: inventory.itemName,
|
||||
price: undefined,
|
||||
quantity: undefined,
|
||||
skuCode: inventory.skuCode,
|
||||
skuId: inventory.skuId,
|
||||
skuName: inventory.skuName,
|
||||
sourceWarehouseId: inventory.warehouseId,
|
||||
sourceWarehouseName: inventory.warehouseName,
|
||||
targetWarehouseId: formData.value.targetWarehouseId,
|
||||
targetWarehouseName: formData.value.targetWarehouseName,
|
||||
totalPrice: undefined,
|
||||
unit: inventory.unit,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置移库明细 */
|
||||
function setDetails(list?: WmsMovementOrderDetailApi.MovementOrderDetail[]) {
|
||||
detailSeq = 0;
|
||||
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 刷新明细合计行 */
|
||||
async function refreshDetailFooter() {
|
||||
await nextTick();
|
||||
await detailTableRef.value?.updateFooter();
|
||||
}
|
||||
|
||||
/** 添加商品明细 */
|
||||
async function handleAddDetail() {
|
||||
const values =
|
||||
(await formApi.getValues()) as WmsMovementOrderApi.MovementOrder;
|
||||
if (!values.sourceWarehouseId || !values.targetWarehouseId) {
|
||||
message.warning('请先选择来源仓库和目标仓库');
|
||||
return;
|
||||
}
|
||||
formData.value.sourceWarehouseId = values.sourceWarehouseId;
|
||||
await nextTick();
|
||||
inventorySelectRef.value?.open(getSelectedInventoryKeys());
|
||||
}
|
||||
|
||||
/** 选择库存 */
|
||||
function handleSelectInventory(inventories: InventorySelectRow[]) {
|
||||
if (inventories.length === 0) {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
for (const inventory of inventories) {
|
||||
if (!inventory.skuId || isInventorySelected(inventory)) {
|
||||
continue;
|
||||
}
|
||||
details.value.push(buildDetail(inventory));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断库存是否已选择 */
|
||||
function isInventorySelected(inventory: InventorySelectRow) {
|
||||
return details.value.some((detail) => {
|
||||
return (
|
||||
detail.skuId === inventory.skuId &&
|
||||
detail.sourceWarehouseId === inventory.warehouseId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 获得已选择的库存标识 */
|
||||
function getSelectedInventoryKeys() {
|
||||
return details.value
|
||||
.map((detail) =>
|
||||
detail.skuId && detail.sourceWarehouseId
|
||||
? `${detail.skuId}-${detail.sourceWarehouseId}`
|
||||
: undefined,
|
||||
)
|
||||
.filter((key): key is string => !!key);
|
||||
}
|
||||
|
||||
/** 删除商品明细 */
|
||||
function handleDeleteDetail(row: DetailRow) {
|
||||
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
details.value.splice(index, 1);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 来源仓库变化时清空移库明细 */
|
||||
function handleSourceWarehouseChange(warehouse?: WmsWarehouseApi.Warehouse) {
|
||||
formData.value.sourceWarehouseId = warehouse?.id;
|
||||
formData.value.sourceWarehouseName = warehouse?.name;
|
||||
setDetails([]);
|
||||
}
|
||||
|
||||
/** 目标仓库变化时同步到移库明细 */
|
||||
function handleTargetWarehouseChange(warehouse?: WmsWarehouseApi.Warehouse) {
|
||||
formData.value.targetWarehouseId = warehouse?.id;
|
||||
formData.value.targetWarehouseName = warehouse?.name;
|
||||
for (const detail of details.value) {
|
||||
detail.targetWarehouseId = warehouse?.id;
|
||||
detail.targetWarehouseName = warehouse?.name;
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细数量变化 */
|
||||
function handleDetailQuantityChange(detail: DetailRow) {
|
||||
if (detail.price !== undefined && detail.price !== null) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
return;
|
||||
}
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细单价变化 */
|
||||
function handleDetailPriceChange(detail: DetailRow) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细金额变化 */
|
||||
function handleDetailTotalPriceChange(detail: DetailRow) {
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 校验商品明细 */
|
||||
async function validateDetails(required = false) {
|
||||
const values =
|
||||
(await formApi.getValues()) as WmsMovementOrderApi.MovementOrder;
|
||||
if (values.sourceWarehouseId === values.targetWarehouseId) {
|
||||
message.error('来源仓库和目标仓库不能相同');
|
||||
return false;
|
||||
}
|
||||
if (details.value.length === 0) {
|
||||
if (required) {
|
||||
message.error('至少包含一条移库明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (let index = 0; index < details.value.length; index += 1) {
|
||||
const detail = details.value[index]!;
|
||||
if (!detail.skuId) {
|
||||
message.error(`第 ${index + 1} 行明细请选择商品规格`);
|
||||
return false;
|
||||
}
|
||||
if (!detail.quantity || detail.quantity <= 0) {
|
||||
message.error(`第 ${index + 1} 行明细移库数量必须大于 0`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
detail.availableQuantity !== undefined &&
|
||||
detail.quantity > detail.availableQuantity
|
||||
) {
|
||||
message.error(`第 ${index + 1} 行明细移库数量不能大于可用库存`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 构建提交用的明细数据 */
|
||||
function buildSubmitDetails() {
|
||||
return details.value.map((row) => {
|
||||
const { seq: _seq, ...detail } = row;
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建提交用的单据数据 */
|
||||
async function buildSubmitData(): Promise<WmsMovementOrderApi.MovementOrder> {
|
||||
const values =
|
||||
(await formApi.getValues()) as WmsMovementOrderApi.MovementOrder;
|
||||
const {
|
||||
details: _details,
|
||||
totalPrice: _totalPrice,
|
||||
totalQuantity: _totalQuantity,
|
||||
...order
|
||||
} = formData.value;
|
||||
return {
|
||||
...order,
|
||||
...values,
|
||||
details: buildSubmitDetails(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 完成移库 */
|
||||
async function handleFormComplete() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !(await validateDetails(true)) || !formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认完成移库?完成后将更新库存。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = await buildSubmitData();
|
||||
if (!isEqual(data, originalSubmitData.value)) {
|
||||
await updateMovementOrder(data);
|
||||
}
|
||||
await completeMovementOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('移库成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 作废移库单 */
|
||||
async function handleFormCancel() {
|
||||
if (!formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认作废该移库单?作废后不可恢复。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
await cancelMovementOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('作废成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !(await validateDetails(false)) || !isPrepareOrder.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await buildSubmitData();
|
||||
try {
|
||||
await (formType.value === 'update'
|
||||
? updateMovementOrder(data)
|
||||
: createMovementOrder(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = {};
|
||||
originalSubmitData.value = undefined;
|
||||
setDetails([]);
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ formType: FormType; id?: number }>();
|
||||
formType.value = data.formType;
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
const order = await getMovementOrder(data.id);
|
||||
const orderDetails =
|
||||
order.details || (await getMovementOrderDetailListByOrderId(data.id));
|
||||
formData.value = { ...order, details: orderDetails };
|
||||
setDetails(orderDetails);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 初始化新增表单
|
||||
formData.value = {
|
||||
details: [],
|
||||
no: generateOrderNo('YK'),
|
||||
status: OrderStatusEnum.PREPARE,
|
||||
};
|
||||
setDetails([]);
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/4" :show-confirm-button="isPrepareOrder">
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">移库明细</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '添加商品',
|
||||
onClick: handleAddDetail,
|
||||
type: 'primary',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<VxeTable
|
||||
ref="detailTableRef"
|
||||
:data="details"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="availableQuantity"
|
||||
title="可用库存"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.availableQuantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="移库数量" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.quantity"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="QUANTITY_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="数量"
|
||||
@change="handleDetailQuantityChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="price" title="单价(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.price"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="单价"
|
||||
@change="handleDetailPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="金额(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.totalPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="金额"
|
||||
@change="handleDetailTotalPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: handleDeleteDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div v-if="isSavedPrepareOrder" class="flex flex-auto items-center gap-2">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '完成移库',
|
||||
type: 'primary',
|
||||
auth: ['wms:movement-order:complete'],
|
||||
onClick: handleFormComplete,
|
||||
},
|
||||
{
|
||||
label: '作废',
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
auth: ['wms:movement-order:cancel'],
|
||||
onClick: handleFormCancel,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<WmsInventorySelect
|
||||
ref="inventorySelectRef"
|
||||
:warehouse-id="formData.sourceWarehouseId"
|
||||
@change="handleSelectInventory"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsMovementOrderApi } from '#/api/wms/order/movement';
|
||||
import type { WmsMovementOrderDetailApi } from '#/api/wms/order/movement/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
getMovementOrder,
|
||||
getMovementOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/movement';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
interface PrintRow extends WmsMovementOrderDetailApi.MovementOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsMovementOrderPrint' });
|
||||
|
||||
const printData = ref<WmsMovementOrderApi.MovementOrder>({});
|
||||
const tableColumnCount = 5;
|
||||
|
||||
const printRows = computed<PrintRow[]>(() =>
|
||||
(printData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||
function waitForPaint() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 退出打印模式,恢复当前页面显示 */
|
||||
function removePrintMode() {
|
||||
document.body.classList.remove('wms-movement-order-printing');
|
||||
}
|
||||
|
||||
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||
function getPrintDictLabel(dictType: string, value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
return getDictLabel(dictType, value) || '-';
|
||||
}
|
||||
|
||||
/** 打印移库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||
async function print(id: number) {
|
||||
const order = await getMovementOrder(id);
|
||||
const details =
|
||||
order.details || (await getMovementOrderDetailListByOrderId(id));
|
||||
printData.value = { ...order, details };
|
||||
await nextTick();
|
||||
await waitForPaint();
|
||||
document.body.classList.add('wms-movement-order-printing');
|
||||
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||
window.print();
|
||||
}
|
||||
|
||||
defineExpose({ print });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="wmsMovementOrderPrint"
|
||||
class="wms-movement-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||
>
|
||||
<div class="relative mb-2">
|
||||
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||
移库单
|
||||
</h2>
|
||||
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||
<Barcode
|
||||
:content="printData.no"
|
||||
:display-value="false"
|
||||
:format="BarcodeFormatEnum.CODE39"
|
||||
:height="40"
|
||||
:width="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||
<div>移库单号:{{ printData.no || '-' }}</div>
|
||||
<div>来源仓库:{{ printData.sourceWarehouseName || '-' }}</div>
|
||||
<div>目标仓库:{{ printData.targetWarehouseName || '-' }}</div>
|
||||
<div>
|
||||
移库状态:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status)
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}
|
||||
</div>
|
||||
<div>总数量:{{ formatQuantity(printData.totalQuantity) || '-' }}</div>
|
||||
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||
{{ printData.creatorName || printData.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||
{{ printData.updaterName || printData.updater || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
商品信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
规格信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
数量
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
单价(元)
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
金额(元)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs">
|
||||
编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs">
|
||||
编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.quantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.totalPrice) || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length > 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||
colspan="2"
|
||||
>
|
||||
合计
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
></td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumPrice(printRows, (detail) => detail.totalPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length === 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||
:colspan="tableColumnCount"
|
||||
>
|
||||
暂无明细
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@page {
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body.wms-movement-order-printing) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
:global(body.wms-movement-order-printing *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
:global(body.wms-movement-order-printing .wms-movement-order-print),
|
||||
:global(body.wms-movement-order-printing .wms-movement-order-print *) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
:global(body.wms-movement-order-printing .wms-movement-order-print) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { buildNumberRangeSchema } from '#/components/number-range-input';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
import { WmsMerchantSelect } from '#/views/wms/md/merchant/components';
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
/** 表单类型 */
|
||||
export type FormType = 'create' | 'update';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入入库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '入库单号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||
placeholder: '请选择单据状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '单据状态',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
placeholder: '请选择供应商',
|
||||
supplier: true,
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '供应商',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
},
|
||||
buildNumberRangeSchema(
|
||||
'数量',
|
||||
'totalQuantityRange',
|
||||
'totalQuantityMin',
|
||||
'totalQuantityMax',
|
||||
QUANTITY_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'总金额',
|
||||
'totalPriceRange',
|
||||
'totalPriceMin',
|
||||
'totalPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择入库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '入库类型',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择创建用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'creator',
|
||||
label: '创建用户',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择更新用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'updater',
|
||||
label: '更新用户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'updateTime',
|
||||
label: '更新时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表表格列 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
fixed: 'left',
|
||||
slots: { content: 'expand_content' },
|
||||
type: 'expand',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
title: '单号/业务单号',
|
||||
width: 260,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||
},
|
||||
field: 'status',
|
||||
fixed: 'left',
|
||||
title: '入库状态',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_RECEIPT_ORDER_TYPE },
|
||||
},
|
||||
field: 'type',
|
||||
title: '入库类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
minWidth: 180,
|
||||
title: '仓库',
|
||||
},
|
||||
{
|
||||
field: 'quantityAmount',
|
||||
minWidth: 180,
|
||||
slots: { default: 'quantityAmount' },
|
||||
title: '总数量/总金额(元)',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
minWidth: 160,
|
||||
title: '供应商',
|
||||
},
|
||||
{
|
||||
field: 'operateInfo',
|
||||
minWidth: 260,
|
||||
slots: { default: 'operateInfo' },
|
||||
title: '操作信息',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 160,
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '入库单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '入库类型',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_RECEIPT_ORDER_TYPE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
label: '仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '单据状态',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'orderTime',
|
||||
label: '单据日期',
|
||||
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
label: '供应商',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalQuantity',
|
||||
label: '总数量',
|
||||
render: (val) => formatQuantity(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '总金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
render: (val, data) => val || data?.creator || '-',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'updaterName',
|
||||
label: '更新人',
|
||||
render: (val, data) => val || data?.updater || '-',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
render: (val) => val || '-',
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入入库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '入库单号',
|
||||
rules: z.string().min(1, '入库单号不能为空').max(64),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择入库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '入库类型',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
format: 'YYYY-MM-DD',
|
||||
placeholder: '请选择单据日期',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
placeholder: '请选择供应商',
|
||||
supplier: true,
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '供应商',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'col-span-2',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface ReceiptOrderDetailFooterRow {
|
||||
quantity?: number;
|
||||
totalPrice?: number;
|
||||
}
|
||||
type ReceiptOrderDetailFooterColumn = Pick<
|
||||
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||
'field'
|
||||
>;
|
||||
|
||||
/** 明细表格的合计行 */
|
||||
export function getDetailFooter({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: ReceiptOrderDetailFooterColumn[];
|
||||
data: ReceiptOrderDetailFooterRow[];
|
||||
}) {
|
||||
return [
|
||||
columns.map((column, index) => {
|
||||
if (index === 0) {
|
||||
return '合计';
|
||||
}
|
||||
if (column.field === 'quantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||
}
|
||||
if (column.field === 'totalPrice') {
|
||||
return formatSumPrice(data, (detail) => detail.totalPrice);
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderDeleteStatusList,
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import {
|
||||
ACTION_ICON,
|
||||
TableAction,
|
||||
useVbenVxeGrid,
|
||||
VxeColumn,
|
||||
VxeTable,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteReceiptOrder,
|
||||
exportReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
getReceiptOrderPage,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ReceiptOrderDetail from './modules/detail.vue';
|
||||
import ReceiptOrderForm from './modules/form.vue';
|
||||
import ReceiptOrderPrint from './modules/print.vue';
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrder' });
|
||||
|
||||
const printRef = ref<InstanceType<typeof ReceiptOrderPrint>>();
|
||||
const detailMap = reactive<
|
||||
Record<number, WmsReceiptOrderDetailApi.ReceiptOrderDetail[]>
|
||||
>({});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ReceiptOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: ReceiptOrderDetail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 清空展开明细缓存 */
|
||||
function clearDetailMap() {
|
||||
for (const id of Object.keys(detailMap)) {
|
||||
delete detailMap[Number(id)];
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
clearDetailMap();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建入库单 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ formType: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑入库单 */
|
||||
function handleEdit(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
formModalApi.setData({ id: row.id!, formType: 'update' }).open();
|
||||
}
|
||||
|
||||
/** 查看入库单详情 */
|
||||
function handleDetail(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
detailModalApi.setData({ id: row.id! }).open();
|
||||
}
|
||||
|
||||
/** 计算单据明细金额 */
|
||||
function getDetailTotalPrice(
|
||||
detail: WmsReceiptOrderDetailApi.ReceiptOrderDetail,
|
||||
) {
|
||||
return detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price);
|
||||
}
|
||||
|
||||
/** 获取已展开行的明细 */
|
||||
function getExpandedDetails(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
return detailMap[row.id!] || [];
|
||||
}
|
||||
|
||||
/** 展开列表行时懒加载入库明细 */
|
||||
async function handleExpandChange(
|
||||
row: WmsReceiptOrderApi.ReceiptOrder,
|
||||
expanded: boolean,
|
||||
) {
|
||||
if (!expanded) {
|
||||
return;
|
||||
}
|
||||
delete detailMap[row.id!];
|
||||
detailMap[row.id!] = await getReceiptOrderDetailListByOrderId(row.id!);
|
||||
}
|
||||
|
||||
/** 判断入库单是否可修改 */
|
||||
function canUpdateReceiptOrder(status?: number) {
|
||||
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 判断入库单是否可删除 */
|
||||
function canDeleteReceiptOrder(status?: number) {
|
||||
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 获取修改按钮禁用提示 */
|
||||
function getReceiptOrderUpdateTip(status?: number) {
|
||||
if (canUpdateReceiptOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已入库,无法修改';
|
||||
}
|
||||
if (status === OrderStatusEnum.CANCELED) {
|
||||
return '已作废,无法修改';
|
||||
}
|
||||
return '当前状态无法修改';
|
||||
}
|
||||
|
||||
/** 获取删除按钮禁用提示 */
|
||||
function getReceiptOrderDeleteTip(status?: number) {
|
||||
if (canDeleteReceiptOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已入库,无法删除';
|
||||
}
|
||||
return '当前状态无法删除';
|
||||
}
|
||||
|
||||
/** 删除入库单 */
|
||||
async function handleDelete(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.no]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteReceiptOrder(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出入库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportReceiptOrder(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '入库单.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
collapsed: true,
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
expandConfig: {
|
||||
padding: true,
|
||||
},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceiptOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsReceiptOrderApi.ReceiptOrder>,
|
||||
gridEvents: {
|
||||
toggleRowExpand: ({
|
||||
expanded,
|
||||
row,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
row: WmsReceiptOrderApi.ReceiptOrder;
|
||||
}) => {
|
||||
handleExpandChange(row, expanded);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【单据】入库"
|
||||
url="https://doc.iocoder.cn/wms/order/receipt/"
|
||||
/>
|
||||
</template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<ReceiptOrderPrint ref="printRef" />
|
||||
|
||||
<Grid table-title="入库单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<VxeTable
|
||||
:data="getExpandedDetails(row)"
|
||||
border
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="入库数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.quantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="金额(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(getDetailTotalPrice(detail)) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['入库单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:receipt-order:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:receipt-order:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<div>
|
||||
单号:
|
||||
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||
</div>
|
||||
<div v-if="row.bizOrderNo" class="text-xs text-gray-500">
|
||||
业务:{{ row.bizOrderNo }}
|
||||
</div>
|
||||
</template>
|
||||
<template #quantityAmount="{ row }">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>数量:</span>
|
||||
<span>{{ formatQuantity(row.totalQuantity) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #operateInfo="{ row }">
|
||||
<div>
|
||||
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||
{{ row.updaterName || row.updater || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: !canUpdateReceiptOrder(row.status),
|
||||
tooltip: getReceiptOrderUpdateTip(row.status),
|
||||
auth: ['wms:receipt-order:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: !canDeleteReceiptOrder(row.status),
|
||||
tooltip: getReceiptOrderDeleteTip(row.status),
|
||||
auth: ['wms:receipt-order:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
type: 'link',
|
||||
auth: ['wms:receipt-order:query'],
|
||||
onClick: () => printRef?.print(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { useDescription } from '#/components/description';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { getDetailFooter, useDetailSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderDetail' });
|
||||
|
||||
const detailData = ref<WmsReceiptOrderApi.ReceiptOrder>({});
|
||||
|
||||
const detailRows = computed<DetailRow[]>(() =>
|
||||
(detailData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: true,
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
useCard: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
detailData.value = {};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const order = await getReceiptOrder(data.id);
|
||||
const details =
|
||||
order.details || (await getReceiptOrderDetailListByOrderId(data.id));
|
||||
detailData.value = { ...order, details };
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="入库单详情"
|
||||
class="w-2/3"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<Descriptions :data="detailData" />
|
||||
<VxeTable
|
||||
:data="detailRows"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="数量" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="unit" title="单位" align="center" width="100" />
|
||||
<VxeColumn title="单价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="总价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.totalPrice) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormType } from '../data';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { isEqual } from '@vben/utils';
|
||||
|
||||
import { InputNumber, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelReceiptOrder,
|
||||
completeReceiptOrder,
|
||||
createReceiptOrder,
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
updateReceiptOrder,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsItemSkuSelect } from '#/views/wms/md/item/sku/components';
|
||||
import {
|
||||
dividePrice,
|
||||
multiplyPrice,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||
|
||||
import { getDetailFooter, useFormSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
seq: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderForm' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<WmsReceiptOrderApi.ReceiptOrder>({});
|
||||
const formType = ref<FormType>('create');
|
||||
const originalSubmitData = ref<WmsReceiptOrderApi.ReceiptOrder>();
|
||||
const details = ref<DetailRow[]>([]);
|
||||
const detailTableRef = ref<VxeTableInstance>();
|
||||
const skuSelectRef = ref<InstanceType<typeof WmsItemSkuSelect>>();
|
||||
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formType.value === 'update'
|
||||
? $t('ui.actionTitle.edit', ['入库单'])
|
||||
: $t('ui.actionTitle.create', ['入库单']);
|
||||
});
|
||||
|
||||
const isPrepareOrder = computed(() => {
|
||||
return (
|
||||
!formData.value?.id ||
|
||||
(formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status))
|
||||
);
|
||||
});
|
||||
const isSavedPrepareOrder = computed(() => {
|
||||
return (
|
||||
!!formData.value?.id &&
|
||||
formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status)
|
||||
);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 标准化明细行,补齐本地序号和金额 */
|
||||
function normalizeDetail(
|
||||
detail: WmsReceiptOrderDetailApi.ReceiptOrderDetail,
|
||||
): DetailRow {
|
||||
detailSeq += 1;
|
||||
return {
|
||||
...detail,
|
||||
seq: detailSeq,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据商品 SKU 构建新的入库明细 */
|
||||
function buildDetail(sku: WmsItemSkuApi.ItemSku): DetailRow {
|
||||
return normalizeDetail({
|
||||
id: undefined,
|
||||
itemCode: sku.itemCode,
|
||||
itemId: sku.itemId,
|
||||
itemName: sku.itemName,
|
||||
price: undefined,
|
||||
quantity: undefined,
|
||||
skuCode: sku.code,
|
||||
skuId: sku.id,
|
||||
skuName: sku.name,
|
||||
totalPrice: undefined,
|
||||
unit: sku.unit,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置入库明细 */
|
||||
function setDetails(list?: WmsReceiptOrderDetailApi.ReceiptOrderDetail[]) {
|
||||
detailSeq = 0;
|
||||
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 刷新明细合计行 */
|
||||
async function refreshDetailFooter() {
|
||||
await nextTick();
|
||||
await detailTableRef.value?.updateFooter();
|
||||
}
|
||||
|
||||
/** 获取已选择的 SKU 编号,避免重复选择 */
|
||||
function getSelectedSkuIds() {
|
||||
return details.value
|
||||
.map((detail) => detail.skuId)
|
||||
.filter((id): id is number => !!id);
|
||||
}
|
||||
|
||||
/** 添加商品明细 */
|
||||
async function handleAddDetail() {
|
||||
const values = (await formApi.getValues()) as WmsReceiptOrderApi.ReceiptOrder;
|
||||
if (!values.warehouseId) {
|
||||
message.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
skuSelectRef.value?.open(getSelectedSkuIds());
|
||||
}
|
||||
|
||||
/** 选择商品 SKU */
|
||||
function handleSelectSku(skus: WmsItemSkuApi.ItemSku[]) {
|
||||
if (skus.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectedSkuIds = new Set(getSelectedSkuIds());
|
||||
let changed = false;
|
||||
for (const sku of skus) {
|
||||
if (!sku.id || selectedSkuIds.has(sku.id)) {
|
||||
continue;
|
||||
}
|
||||
details.value.push(buildDetail(sku));
|
||||
selectedSkuIds.add(sku.id);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除商品明细 */
|
||||
function handleDeleteDetail(row: DetailRow) {
|
||||
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
details.value.splice(index, 1);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细数量变化 */
|
||||
function handleDetailQuantityChange(detail: DetailRow) {
|
||||
if (detail.price !== undefined && detail.price !== null) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
return;
|
||||
}
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细单价变化 */
|
||||
function handleDetailPriceChange(detail: DetailRow) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细金额变化 */
|
||||
function handleDetailTotalPriceChange(detail: DetailRow) {
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 校验商品明细 */
|
||||
function validateDetails(required = false) {
|
||||
if (details.value.length === 0) {
|
||||
if (required) {
|
||||
message.error('至少包含一条入库明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (let index = 0; index < details.value.length; index += 1) {
|
||||
const detail = details.value[index]!;
|
||||
if (!detail.skuId) {
|
||||
message.error(`第 ${index + 1} 行明细请选择商品规格`);
|
||||
return false;
|
||||
}
|
||||
if (!detail.quantity || detail.quantity <= 0) {
|
||||
message.error(`第 ${index + 1} 行明细入库数量必须大于 0`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 构建提交用的明细数据 */
|
||||
function buildSubmitDetails() {
|
||||
return details.value.map((row) => {
|
||||
const { seq: _seq, ...detail } = row;
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建提交用的单据数据 */
|
||||
async function buildSubmitData(): Promise<WmsReceiptOrderApi.ReceiptOrder> {
|
||||
const values = (await formApi.getValues()) as WmsReceiptOrderApi.ReceiptOrder;
|
||||
const {
|
||||
details: _details,
|
||||
totalPrice: _totalPrice,
|
||||
totalQuantity: _totalQuantity,
|
||||
...order
|
||||
} = formData.value;
|
||||
return {
|
||||
...order,
|
||||
...values,
|
||||
details: buildSubmitDetails(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 完成入库 */
|
||||
async function handleFormComplete() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认完成入库?完成后将更新库存。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = await buildSubmitData();
|
||||
if (!isEqual(data, originalSubmitData.value)) {
|
||||
await updateReceiptOrder(data);
|
||||
}
|
||||
await completeReceiptOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('入库成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 作废入库单 */
|
||||
async function handleFormCancel() {
|
||||
if (!formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认作废该入库单?作废后不可恢复。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
await cancelReceiptOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('作废成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await buildSubmitData();
|
||||
try {
|
||||
await (formType.value === 'update'
|
||||
? updateReceiptOrder(data)
|
||||
: createReceiptOrder(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = {};
|
||||
originalSubmitData.value = undefined;
|
||||
setDetails([]);
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ formType: FormType; id?: number }>();
|
||||
formType.value = data.formType;
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
const order = await getReceiptOrder(data.id);
|
||||
const orderDetails =
|
||||
order.details || (await getReceiptOrderDetailListByOrderId(data.id));
|
||||
formData.value = { ...order, details: orderDetails };
|
||||
setDetails(orderDetails);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 初始化新增表单
|
||||
formData.value = {
|
||||
details: [],
|
||||
no: generateOrderNo('RK'),
|
||||
status: OrderStatusEnum.PREPARE,
|
||||
};
|
||||
setDetails([]);
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/4" :show-confirm-button="isPrepareOrder">
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">入库明细</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '添加商品',
|
||||
onClick: handleAddDetail,
|
||||
type: 'primary',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<VxeTable
|
||||
ref="detailTableRef"
|
||||
:data="details"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="入库数量" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.quantity"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="QUANTITY_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="数量"
|
||||
@change="handleDetailQuantityChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="price" title="单价(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.price"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="单价"
|
||||
@change="handleDetailPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="金额(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.totalPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="金额"
|
||||
@change="handleDetailTotalPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: handleDeleteDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div v-if="isSavedPrepareOrder" class="flex flex-auto items-center gap-2">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '完成入库',
|
||||
type: 'primary',
|
||||
auth: ['wms:receipt-order:complete'],
|
||||
onClick: handleFormComplete,
|
||||
},
|
||||
{
|
||||
label: '作废',
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
auth: ['wms:receipt-order:cancel'],
|
||||
onClick: handleFormCancel,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<WmsItemSkuSelect ref="skuSelectRef" @change="handleSelectSku" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
interface PrintRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderPrint' });
|
||||
|
||||
const printData = ref<WmsReceiptOrderApi.ReceiptOrder>({});
|
||||
const tableColumnCount = 5;
|
||||
|
||||
const printRows = computed<PrintRow[]>(() =>
|
||||
(printData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||
function waitForPaint() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 退出打印模式,恢复当前页面显示 */
|
||||
function removePrintMode() {
|
||||
document.body.classList.remove('wms-receipt-order-printing');
|
||||
}
|
||||
|
||||
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||
function getPrintDictLabel(dictType: string, value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
return getDictLabel(dictType, value) || '-';
|
||||
}
|
||||
|
||||
/** 打印入库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||
async function print(id: number) {
|
||||
const order = await getReceiptOrder(id);
|
||||
const details =
|
||||
order.details || (await getReceiptOrderDetailListByOrderId(id));
|
||||
printData.value = { ...order, details };
|
||||
await nextTick();
|
||||
await waitForPaint();
|
||||
document.body.classList.add('wms-receipt-order-printing');
|
||||
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||
window.print();
|
||||
}
|
||||
|
||||
defineExpose({ print });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="wmsReceiptOrderPrint"
|
||||
class="wms-receipt-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||
>
|
||||
<div class="relative mb-2">
|
||||
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||
入库单
|
||||
</h2>
|
||||
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||
<Barcode
|
||||
:content="printData.no"
|
||||
:display-value="false"
|
||||
:format="BarcodeFormatEnum.CODE39"
|
||||
:height="40"
|
||||
:width="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||
<div>入库单号:{{ printData.no || '-' }}</div>
|
||||
<div>
|
||||
入库类型:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, printData.type)
|
||||
}}
|
||||
</div>
|
||||
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||
<div>
|
||||
入库状态:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status)
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}
|
||||
</div>
|
||||
<div>供应商:{{ printData.merchantName || '-' }}</div>
|
||||
<div>业务单号:{{ printData.bizOrderNo || '-' }}</div>
|
||||
<div>总数量:{{ formatQuantity(printData.totalQuantity) || '-' }}</div>
|
||||
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||
{{ printData.creatorName || printData.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||
{{ printData.updaterName || printData.updater || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
商品信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
规格信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
数量
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
单价(元)
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
金额(元)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs">
|
||||
编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs">
|
||||
编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.quantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.totalPrice) || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length > 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||
colspan="2"
|
||||
>
|
||||
合计
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
></td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumPrice(printRows, (detail) => detail.totalPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length === 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||
:colspan="tableColumnCount"
|
||||
>
|
||||
暂无明细
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@page {
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body.wms-receipt-order-printing) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print),
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print *) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { buildNumberRangeSchema } from '#/components/number-range-input';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
import { WmsMerchantSelect } from '#/views/wms/md/merchant/components';
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
/** 表单类型 */
|
||||
export type FormType = 'create' | 'update';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入出库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '出库单号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||
placeholder: '请选择单据状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '单据状态',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
customer: true,
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '客户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
},
|
||||
buildNumberRangeSchema(
|
||||
'数量',
|
||||
'totalQuantityRange',
|
||||
'totalQuantityMin',
|
||||
'totalQuantityMax',
|
||||
QUANTITY_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'总金额',
|
||||
'totalPriceRange',
|
||||
'totalPriceMin',
|
||||
'totalPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择出库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '出库类型',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择创建用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'creator',
|
||||
label: '创建用户',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择更新用户',
|
||||
showSearch: true,
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'updater',
|
||||
label: '更新用户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
fieldName: 'updateTime',
|
||||
label: '更新时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表表格列 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
fixed: 'left',
|
||||
slots: { content: 'expand_content' },
|
||||
type: 'expand',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
title: '单号/业务单号',
|
||||
width: 260,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||
},
|
||||
field: 'status',
|
||||
fixed: 'left',
|
||||
title: '出库状态',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE },
|
||||
},
|
||||
field: 'type',
|
||||
title: '出库类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
minWidth: 180,
|
||||
title: '仓库',
|
||||
},
|
||||
{
|
||||
field: 'quantityAmount',
|
||||
minWidth: 180,
|
||||
slots: { default: 'quantityAmount' },
|
||||
title: '总数量/总金额(元)',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
minWidth: 160,
|
||||
title: '客户',
|
||||
},
|
||||
{
|
||||
field: 'operateInfo',
|
||||
minWidth: 260,
|
||||
slots: { default: 'operateInfo' },
|
||||
title: '操作信息',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 160,
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '出库单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '出库类型',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
label: '仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '单据状态',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'orderTime',
|
||||
label: '单据日期',
|
||||
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
label: '客户',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalQuantity',
|
||||
label: '总数量',
|
||||
render: (val) => formatQuantity(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '总金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
render: (val, data) => val || data?.creator || '-',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'updaterName',
|
||||
label: '更新人',
|
||||
render: (val, data) => val || data?.updater || '-',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
render: (val) => val || '-',
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface ShipmentFormSchemaOptions {
|
||||
onWarehouseChange: (warehouse?: WmsWarehouseApi.Warehouse) => void;
|
||||
}
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema({
|
||||
onWarehouseChange,
|
||||
}: ShipmentFormSchemaOptions): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入出库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '出库单号',
|
||||
rules: z.string().min(1, '出库单号不能为空').max(64),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择出库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '出库类型',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
componentProps: {
|
||||
onChange: onWarehouseChange,
|
||||
},
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
format: 'YYYY-MM-DD',
|
||||
placeholder: '请选择单据日期',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
customer: true,
|
||||
placeholder: '请选择客户',
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '客户',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'col-span-2',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface ShipmentOrderDetailFooterRow {
|
||||
quantity?: number;
|
||||
totalPrice?: number;
|
||||
}
|
||||
type ShipmentOrderDetailFooterColumn = Pick<
|
||||
NonNullable<NonNullable<VxeTableGridOptions['columns']>[number]>,
|
||||
'field'
|
||||
>;
|
||||
|
||||
/** 明细表格的合计行 */
|
||||
export function getDetailFooter({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: ShipmentOrderDetailFooterColumn[];
|
||||
data: ShipmentOrderDetailFooterRow[];
|
||||
}) {
|
||||
return [
|
||||
columns.map((column, index) => {
|
||||
if (index === 0) {
|
||||
return '合计';
|
||||
}
|
||||
if (column.field === 'quantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||
}
|
||||
if (column.field === 'totalPrice') {
|
||||
return formatSumPrice(data, (detail) => detail.totalPrice);
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsShipmentOrderApi } from '#/api/wms/order/shipment';
|
||||
import type { WmsShipmentOrderDetailApi } from '#/api/wms/order/shipment/detail';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderDeleteStatusList,
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { message } from 'antdv-next';
|
||||
|
||||
import {
|
||||
ACTION_ICON,
|
||||
TableAction,
|
||||
useVbenVxeGrid,
|
||||
VxeColumn,
|
||||
VxeTable,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteShipmentOrder,
|
||||
exportShipmentOrder,
|
||||
getShipmentOrderDetailListByOrderId,
|
||||
getShipmentOrderPage,
|
||||
} from '#/api/wms/order/shipment';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ShipmentOrderDetail from './modules/detail.vue';
|
||||
import ShipmentOrderForm from './modules/form.vue';
|
||||
import ShipmentOrderPrint from './modules/print.vue';
|
||||
|
||||
defineOptions({ name: 'WmsShipmentOrder' });
|
||||
|
||||
const printRef = ref<InstanceType<typeof ShipmentOrderPrint>>();
|
||||
const detailMap = reactive<
|
||||
Record<number, WmsShipmentOrderDetailApi.ShipmentOrderDetail[]>
|
||||
>({});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ShipmentOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: ShipmentOrderDetail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 清空展开明细缓存 */
|
||||
function clearDetailMap() {
|
||||
for (const id of Object.keys(detailMap)) {
|
||||
delete detailMap[Number(id)];
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
clearDetailMap();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建出库单 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ formType: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑出库单 */
|
||||
function handleEdit(row: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
formModalApi.setData({ id: row.id!, formType: 'update' }).open();
|
||||
}
|
||||
|
||||
/** 查看出库单详情 */
|
||||
function handleDetail(row: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
detailModalApi.setData({ id: row.id! }).open();
|
||||
}
|
||||
|
||||
/** 计算单据明细金额 */
|
||||
function getDetailTotalPrice(
|
||||
detail: WmsShipmentOrderDetailApi.ShipmentOrderDetail,
|
||||
) {
|
||||
return detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price);
|
||||
}
|
||||
|
||||
/** 获取已展开行的明细 */
|
||||
function getExpandedDetails(row: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
return detailMap[row.id!] || [];
|
||||
}
|
||||
|
||||
/** 展开列表行时懒加载出库明细 */
|
||||
async function handleExpandChange(
|
||||
row: WmsShipmentOrderApi.ShipmentOrder,
|
||||
expanded: boolean,
|
||||
) {
|
||||
if (!expanded) {
|
||||
return;
|
||||
}
|
||||
delete detailMap[row.id!];
|
||||
detailMap[row.id!] = await getShipmentOrderDetailListByOrderId(row.id!);
|
||||
}
|
||||
|
||||
/** 判断出库单是否可修改 */
|
||||
function canUpdateShipmentOrder(status?: number) {
|
||||
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 判断出库单是否可删除 */
|
||||
function canDeleteShipmentOrder(status?: number) {
|
||||
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 获取修改按钮禁用提示 */
|
||||
function getShipmentOrderUpdateTip(status?: number) {
|
||||
if (canUpdateShipmentOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已出库,无法修改';
|
||||
}
|
||||
if (status === OrderStatusEnum.CANCELED) {
|
||||
return '已作废,无法修改';
|
||||
}
|
||||
return '当前状态无法修改';
|
||||
}
|
||||
|
||||
/** 获取删除按钮禁用提示 */
|
||||
function getShipmentOrderDeleteTip(status?: number) {
|
||||
if (canDeleteShipmentOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已出库,无法删除';
|
||||
}
|
||||
return '当前状态无法删除';
|
||||
}
|
||||
|
||||
/** 删除出库单 */
|
||||
async function handleDelete(row: WmsShipmentOrderApi.ShipmentOrder) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.no]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteShipmentOrder(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出出库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportShipmentOrder(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '出库单.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
collapsed: true,
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
expandConfig: {
|
||||
padding: true,
|
||||
},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getShipmentOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsShipmentOrderApi.ShipmentOrder>,
|
||||
gridEvents: {
|
||||
toggleRowExpand: ({
|
||||
expanded,
|
||||
row,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
row: WmsShipmentOrderApi.ShipmentOrder;
|
||||
}) => {
|
||||
handleExpandChange(row, expanded);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert
|
||||
title="【单据】出库"
|
||||
url="https://doc.iocoder.cn/wms/order/shipment/"
|
||||
/>
|
||||
</template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<ShipmentOrderPrint ref="printRef" />
|
||||
|
||||
<Grid table-title="出库单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<VxeTable
|
||||
:data="getExpandedDetails(row)"
|
||||
border
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="出库数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.quantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="金额(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(getDetailTotalPrice(detail)) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['出库单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:shipment-order:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:shipment-order:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<div>
|
||||
单号:
|
||||
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||
</div>
|
||||
<div v-if="row.bizOrderNo" class="text-xs text-gray-500">
|
||||
业务:{{ row.bizOrderNo }}
|
||||
</div>
|
||||
</template>
|
||||
<template #quantityAmount="{ row }">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>数量:</span>
|
||||
<span>{{ formatQuantity(row.totalQuantity) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #operateInfo="{ row }">
|
||||
<div>
|
||||
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||
{{ row.updaterName || row.updater || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: !canUpdateShipmentOrder(row.status),
|
||||
tooltip: getShipmentOrderUpdateTip(row.status),
|
||||
auth: ['wms:shipment-order:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: !canDeleteShipmentOrder(row.status),
|
||||
tooltip: getShipmentOrderDeleteTip(row.status),
|
||||
auth: ['wms:shipment-order:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
type: 'link',
|
||||
auth: ['wms:shipment-order:query'],
|
||||
onClick: () => printRef?.print(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsShipmentOrderApi } from '#/api/wms/order/shipment';
|
||||
import type { WmsShipmentOrderDetailApi } from '#/api/wms/order/shipment/detail';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getShipmentOrder,
|
||||
getShipmentOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/shipment';
|
||||
import { useDescription } from '#/components/description';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { getDetailFooter, useDetailSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsShipmentOrderDetailApi.ShipmentOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsShipmentOrderDetail' });
|
||||
|
||||
const detailData = ref<WmsShipmentOrderApi.ShipmentOrder>({});
|
||||
|
||||
const detailRows = computed<DetailRow[]>(() =>
|
||||
(detailData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
bordered: true,
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
useCard: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
detailData.value = {};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const order = await getShipmentOrder(data.id);
|
||||
const details =
|
||||
order.details || (await getShipmentOrderDetailListByOrderId(data.id));
|
||||
detailData.value = { ...order, details };
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="出库单详情"
|
||||
class="w-2/3"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<Descriptions :data="detailData" />
|
||||
<VxeTable
|
||||
:data="detailRows"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="数量" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="unit" title="单位" align="center" width="100" />
|
||||
<VxeColumn title="单价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="总价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.totalPrice) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,537 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormType } from '../data';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import type { WmsWarehouseApi } from '#/api/wms/md/warehouse';
|
||||
import type { WmsShipmentOrderApi } from '#/api/wms/order/shipment';
|
||||
import type { WmsShipmentOrderDetailApi } from '#/api/wms/order/shipment/detail';
|
||||
import type { InventorySelectRow } from '#/views/wms/inventory/components';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '@vben/constants';
|
||||
import { isEqual } from '@vben/utils';
|
||||
|
||||
import { InputNumber, message } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelShipmentOrder,
|
||||
completeShipmentOrder,
|
||||
createShipmentOrder,
|
||||
getShipmentOrder,
|
||||
getShipmentOrderDetailListByOrderId,
|
||||
updateShipmentOrder,
|
||||
} from '#/api/wms/order/shipment';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsInventorySelect } from '#/views/wms/inventory/components';
|
||||
import {
|
||||
dividePrice,
|
||||
formatQuantity,
|
||||
multiplyPrice,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||
|
||||
import { getDetailFooter, useFormSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsShipmentOrderDetailApi.ShipmentOrderDetail {
|
||||
seq: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsShipmentOrderForm' });
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<WmsShipmentOrderApi.ShipmentOrder>({});
|
||||
const formType = ref<FormType>('create');
|
||||
const originalSubmitData = ref<WmsShipmentOrderApi.ShipmentOrder>();
|
||||
const details = ref<DetailRow[]>([]);
|
||||
const detailTableRef = ref<VxeTableInstance>();
|
||||
const inventorySelectRef = ref<InstanceType<typeof WmsInventorySelect>>();
|
||||
const currentWarehouseId = ref<number>();
|
||||
const initializing = ref(false);
|
||||
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formType.value === 'update'
|
||||
? $t('ui.actionTitle.edit', ['出库单'])
|
||||
: $t('ui.actionTitle.create', ['出库单']);
|
||||
});
|
||||
|
||||
const isPrepareOrder = computed(() => {
|
||||
return (
|
||||
!formData.value?.id ||
|
||||
(formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status))
|
||||
);
|
||||
});
|
||||
const isSavedPrepareOrder = computed(() => {
|
||||
return (
|
||||
!!formData.value?.id &&
|
||||
formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status)
|
||||
);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema({ onWarehouseChange: handleWarehouseChange }),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 标准化明细行,补齐本地序号和金额 */
|
||||
function normalizeDetail(
|
||||
detail: WmsShipmentOrderDetailApi.ShipmentOrderDetail,
|
||||
): DetailRow {
|
||||
detailSeq += 1;
|
||||
return {
|
||||
...detail,
|
||||
seq: detailSeq,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据库存构建新的出库明细 */
|
||||
function buildDetail(inventory: InventorySelectRow): DetailRow {
|
||||
return normalizeDetail({
|
||||
availableQuantity: inventory.availableQuantity,
|
||||
id: undefined,
|
||||
itemCode: inventory.itemCode,
|
||||
itemId: inventory.itemId,
|
||||
itemName: inventory.itemName,
|
||||
price: undefined,
|
||||
quantity: undefined,
|
||||
skuCode: inventory.skuCode,
|
||||
skuId: inventory.skuId,
|
||||
skuName: inventory.skuName,
|
||||
totalPrice: undefined,
|
||||
unit: inventory.unit,
|
||||
warehouseId: inventory.warehouseId,
|
||||
warehouseName: inventory.warehouseName,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置出库明细 */
|
||||
function setDetails(list?: WmsShipmentOrderDetailApi.ShipmentOrderDetail[]) {
|
||||
detailSeq = 0;
|
||||
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 刷新明细合计行 */
|
||||
async function refreshDetailFooter() {
|
||||
await nextTick();
|
||||
await detailTableRef.value?.updateFooter();
|
||||
}
|
||||
|
||||
/** 添加商品明细 */
|
||||
async function handleAddDetail() {
|
||||
const values =
|
||||
(await formApi.getValues()) as WmsShipmentOrderApi.ShipmentOrder;
|
||||
if (!values.warehouseId) {
|
||||
message.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
currentWarehouseId.value = values.warehouseId;
|
||||
await nextTick();
|
||||
inventorySelectRef.value?.open(getSelectedInventoryKeys());
|
||||
}
|
||||
|
||||
/** 选择库存 */
|
||||
function handleSelectInventory(inventories: InventorySelectRow[]) {
|
||||
if (inventories.length === 0) {
|
||||
return;
|
||||
}
|
||||
let changed = false;
|
||||
for (const inventory of inventories) {
|
||||
if (!inventory.skuId || isInventorySelected(inventory)) {
|
||||
continue;
|
||||
}
|
||||
details.value.push(buildDetail(inventory));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断库存是否已选择 */
|
||||
function isInventorySelected(inventory: InventorySelectRow) {
|
||||
return details.value.some((detail) => {
|
||||
return (
|
||||
detail.skuId === inventory.skuId &&
|
||||
detail.warehouseId === inventory.warehouseId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 获得已选择的库存标识 */
|
||||
function getSelectedInventoryKeys() {
|
||||
return details.value
|
||||
.map((detail) =>
|
||||
detail.skuId && detail.warehouseId
|
||||
? `${detail.skuId}-${detail.warehouseId}`
|
||||
: undefined,
|
||||
)
|
||||
.filter((key): key is string => !!key);
|
||||
}
|
||||
|
||||
/** 删除商品明细 */
|
||||
function handleDeleteDetail(row: DetailRow) {
|
||||
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
details.value.splice(index, 1);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 仓库变化时清空出库明细 */
|
||||
function handleWarehouseChange(warehouse?: WmsWarehouseApi.Warehouse) {
|
||||
if (initializing.value) {
|
||||
return;
|
||||
}
|
||||
formData.value.warehouseId = warehouse?.id;
|
||||
formData.value.warehouseName = warehouse?.name;
|
||||
currentWarehouseId.value = warehouse?.id;
|
||||
setDetails([]);
|
||||
}
|
||||
|
||||
/** 明细数量变化 */
|
||||
function handleDetailQuantityChange(detail: DetailRow) {
|
||||
if (detail.price !== undefined && detail.price !== null) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
return;
|
||||
}
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细单价变化 */
|
||||
function handleDetailPriceChange(detail: DetailRow) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细金额变化 */
|
||||
function handleDetailTotalPriceChange(detail: DetailRow) {
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 校验商品明细 */
|
||||
function validateDetails(required = false) {
|
||||
if (details.value.length === 0) {
|
||||
if (required) {
|
||||
message.error('至少包含一条出库明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (let index = 0; index < details.value.length; index += 1) {
|
||||
const detail = details.value[index]!;
|
||||
if (!detail.skuId) {
|
||||
message.error(`第 ${index + 1} 行明细请选择商品规格`);
|
||||
return false;
|
||||
}
|
||||
if (!detail.quantity || detail.quantity <= 0) {
|
||||
message.error(`第 ${index + 1} 行明细出库数量必须大于 0`);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
detail.availableQuantity !== undefined &&
|
||||
detail.quantity > detail.availableQuantity
|
||||
) {
|
||||
message.error(`第 ${index + 1} 行明细出库数量不能大于可用库存`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 构建提交用的明细数据 */
|
||||
function buildSubmitDetails() {
|
||||
return details.value.map((row) => {
|
||||
const { seq: _seq, ...detail } = row;
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建提交用的单据数据 */
|
||||
async function buildSubmitData(): Promise<WmsShipmentOrderApi.ShipmentOrder> {
|
||||
const values =
|
||||
(await formApi.getValues()) as WmsShipmentOrderApi.ShipmentOrder;
|
||||
const {
|
||||
details: _details,
|
||||
totalPrice: _totalPrice,
|
||||
totalQuantity: _totalQuantity,
|
||||
...order
|
||||
} = formData.value;
|
||||
return {
|
||||
...order,
|
||||
...values,
|
||||
details: buildSubmitDetails(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 完成出库 */
|
||||
async function handleFormComplete() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认完成出库?完成后将更新库存。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = await buildSubmitData();
|
||||
if (!isEqual(data, originalSubmitData.value)) {
|
||||
await updateShipmentOrder(data);
|
||||
}
|
||||
await completeShipmentOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('出库成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 作废出库单 */
|
||||
async function handleFormCancel() {
|
||||
if (!formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认作废该出库单?作废后不可恢复。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
await cancelShipmentOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success('作废成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await buildSubmitData();
|
||||
try {
|
||||
await (formType.value === 'update'
|
||||
? updateShipmentOrder(data)
|
||||
: createShipmentOrder(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = {};
|
||||
originalSubmitData.value = undefined;
|
||||
currentWarehouseId.value = undefined;
|
||||
setDetails([]);
|
||||
return;
|
||||
}
|
||||
initializing.value = true;
|
||||
const data = modalApi.getData<{ formType: FormType; id?: number }>();
|
||||
formType.value = data.formType;
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
const order = await getShipmentOrder(data.id);
|
||||
const orderDetails =
|
||||
order.details || (await getShipmentOrderDetailListByOrderId(data.id));
|
||||
formData.value = { ...order, details: orderDetails };
|
||||
currentWarehouseId.value = order.warehouseId;
|
||||
setDetails(orderDetails);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
} finally {
|
||||
initializing.value = false;
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 初始化新增表单
|
||||
formData.value = {
|
||||
details: [],
|
||||
no: generateOrderNo('CK'),
|
||||
status: OrderStatusEnum.PREPARE,
|
||||
};
|
||||
currentWarehouseId.value = undefined;
|
||||
setDetails([]);
|
||||
await formApi.setValues(formData.value);
|
||||
await nextTick();
|
||||
originalSubmitData.value = await buildSubmitData();
|
||||
initializing.value = false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/4" :show-confirm-button="isPrepareOrder">
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">出库明细</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '添加商品',
|
||||
onClick: handleAddDetail,
|
||||
type: 'primary',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<VxeTable
|
||||
ref="detailTableRef"
|
||||
:data="details"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn
|
||||
field="availableQuantity"
|
||||
title="可用库存"
|
||||
align="right"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.availableQuantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="出库数量" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.quantity"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="QUANTITY_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="数量"
|
||||
@change="handleDetailQuantityChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="price" title="单价(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.price"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="单价"
|
||||
@change="handleDetailPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="金额(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<InputNumber
|
||||
v-model:value="row.totalPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="金额"
|
||||
@change="handleDetailTotalPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: handleDeleteDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div v-if="isSavedPrepareOrder" class="flex flex-auto items-center gap-2">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '完成出库',
|
||||
type: 'primary',
|
||||
auth: ['wms:shipment-order:complete'],
|
||||
onClick: handleFormComplete,
|
||||
},
|
||||
{
|
||||
label: '作废',
|
||||
type: 'primary',
|
||||
danger: true,
|
||||
auth: ['wms:shipment-order:cancel'],
|
||||
onClick: handleFormCancel,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<WmsInventorySelect
|
||||
ref="inventorySelectRef"
|
||||
:warehouse-id="currentWarehouseId"
|
||||
@change="handleSelectInventory"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsShipmentOrderApi } from '#/api/wms/order/shipment';
|
||||
import type { WmsShipmentOrderDetailApi } from '#/api/wms/order/shipment/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
getShipmentOrder,
|
||||
getShipmentOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/shipment';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
interface PrintRow extends WmsShipmentOrderDetailApi.ShipmentOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsShipmentOrderPrint' });
|
||||
|
||||
const printData = ref<WmsShipmentOrderApi.ShipmentOrder>({});
|
||||
const tableColumnCount = 5;
|
||||
|
||||
const printRows = computed<PrintRow[]>(() =>
|
||||
(printData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice:
|
||||
detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||
function waitForPaint() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 退出打印模式,恢复当前页面显示 */
|
||||
function removePrintMode() {
|
||||
document.body.classList.remove('wms-shipment-order-printing');
|
||||
}
|
||||
|
||||
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||
function getPrintDictLabel(dictType: string, value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
return getDictLabel(dictType, value) || '-';
|
||||
}
|
||||
|
||||
/** 打印出库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||
async function print(id: number) {
|
||||
const order = await getShipmentOrder(id);
|
||||
const details =
|
||||
order.details || (await getShipmentOrderDetailListByOrderId(id));
|
||||
printData.value = { ...order, details };
|
||||
await nextTick();
|
||||
await waitForPaint();
|
||||
document.body.classList.add('wms-shipment-order-printing');
|
||||
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||
window.print();
|
||||
}
|
||||
|
||||
defineExpose({ print });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="wmsShipmentOrderPrint"
|
||||
class="wms-shipment-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||
>
|
||||
<div class="relative mb-2">
|
||||
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||
出库单
|
||||
</h2>
|
||||
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||
<Barcode
|
||||
:content="printData.no"
|
||||
:display-value="false"
|
||||
:format="BarcodeFormatEnum.CODE39"
|
||||
:height="40"
|
||||
:width="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||
<div>出库单号:{{ printData.no || '-' }}</div>
|
||||
<div>
|
||||
出库类型:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_SHIPMENT_ORDER_TYPE, printData.type)
|
||||
}}
|
||||
</div>
|
||||
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||
<div>
|
||||
出库状态:{{
|
||||
getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status)
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}
|
||||
</div>
|
||||
<div>客户:{{ printData.merchantName || '-' }}</div>
|
||||
<div>业务单号:{{ printData.bizOrderNo || '-' }}</div>
|
||||
<div>总数量:{{ formatQuantity(printData.totalQuantity) || '-' }}</div>
|
||||
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||
{{ printData.creatorName || printData.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||
{{ printData.updaterName || printData.updater || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
商品信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
规格信息
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
数量
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
单价(元)
|
||||
</th>
|
||||
<th
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
|
||||
>
|
||||
金额(元)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs">
|
||||
编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs">
|
||||
编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.quantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.totalPrice) || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length > 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||
colspan="2"
|
||||
>
|
||||
合计
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||
</td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
></td>
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
|
||||
>
|
||||
{{ formatSumPrice(printRows, (detail) => detail.totalPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length === 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||
:colspan="tableColumnCount"
|
||||
>
|
||||
暂无明细
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@page {
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body.wms-shipment-order-printing) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
:global(body.wms-shipment-order-printing *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
:global(body.wms-shipment-order-printing .wms-shipment-order-print),
|
||||
:global(body.wms-shipment-order-printing .wms-shipment-order-print *) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
:global(body.wms-shipment-order-printing .wms-shipment-order-print) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* WMS 展示格式化和表单配置
|
||||
*/
|
||||
|
||||
type DecimalValue = null | number | string | undefined;
|
||||
|
||||
/** 数量小数位 */
|
||||
export const QUANTITY_PRECISION = 2;
|
||||
/** 金额小数位 */
|
||||
export const PRICE_PRECISION = 2;
|
||||
/** 重量小数位 */
|
||||
export const WEIGHT_PRECISION = 3;
|
||||
/** 长宽高小数位 */
|
||||
export const DIMENSION_PRECISION = 1;
|
||||
|
||||
function isNullOrUndefined(value: unknown) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
function toFiniteDecimal(value: DecimalValue) {
|
||||
if (isNullOrUndefined(value)) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
return undefined;
|
||||
}
|
||||
const decimalValue = typeof value === 'string' ? Number(value) : value;
|
||||
if (!Number.isFinite(decimalValue)) {
|
||||
return undefined;
|
||||
}
|
||||
return decimalValue;
|
||||
}
|
||||
|
||||
function sumDecimal<T>(list: T[], getter: (item: T) => DecimalValue) {
|
||||
let sum = 0;
|
||||
for (const item of list) {
|
||||
const decimalValue = toFiniteDecimal(getter(item));
|
||||
if (decimalValue !== undefined) {
|
||||
sum += decimalValue;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** 格式化数量 */
|
||||
export function formatQuantity(value?: null | number | string) {
|
||||
const decimalValue = toFiniteDecimal(value);
|
||||
return decimalValue === undefined
|
||||
? ''
|
||||
: decimalValue.toFixed(QUANTITY_PRECISION);
|
||||
}
|
||||
|
||||
/** 格式化金额 */
|
||||
export function formatPrice(value?: null | number | string) {
|
||||
const decimalValue = toFiniteDecimal(value);
|
||||
return decimalValue === undefined
|
||||
? ''
|
||||
: decimalValue.toFixed(PRICE_PRECISION);
|
||||
}
|
||||
|
||||
/** 金额四舍五入 */
|
||||
export function roundPrice(value: number) {
|
||||
return Number.isFinite(value)
|
||||
? Number(value.toFixed(PRICE_PRECISION))
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** 亏损数字样式 */
|
||||
export function getLossClass(value?: null | number | string) {
|
||||
const decimalValue = toFiniteDecimal(value);
|
||||
return decimalValue !== undefined && decimalValue < 0 ? 'text-red-500' : '';
|
||||
}
|
||||
|
||||
/** 数量 * 单价,计算金额 */
|
||||
export function multiplyPrice(quantity?: number, price?: number) {
|
||||
if (
|
||||
quantity === undefined ||
|
||||
quantity === null ||
|
||||
price === undefined ||
|
||||
price === null
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return roundPrice(Number(quantity) * Number(price));
|
||||
}
|
||||
|
||||
/** 金额 / 数量,反算单价 */
|
||||
export function dividePrice(totalPrice?: number, quantity?: number) {
|
||||
if (totalPrice === undefined || totalPrice === null || !quantity) {
|
||||
return undefined;
|
||||
}
|
||||
return roundPrice(Number(totalPrice) / Number(quantity));
|
||||
}
|
||||
|
||||
/** 汇总数量 */
|
||||
export function sumQuantity<T>(list: T[], getter: (item: T) => DecimalValue) {
|
||||
return sumDecimal(list, getter);
|
||||
}
|
||||
|
||||
/** 汇总金额 */
|
||||
export function sumPrice<T>(list: T[], getter: (item: T) => DecimalValue) {
|
||||
return sumDecimal(list, getter);
|
||||
}
|
||||
|
||||
/** 格式化汇总数量 */
|
||||
export function formatSumQuantity<T>(
|
||||
list: T[],
|
||||
getter: (item: T) => DecimalValue,
|
||||
) {
|
||||
return formatQuantity(sumQuantity(list, getter));
|
||||
}
|
||||
|
||||
/** 格式化汇总金额 */
|
||||
export function formatSumPrice<T>(
|
||||
list: T[],
|
||||
getter: (item: T) => DecimalValue,
|
||||
) {
|
||||
return formatPrice(sumPrice(list, getter));
|
||||
}
|
||||
|
||||
/** 格式化重量 */
|
||||
export function formatWeight(value?: null | number | string) {
|
||||
const decimalValue = toFiniteDecimal(value);
|
||||
return decimalValue === undefined
|
||||
? ''
|
||||
: decimalValue.toFixed(WEIGHT_PRECISION);
|
||||
}
|
||||
|
||||
/** 格式化长宽高 */
|
||||
export function formatDimension(value?: null | number | string) {
|
||||
const decimalValue = toFiniteDecimal(value);
|
||||
return decimalValue === undefined
|
||||
? ''
|
||||
: decimalValue.toFixed(DIMENSION_PRECISION);
|
||||
}
|
||||
|
||||
/** 格式化长宽高组合 */
|
||||
export function formatDimensionText(
|
||||
length?: null | number | string,
|
||||
width?: null | number | string,
|
||||
height?: null | number | string,
|
||||
) {
|
||||
if (
|
||||
!isNullOrUndefined(length) &&
|
||||
!isNullOrUndefined(width) &&
|
||||
!isNullOrUndefined(height)
|
||||
) {
|
||||
return [
|
||||
formatDimension(length),
|
||||
formatDimension(width),
|
||||
formatDimension(height),
|
||||
].join(' * ');
|
||||
}
|
||||
return [
|
||||
isNullOrUndefined(length) ? undefined : `长:${formatDimension(length)}`,
|
||||
isNullOrUndefined(width) ? undefined : `宽:${formatDimension(width)}`,
|
||||
isNullOrUndefined(height) ? undefined : `高:${formatDimension(height)}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* WMS 订单工具
|
||||
*/
|
||||
|
||||
/** 生成业务单号:前缀 + 月日 + 4 位随机数 */
|
||||
export function generateOrderNo(prefix: string) {
|
||||
const now = new Date();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const randomNo = String(Math.floor(Math.random() * 10_000)).padStart(4, '0');
|
||||
return `${prefix}${month}${day}${randomNo}`;
|
||||
}
|
||||
Loading…
Reference in New Issue