银川
1.物料管理页面批次属性功能联调
2.库存现有量添加自定义出库入库功能
已添加11个文件
已修改18个文件
2158 ■■■■■ 文件已修改
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/core/auth.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item-batch-config/index.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/materialstock/index.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productsales/line/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/salesnotice/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/salesnotice/line/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant-package/index.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant/index.ts 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/components/batch-config.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/components/data.ts 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/components/select-dialog.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/components/select.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/components/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/components/select-dialog.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/components/select.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/batch-config-display.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/components/stock-adjust-modal.vue 488 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/data.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/index.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/data.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -1,9 +1,9 @@
VITE_BASE=/
# è¯·æ±‚路径
VITE_BASE_URL=http://127.0.0.1:48080
VITE_BASE_URL=http://36.213.180.217:9500
# æŽ¥å£åœ°å€
VITE_GLOB_API_URL=http://127.0.0.1:48080/admin-api
VITE_GLOB_API_URL=http://36.213.180.217:9500/admin-api
# æ–‡ä»¶ä¸Šä¼ ç±»åž‹ï¼šserver - åŽç«¯ä¸Šä¼ ï¼Œ client - å‰ç«¯ç›´è¿žä¸Šä¼ ï¼Œä»…支持S3服务
VITE_UPLOAD_TYPE=server
src/api/core/auth.ts
@@ -100,16 +100,20 @@
/** èŽ·å–ç§Ÿæˆ·åˆ—è¡¨ */
export async function getTenantSimpleList() {
  return requestClient.get<AuthApi.TenantResult[]>(
    `/system/tenant/simple-list`,
  );
  // ç§Ÿæˆ·æŽ¥å£å·²ç¦ç”¨ï¼Œè¿”回空数组
  return Promise.resolve([]);
  // return requestClient.get<AuthApi.TenantResult[]>(
  //   `/system/tenant/simple-list`,
  // );
}
/** ä½¿ç”¨ç§Ÿæˆ·åŸŸåï¼ŒèŽ·å¾—ç§Ÿæˆ·ä¿¡æ¯ */
export async function getTenantByWebsite(website: string) {
  return requestClient.get<AuthApi.TenantResult>(
    `/system/tenant/get-by-website?website=${website}`,
  );
  // ç§Ÿæˆ·æŽ¥å£å·²ç¦ç”¨ï¼Œè¿”回空对象
  return Promise.resolve({} as AuthApi.TenantResult);
  // return requestClient.get<AuthApi.TenantResult>(
  //   `/system/tenant/get-by-website?website=${website}`,
  // );
}
/** èŽ·å–éªŒè¯ç  */
src/api/mes/md/item-batch-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace MdmItemBatchConfigApi {
  /** ç‰©æ–™æ‰¹æ¬¡é…ç½®ï¼ˆä»…保留7个字段) */
  export interface ItemBatchConfig {
    id?: number; // é…ç½®ID
    itemId: number; // ç‰©æ–™ID
    produceDateFlag?: boolean; // ç”Ÿäº§æ—¥æœŸå¿…å¡«
    expireDateFlag?: boolean; // æœ‰æ•ˆæœŸå¿…å¡«
    receiptDateFlag?: boolean; // å…¥åº“日期必填
    vendorFlag?: boolean; // ä¾›åº”商必填
    purchaseOrderCodeFlag?: boolean; // é‡‡è´­è®¢å•号必填
    lotNumberFlag?: boolean; // ç”Ÿäº§æ‰¹å·å¿…å¡«
    qualityStatusFlag?: boolean; // è´¨é‡çŠ¶æ€å¿…å¡«
  }
}
/** æ ¹æ®ç‰©æ–™ID获取批次配置 */
export function getItemBatchConfigByItemId(itemId: number) {
  return requestClient.get<MdmItemBatchConfigApi.ItemBatchConfig | null>(
    '/mes/md/item-batch-config/get-by-item-id',
    { params: { itemId } },
  );
}
/** ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆæ–°å¢ž/更新) */
export function saveItemBatchConfig(data: MdmItemBatchConfigApi.ItemBatchConfig) {
  return requestClient.post<number>('/mes/md/item-batch-config/save', data);
}
/** åˆ é™¤æ‰¹æ¬¡é…ç½® */
export function deleteItemBatchConfig(id: number) {
  return requestClient.delete(`/mes/md/item-batch-config/delete?id=${id}`);
}
src/api/mes/wm/materialstock/index.ts
@@ -24,6 +24,10 @@
    vendorId?: number; // ä¾›åº”商编号
    vendorName?: string; // ä¾›åº”商名称
    quantity?: number; // åœ¨åº“数量
    frozenQuantity?: number; // å†»ç»“数量
    reservedQuantity?: number; // å ç”¨é‡
    inTransitQuantity?: number; // åœ¨é€”量
    availableQuantity?: number; // å¯ç”¨é‡
    receiptTime?: string; // å…¥åº“日期
    frozen?: boolean; // æ˜¯å¦å†»ç»“
    createTime?: Date; // åˆ›å»ºæ—¶é—´
@@ -59,3 +63,59 @@
    params,
  });
}
/** åº“存调整(自定义出入库)- å…¥åº“ */
export interface MaterialStockAdjustInParams {
  itemId: number; // ç‰©æ–™ID
  warehouseId: number; // ä»“库ID
  locationId: number; // åº“区ID
  areaId: number; // åº“位ID
  batchId?: number; // æ‰¹æ¬¡ID(选择已有批次时传入)
  batchCode?: string; // æ‰¹æ¬¡å·ï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰
  produceDate?: string; // ç”Ÿäº§æ—¥æœŸï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰
  expireDate?: string; // æœ‰æ•ˆæœŸï¼ˆæ–°å»ºæ‰¹æ¬¡æ—¶ä¼ å…¥ï¼‰
  vendorId?: number; // ä¾›åº”商ID
  quantity: number; // å…¥åº“数量(必须大于0)
  reason?: string; // å…¥åº“原因
}
/** åº“存调整(自定义出入库)- å‡ºåº“ */
export interface MaterialStockAdjustOutParams {
  itemId: number; // ç‰©æ–™ID
  warehouseId: number; // ä»“库ID
  locationId: number; // åº“区ID
  areaId: number; // åº“位ID
  batchId?: number; // æ‰¹æ¬¡ID
  batchCode?: string; // æ‰¹æ¬¡å·
  vendorId?: number; // ä¾›åº”商ID
  quantity: number; // å‡ºåº“数量(必须大于0)
  reason?: string; // å‡ºåº“原因
}
/** è‡ªå®šä¹‰å…¥åº“ */
export function adjustMaterialStockIn(data: MaterialStockAdjustInParams) {
  return requestClient.post<number>('/mes/wm/material-stock/adjust-in', data);
}
/** è‡ªå®šä¹‰å‡ºåº“ */
export function adjustMaterialStockOut(data: MaterialStockAdjustOutParams) {
  return requestClient.post<number>('/mes/wm/material-stock/adjust-out', data);
}
/** åº“存调整(正数=入库,负数=出库)- æ—§æŽ¥å£ï¼Œä¿ç•™å…¼å®¹ */
export interface MaterialStockAdjustParams {
  itemId: number; // ç‰©æ–™ID
  warehouseId: number; // ä»“库ID
  locationId: number; // åº“区ID
  areaId: number; // åº“位ID
  batchId?: number; // æ‰¹æ¬¡ID
  batchCode?: string; // æ‰¹æ¬¡å·
  vendorId?: number; // ä¾›åº”商ID
  quantity: number; // è°ƒæ•´æ•°é‡ï¼ˆæ­£æ•°=入库,负数=出库)
  reason?: string; // è°ƒæ•´åŽŸå› 
}
/** åº“存调整(正数=入库,负数=出库) - æ—§æŽ¥å£ï¼Œä¿ç•™å…¼å®¹ */
export function adjustMaterialStock(data: MaterialStockAdjustParams) {
  return requestClient.post<number>('/mes/wm/material-stock/adjust', data);
}
src/api/mes/wm/productsales/line/index.ts
@@ -8,6 +8,7 @@
    id?: number; // è¡Œç¼–号
    salesId?: number; // å‡ºåº“单编号
    noticeLineId?: number; // å‘货通知单行编号
    saleOrderItemId?: number; // é”€å”®è®¢å•明细ID(用于追溯)
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
src/api/mes/wm/salesnotice/index.ts
@@ -8,6 +8,7 @@
    id?: number; // é€šçŸ¥å•编号
    code?: string; // é€šçŸ¥å•编号
    name?: string; // é€šçŸ¥å•名称
    saleOrderId?: number; // é”€å”®è®¢å•编号
    salesOrderCode?: string; // é”€å”®è®¢å•编号
    clientId?: number; // å®¢æˆ·ç¼–号
    clientCode?: string; // å®¢æˆ·ç¼–码
@@ -17,6 +18,7 @@
    recipientTelephone?: string; // è”系方式
    recipientAddress?: string; // æ”¶è´§åœ°å€
    status?: number; // å•据状态
    outStatus?: number; // å‡ºåº“状态:0-未出库,1-部分出库,2-全部出库
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
@@ -63,3 +65,17 @@
    params,
  });
}
/** ä»Žé”€å”®è®¢å•生成发货通知单 */
export function generateFromSaleOrder(saleOrderId: number) {
  return requestClient.post<number>('/mes/wm/sales-notice/generate-from-sale-order', {
    saleOrderId,
  });
}
/** ä»Žå‘货通知单生成销售出库单 */
export function generateProductSales(id: number) {
  return requestClient.post<number>('/mes/wm/sales-notice/generate-product-sales', null, {
    params: { id },
  });
}
src/api/mes/wm/salesnotice/line/index.ts
@@ -7,6 +7,7 @@
  export interface SalesNoticeLine {
    id?: number; // è¡Œç¼–号
    noticeId?: number; // å‘货通知单编号
    saleOrderItemId?: number; // é”€å”®è®¢å•明细ID(用于追溯)
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
@@ -15,6 +16,7 @@
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    quantity?: number; // å‘货数量
    outCount?: number; // å·²å‡ºåº“数量
    oqcCheckFlag?: boolean; // æ˜¯å¦æ£€éªŒ
    remark?: string; // å¤‡æ³¨
  }
src/api/system/tenant-package/index.ts
@@ -17,48 +17,78 @@
  }
}
/** ç§Ÿæˆ·å¥—餐列表 */
// ç§Ÿæˆ·å¥—餐接口已禁用
// /** ç§Ÿæˆ·å¥—餐列表 */
// export function getTenantPackagePage(params: PageParam) {
//   return requestClient.get<PageResult<SystemTenantPackageApi.TenantPackage>>(
//     '/system/tenant-package/page',
//     { params },
//   );
// }
// /** æŸ¥è¯¢ç§Ÿæˆ·å¥—餐详情 */
// export function getTenantPackage(id: number) {
//   return requestClient.get(`/system/tenant-package/get?id=${id}`);
// }
// /** æ–°å¢žç§Ÿæˆ·å¥—餐 */
// export function createTenantPackage(
//   data: SystemTenantPackageApi.TenantPackage,
// ) {
//   return requestClient.post('/system/tenant-package/create', data);
// }
// /** ä¿®æ”¹ç§Ÿæˆ·å¥—餐 */
// export function updateTenantPackage(
//   data: SystemTenantPackageApi.TenantPackage,
// ) {
//   return requestClient.put('/system/tenant-package/update', data);
// }
// /** åˆ é™¤ç§Ÿæˆ·å¥—餐 */
// export function deleteTenantPackage(id: number) {
//   return requestClient.delete(`/system/tenant-package/delete?id=${id}`);
// }
// /** æ‰¹é‡åˆ é™¤ç§Ÿæˆ·å¥—餐 */
// export function deleteTenantPackageList(ids: number[]) {
//   return requestClient.delete(
//     `/system/tenant-package/delete-list?ids=${ids.join(',')}`,
//   );
// }
// /** èŽ·å–ç§Ÿæˆ·å¥—é¤ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
// export function getTenantPackageList() {
//   return requestClient.get<SystemTenantPackageApi.TenantPackage[]>(
//     '/system/tenant-package/get-simple-list',
//   );
// }
// æä¾›ç©ºå‡½æ•°ä»¥é¿å…å¼•用错误
export function getTenantPackagePage(params: PageParam) {
  return requestClient.get<PageResult<SystemTenantPackageApi.TenantPackage>>(
    '/system/tenant-package/page',
    { params },
  );
  return Promise.resolve({ list: [], total: 0 });
}
/** æŸ¥è¯¢ç§Ÿæˆ·å¥—餐详情 */
export function getTenantPackage(id: number) {
  return requestClient.get(`/system/tenant-package/get?id=${id}`);
  return Promise.resolve({} as SystemTenantPackageApi.TenantPackage);
}
/** æ–°å¢žç§Ÿæˆ·å¥—餐 */
export function createTenantPackage(
  data: SystemTenantPackageApi.TenantPackage,
) {
  return requestClient.post('/system/tenant-package/create', data);
export function createTenantPackage(data: SystemTenantPackageApi.TenantPackage) {
  return Promise.resolve({});
}
/** ä¿®æ”¹ç§Ÿæˆ·å¥—餐 */
export function updateTenantPackage(
  data: SystemTenantPackageApi.TenantPackage,
) {
  return requestClient.put('/system/tenant-package/update', data);
export function updateTenantPackage(data: SystemTenantPackageApi.TenantPackage) {
  return Promise.resolve({});
}
/** åˆ é™¤ç§Ÿæˆ·å¥—餐 */
export function deleteTenantPackage(id: number) {
  return requestClient.delete(`/system/tenant-package/delete?id=${id}`);
  return Promise.resolve({});
}
/** æ‰¹é‡åˆ é™¤ç§Ÿæˆ·å¥—餐 */
export function deleteTenantPackageList(ids: number[]) {
  return requestClient.delete(
    `/system/tenant-package/delete-list?ids=${ids.join(',')}`,
  );
  return Promise.resolve({});
}
/** èŽ·å–ç§Ÿæˆ·å¥—é¤ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
export function getTenantPackageList() {
  return requestClient.get<SystemTenantPackageApi.TenantPackage[]>(
    '/system/tenant-package/get-simple-list',
  );
  return Promise.resolve([]);
}
src/api/system/tenant/index.ts
@@ -17,60 +17,98 @@
  }
}
/** ç§Ÿæˆ·åˆ—表 */
// ç§Ÿæˆ·æŽ¥å£å·²ç¦ç”¨
// /** ç§Ÿæˆ·åˆ—表 */
// export function getTenantPage(params: PageParam) {
//   return requestClient.get<PageResult<SystemTenantApi.Tenant>>(
//     '/system/tenant/page',
//     { params },
//   );
// }
// /** èŽ·å–ç§Ÿæˆ·ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
// export function getSimpleTenantList() {
//   return requestClient.get<SystemTenantApi.Tenant[]>(
//     '/system/tenant/simple-list',
//   );
// }
// /** æŸ¥è¯¢ç§Ÿæˆ·è¯¦æƒ… */
// export function getTenant(id: number) {
//   return requestClient.get<SystemTenantApi.Tenant>(
//     `/system/tenant/get?id=${id}`,
//   );
// }
// /** èŽ·å–ç§Ÿæˆ·ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
// export function getTenantList() {
//   return requestClient.get<SystemTenantApi.Tenant[]>(
//     '/system/tenant/simple-list',
//   );
// }
// /** æ–°å¢žç§Ÿæˆ· */
// export function createTenant(data: SystemTenantApi.Tenant) {
//   return requestClient.post('/system/tenant/create', data);
// }
// /** ä¿®æ”¹ç§Ÿæˆ· */
// export function updateTenant(data: SystemTenantApi.Tenant) {
//   return requestClient.put('/system/tenant/update', data);
// }
// /** åˆ é™¤ç§Ÿæˆ· */
// export function deleteTenant(id: number) {
//   return requestClient.delete(`/system/tenant/delete?id=${id}`);
// }
// /** æ‰¹é‡åˆ é™¤ç§Ÿæˆ· */
// export function deleteTenantList(ids: number[]) {
//   return requestClient.delete(
//     `/system/tenant/delete-list?ids=${ids.join(',')}`,
//   );
// }
// /** å¯¼å‡ºç§Ÿæˆ· */
// export function exportTenant(params: any) {
//   return requestClient.download('/system/tenant/export-excel', {
//     params,
//   });
// }
// æä¾›ç©ºå‡½æ•°ä»¥é¿å…å¼•用错误
export function getTenantPage(params: PageParam) {
  return requestClient.get<PageResult<SystemTenantApi.Tenant>>(
    '/system/tenant/page',
    { params },
  );
  return Promise.resolve({ list: [], total: 0 });
}
/** èŽ·å–ç§Ÿæˆ·ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
export function getSimpleTenantList() {
  return requestClient.get<SystemTenantApi.Tenant[]>(
    '/system/tenant/simple-list',
  );
  return Promise.resolve([]);
}
/** æŸ¥è¯¢ç§Ÿæˆ·è¯¦æƒ… */
export function getTenant(id: number) {
  return requestClient.get<SystemTenantApi.Tenant>(
    `/system/tenant/get?id=${id}`,
  );
  return Promise.resolve({} as SystemTenantApi.Tenant);
}
/** èŽ·å–ç§Ÿæˆ·ç²¾ç®€ä¿¡æ¯åˆ—è¡¨ */
export function getTenantList() {
  return requestClient.get<SystemTenantApi.Tenant[]>(
    '/system/tenant/simple-list',
  );
  return Promise.resolve([]);
}
/** æ–°å¢žç§Ÿæˆ· */
export function createTenant(data: SystemTenantApi.Tenant) {
  return requestClient.post('/system/tenant/create', data);
  return Promise.resolve({});
}
/** ä¿®æ”¹ç§Ÿæˆ· */
export function updateTenant(data: SystemTenantApi.Tenant) {
  return requestClient.put('/system/tenant/update', data);
  return Promise.resolve({});
}
/** åˆ é™¤ç§Ÿæˆ· */
export function deleteTenant(id: number) {
  return requestClient.delete(`/system/tenant/delete?id=${id}`);
  return Promise.resolve({});
}
/** æ‰¹é‡åˆ é™¤ç§Ÿæˆ· */
export function deleteTenantList(ids: number[]) {
  return requestClient.delete(
    `/system/tenant/delete-list?ids=${ids.join(',')}`,
  );
  return Promise.resolve({});
}
/** å¯¼å‡ºç§Ÿæˆ· */
export function exportTenant(params: any) {
  return requestClient.download('/system/tenant/export-excel', {
    params,
  });
  return Promise.resolve(new Blob());
}
src/packages/constants/src/biz-mes-enum.ts
@@ -371,6 +371,13 @@
  APPROVED: MesOrderStatusConstants.APPROVED,
} as const;
/** MES å‘货通知单出库状态枚举 */
export const MesWmSalesNoticeOutStatusEnum = {
  NONE: 0,
  PARTIAL: 1,
  ALL: 2,
} as const;
/** MES é”€å”®é€€è´§å•状态枚举 */
export const MesWmReturnSalesStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
src/views/basicData/mdm/components/batch-config.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
<script lang="ts" setup>
import type { MdmItemBatchConfigApi } from '#/api/mes/md/item-batch-config';
import { computed, ref, watch } from 'vue';
import { Checkbox, Divider } from 'ant-design-vue';
import {
  getItemBatchConfigByItemId,
  saveItemBatchConfig,
} from '#/api/mes/md/item-batch-config';
const props = defineProps<{
  itemId?: number;
  isBatchManaged?: boolean;
}>();
const emit = defineEmits<{
  change: [config: Partial<MdmItemBatchConfigApi.ItemBatchConfig>];
}>();
// æ‰¹æ¬¡é…ç½®æ•°æ®
const config = ref<Partial<MdmItemBatchConfigApi.ItemBatchConfig>>({
  produceDateFlag: false,
  expireDateFlag: false,
  receiptDateFlag: false,
  vendorFlag: false,
  purchaseOrderCodeFlag: false,
  lotNumberFlag: false,
  qualityStatusFlag: false,
});
// æ˜¯å¦åŠ è½½ä¸­
const loading = ref(false);
// æ˜¯å¦æ˜¾ç¤ºé…ç½®ï¼ˆå¯ç”¨æ‰¹æ¬¡ç®¡ç†æ—¶æ˜¾ç¤ºï¼‰
const visible = computed(() => props.isBatchManaged === true);
// æ‰¹æ¬¡é…ç½®é€‰é¡¹ï¼ˆä»…保留7个字段)
const configOptions = [
  { key: 'produceDateFlag', label: '生产日期' },
  { key: 'expireDateFlag', label: '有效期' },
  { key: 'receiptDateFlag', label: '入库日期' },
  { key: 'vendorFlag', label: '供应商' },
  { key: 'purchaseOrderCodeFlag', label: '采购订单号' },
  { key: 'lotNumberFlag', label: '生产批号' },
  { key: 'qualityStatusFlag', label: '质量状态' },
] as const;
/** åŠ è½½æ‰¹æ¬¡é…ç½® */
async function loadConfig() {
  if (!props.itemId || !props.isBatchManaged) {
    return;
  }
  loading.value = true;
  try {
    const data = await getItemBatchConfigByItemId(props.itemId);
    if (data) {
      config.value = { ...config.value, ...data };
    }
  } catch {
    // æ— é…ç½®æ—¶ä¸æŠ¥é”™
  } finally {
    loading.value = false;
  }
}
/** ä¿å­˜æ‰¹æ¬¡é…ç½® */
async function saveConfig(): Promise<number | undefined> {
  if (!props.itemId || !props.isBatchManaged) {
    return undefined;
  }
  const data: MdmItemBatchConfigApi.ItemBatchConfig = {
    ...config.value as MdmItemBatchConfigApi.ItemBatchConfig,
    itemId: props.itemId,
  };
  const id = await saveItemBatchConfig(data);
  config.value.id = id;
  return id;
}
/** èŽ·å–å½“å‰é…ç½® */
function getConfig(): Partial<MdmItemBatchConfigApi.ItemBatchConfig> {
  return { ...config.value };
}
/** é‡ç½®é…ç½® */
function resetConfig() {
  config.value = {
    produceDateFlag: false,
    expireDateFlag: false,
    receiptDateFlag: false,
    vendorFlag: false,
    purchaseOrderCodeFlag: false,
    lotNumberFlag: false,
    qualityStatusFlag: false,
  };
}
// ç›‘听 itemId å˜åŒ–,加载配置
watch(
  () => [props.itemId, props.isBatchManaged],
  () => {
    if (props.itemId && props.isBatchManaged) {
      loadConfig();
    } else {
      resetConfig();
    }
  },
  { immediate: true },
);
// ç›‘听配置变化,触发 change äº‹ä»¶
watch(
  config,
  () => {
    emit('change', getConfig());
  },
  { deep: true },
);
defineExpose({
  saveConfig,
  getConfig,
  loadConfig,
  resetConfig,
});
</script>
<template>
  <div v-if="visible" class="mt-4">
    <Divider orientation="left">批次属性配置</Divider>
    <div class="grid grid-cols-4 gap-4 px-4">
      <Checkbox
        v-for="option in configOptions"
        :key="option.key"
        v-model:checked="config[option.key]"
        :disabled="loading"
      >
        {{ option.label }}
      </Checkbox>
    </div>
    <p class="mt-2 px-4 text-xs text-gray-500">
      å‹¾é€‰è¡¨ç¤ºåˆ›å»ºæ‰¹æ¬¡æ—¶è¯¥å­—段必填
    </p>
  </div>
</template>
src/views/basicData/mdm/components/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MdmItemApi } from '#/api/mdm/item';
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
/** ç‰©æ–™é€‰æ‹©å¼¹çª—搜索表单 */
export function useMdmItemSelectGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '物料编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入物料编码',
      },
    },
    {
      fieldName: 'name',
      label: '物料名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入物料名称',
      },
    },
    {
      fieldName: 'itemType',
      label: '物料类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        placeholder: '请选择物料类型',
        options: [
          { label: '原料', value: 1 },
          { label: '半成品', value: 2 },
          { label: '成品', value: 3 },
          { label: '辅料', value: 4 },
        ],
      },
    },
  ];
}
/** ç‰©æ–™ç±»åž‹é¢œè‰²æ˜ å°„ */
const itemTypeMap: Record<number, { label: string; color: string }> = {
  1: { label: '原料', color: 'green' },
  2: { label: '半成品', color: 'orange' },
  3: { label: '成品', color: 'blue' },
  4: { label: '辅料', color: 'purple' },
};
/** ç‰©æ–™é€‰æ‹©å¼¹çª—列表字段 */
export function useMdmItemSelectGridColumns(
  multiple = true,
): VxeTableGridOptions<MdmItemApi.Item>['columns'] {
  return [
    { type: multiple ? 'checkbox' : 'radio', width: 50 },
    {
      field: 'code',
      title: '物料编码',
      width: 180,
    },
    {
      field: 'name',
      title: '物料名称',
      minWidth: 160,
      align: 'left',
    },
    {
      field: 'specification',
      title: '规格型号',
      minWidth: 140,
      align: 'left',
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      width: 90,
      align: 'center',
    },
    {
      field: 'itemType',
      title: '物料类型',
      width: 110,
      align: 'center',
      formatter: ({ cellValue }: { cellValue: number }) => {
        const type = itemTypeMap[cellValue];
        return type ? type.label : '-';
      },
    },
    {
      field: 'isBatchManaged',
      title: '批次管理',
      width: 110,
      align: 'center',
      cellRender: {
        name: 'CellDict',
        props: { type: 'infra_boolean_string' },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
  ];
}
src/views/basicData/mdm/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as MdmItemSelect } from './select.vue';
export { default as MdmItemSelectDialog } from './select-dialog.vue';
src/views/basicData/mdm/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MdmItemApi } from '#/api/mdm/item';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemPage } from '#/api/mdm/item';
import {
  useMdmItemSelectGridColumns,
  useMdmItemSelectGridFormSchema,
} from './data';
defineOptions({ name: 'MdmItemSelectDialog' });
const emit = defineEmits<{
  selected: [rows: MdmItemApi.Item[]];
}>();
const open = ref(false); // å¼¹çª—是否打开
const multiple = ref(true); // æ˜¯å¦å¤šé€‰
const selectedRows = ref<MdmItemApi.Item[]>([]); // å·²é€‰ç‰©æ–™åˆ—表
const preSelectedIds = ref<number[]>([]); // é¢„选物料编号列表
/** èŽ·å–å½“å‰è¡¨æ ¼æ•°æ® */
function getTableRows() {
  return gridApi.grid.getTableData().fullData as MdmItemApi.Item[];
}
/** èŽ·å–å¤šé€‰è®°å½•ï¼ŒåŒ…å« VXE reserve è·¨é¡µè®°å½• */
function getMultipleSelectedRows() {
  const selectedMap = new Map<number, MdmItemApi.Item>();
  const records = [
    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
  ] as MdmItemApi.Item[];
  records.forEach((row) => {
    const rowId = row.id;
    if (rowId) {
      selectedMap.set(rowId, row);
    }
  });
  return [...selectedMap.values()];
}
/** å¤„理勾选变化 */
function handleCheckboxSelectChange() {
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理单选变化 */
function handleRadioChange(row: MdmItemApi.Item) {
  selectedRows.value = [row];
}
/** å¤šé€‰æ¨¡å¼ä¸‹åˆ‡æ¢è¡Œå‹¾é€‰ */
async function toggleMultipleRow(row: MdmItemApi.Item) {
  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
  await gridApi.grid.setCheckboxRow(row, !selected);
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理行双击 */
async function handleCellDblclick({ row }: { row: MdmItemApi.Item }) {
  if (multiple.value) {
    await toggleMultipleRow(row);
    return;
  }
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选物料 */
async function applyPreSelection() {
  if (preSelectedIds.value.length === 0) {
    return;
  }
  const rows = getTableRows();
  for (const row of rows) {
    if (
      row.id === undefined ||
      row.id === null ||
      !preSelectedIds.value.includes(row.id)
    ) {
      continue;
    }
    if (multiple.value) {
      await gridApi.grid.setCheckboxRow(row, true);
    } else {
      await gridApi.grid.setRadioRow(row);
      selectedRows.value = [row];
      return;
    }
  }
  if (multiple.value) {
    selectedRows.value = getMultipleSelectedRows();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useMdmItemSelectGridFormSchema(),
  },
  gridOptions: {
    columns: useMdmItemSelectGridColumns(true),
    height: 560,
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      range: true,
      reserve: true,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getItemPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MdmItemApi.Item>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    checkboxAll: handleCheckboxSelectChange,
    checkboxChange: handleCheckboxSelectChange,
    radioChange: ({ row }: { row: MdmItemApi.Item }) => {
      handleRadioChange(row);
    },
  },
});
/** é‡ç½®æŸ¥è¯¢å’Œé€‰æ‹©çŠ¶æ€ */
async function resetQueryState() {
  selectedRows.value = [];
  await gridApi.grid.clearCheckboxRow();
  await gridApi.grid.clearCheckboxReserve();
  await gridApi.grid.clearRadioRow();
  await gridApi.formApi.resetForm();
}
/** æ‰“开物料选择弹窗 */
async function openModal(
  selectedIds?: number[],
  options?: { multiple?: boolean },
) {
  open.value = true;
  multiple.value = options?.multiple ?? true;
  preSelectedIds.value = selectedIds || [];
  await nextTick();
  gridApi.setGridOptions({
    columns: useMdmItemSelectGridColumns(multiple.value),
  });
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­ç‰©æ–™é€‰æ‹©å¼¹çª— */
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
/** ç¡®è®¤é€‰æ‹©ç‰©æ–™ */
function handleConfirm() {
  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
  if (rows.length === 0) {
    message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
    return;
  }
  emit('selected', multiple.value ? rows : [rows[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="物料选择"
    width="80%"
    :destroy-on-close="true"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <Grid table-title="物料列表" />
  </Modal>
</template>
src/views/basicData/mdm/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getItem } from '#/api/mdm/item';
import MdmItemSelectDialog from './select-dialog.vue';
defineOptions({ name: 'MdmItemSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择物料',
  },
);
const emit = defineEmits<{
  change: [item: MdmItemApi.Item | undefined];
  'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs(); // é€ä¼ å±žæ€§
const dialogRef = ref<InstanceType<typeof MdmItemSelectDialog>>(); // ç‰©æ–™é€‰æ‹©å¼¹çª—
const hovering = ref(false); // æ˜¯å¦æ‚¬åœ
const selectedItem = ref<MdmItemApi.Item>(); // å½“前选中物料
const displayLabel = computed(() => selectedItem.value?.name ?? ''); // é€‰æ‹©å™¨å±•示名称
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== null,
);
/** æ ¹æ®ç‰©æ–™ç¼–号回显选择器 */
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.id === id) {
    return;
  }
  selectedItem.value = await getItem(id);
}
watch(
  () => props.modelValue,
  (value) => {
    resolveItemById(value);
  },
  { immediate: true },
);
/** æ¸…空已选物料 */
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
/** æ‰“开物料选择弹窗 */
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  const selectedIds = (
    props.modelValue === null ? [] : [props.modelValue]
  ) as number[];
  dialogRef.value?.open(selectedIds, { multiple: false });
}
/** å›žå¡«é€‰ä¸­çš„物料 */
function handleSelected(rows: MdmItemApi.Item[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>编码:{{ selectedItem.code || '-' }}</div>
          <div>名称:{{ selectedItem.name || '-' }}</div>
          <div>规格:{{ selectedItem.specification || '-' }}</div>
          <div>单位:{{ selectedItem.unitMeasureName || '-' }}</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <MdmItemSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>
src/views/basicData/mdm/data.ts
@@ -6,7 +6,6 @@
import { z } from '#/adapter/form';
import { getUnitPage } from '#/api/mdm/unit';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
/** ç‰©æ–™ç±»åž‹é€‰é¡¹ */
export const ITEM_TYPE_OPTIONS = [
@@ -154,15 +153,11 @@
      },
    },
    {
      component: 'ApiSelect',
      component: 'Input',
      fieldName: 'warehouseId',
      label: '默认仓库',
      componentProps: {
        placeholder: '请选择默认仓库',
        allowClear: true,
        api: getWarehouseSimpleList,
        labelField: 'name',
        valueField: 'id',
        placeholder: '请输入默认仓库ID',
      },
    },
    {
@@ -243,6 +238,7 @@
      component: 'Switch',
      fieldName: 'isBatchManaged',
      label: '批次管理',
      defaultValue: true,
      componentProps: {
        class: 'w-auto',
        checkedChildren: '是',
src/views/basicData/mdm/modules/form.vue
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import type { MdmItemBatchConfigApi } from '#/api/mes/md/item-batch-config';
import type { VbenFormApi } from '#/adapter/form';
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
@@ -9,14 +11,22 @@
import { useVbenForm } from '#/adapter/form';
import { createItem, getItem, updateItem } from '#/api/mdm/item';
import { saveItemBatchConfig } from '#/api/mes/md/item-batch-config';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import ItemBatchConfig from '../components/batch-config.vue';
defineOptions({ name: 'ItemForm' });
const emit = defineEmits(['success']);
const formData = ref<MdmItemApi.Item>();
const batchConfigRef = ref<InstanceType<typeof ItemBatchConfig>>();
const batchConfig = ref<Partial<MdmItemBatchConfigApi.ItemBatchConfig>>({});
// å½“前表单中的批次管理开关值(用于控制批次配置显示)
const currentIsBatchManaged = ref<boolean>(false);
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['物料'])
@@ -34,6 +44,10 @@
  schema: useFormSchema(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
  // ç›‘听表单值变化
  handleValuesChange: (values: any) => {
    currentIsBatchManaged.value = values?.isBatchManaged === true;
  },
});
const [Modal, modalApi] = useVbenModal({
@@ -45,7 +59,18 @@
    modalApi.lock();
    const data = (await formApi.getValues()) as MdmItemApi.Item;
    try {
      await (formData.value?.id ? updateItem(data) : createItem(data));
      // ä¿å­˜ç‰©æ–™åŸºæœ¬ä¿¡æ¯
      const itemId = await (formData.value?.id ? updateItem(data) : createItem(data));
      const savedItemId = typeof itemId === 'number' ? itemId : data.id;
      // å¦‚果启用批次管理,保存批次配置
      if (data.isBatchManaged && savedItemId) {
        await saveItemBatchConfig({
          ...batchConfig.value,
          itemId: savedItemId,
        });
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
@@ -56,25 +81,50 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      batchConfig.value = {};
      currentIsBatchManaged.value = false;
      return;
    }
    const data = modalApi.getData<MdmItemApi.Item>();
    if (!data || !data.id) {
      return;
    }
    if (data?.id) {
      // ç¼–辑模式
    modalApi.lock();
    try {
      formData.value = await getItem(data.id);
      await formApi.setValues(formData.value);
        currentIsBatchManaged.value = formData.value?.isBatchManaged === true;
        // åŠ è½½æ‰¹æ¬¡é…ç½®
        if (batchConfigRef.value && currentIsBatchManaged.value) {
          await batchConfigRef.value.loadConfig();
        }
    } finally {
      modalApi.unlock();
    }
    } else {
      // æ–°å¢žæ¨¡å¼
      formData.value = undefined;
      await formApi.resetForm();
      // èŽ·å–è¡¨å•é»˜è®¤å€¼ï¼ˆisBatchManaged é»˜è®¤ true)
      const defaultValues = await formApi.getValues();
      currentIsBatchManaged.value = defaultValues?.isBatchManaged === true;
    }
  },
});
/** å¤„理批次配置变化 */
function handleBatchConfigChange(config: Partial<MdmItemBatchConfigApi.ItemBatchConfig>) {
  batchConfig.value = config;
}
</script>
<template>
  <Modal class="w-[1000px]" :title="getTitle">
    <Form class="mx-4" />
    <ItemBatchConfig
      ref="batchConfigRef"
      :item-id="formData?.id"
      :is-batch-managed="currentIsBatchManaged"
      @change="handleBatchConfigChange"
    />
  </Modal>
</template>
src/views/erp/sale/order/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
export { default as ErpSaleOrderSelect } from './select.vue';
export { default as ErpSaleOrderSelectDialog } from './select-dialog.vue';
src/views/erp/sale/order/components/select-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,199 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSaleOrderPage } from '#/api/erp/sale/order';
defineOptions({ name: 'ErpSaleOrderSelectDialog' });
const emit = defineEmits<{
  selected: [rows: ErpSaleOrderApi.SaleOrder[]];
}>();
const open = ref(false);
const multiple = ref(true);
const status = ref<number | undefined>(undefined);
const selectedRows = ref<ErpSaleOrderApi.SaleOrder[]>([]);
const preSelectedIds = ref<number[]>([]);
/** èŽ·å–å½“å‰è¡¨æ ¼æ•°æ® */
function getTableRows() {
  return gridApi.grid.getTableData().fullData as ErpSaleOrderApi.SaleOrder[];
}
/** èŽ·å–å¤šé€‰è®°å½• */
function getMultipleSelectedRows() {
  const selectedMap = new Map<number, ErpSaleOrderApi.SaleOrder>();
  const records = [
    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
  ] as ErpSaleOrderApi.SaleOrder[];
  records.forEach((row) => {
    if (row.id) {
      selectedMap.set(row.id, row);
    }
  });
  return [...selectedMap.values()];
}
/** å¤„理勾选变化 */
function handleCheckboxSelectChange() {
  selectedRows.value = getMultipleSelectedRows();
}
/** å¤„理单选变化 */
function handleRadioChange(row: ErpSaleOrderApi.SaleOrder) {
  selectedRows.value = [row];
}
/** å¤„理行双击 */
async function handleCellDblclick({ row }: { row: ErpSaleOrderApi.SaleOrder }) {
  if (multiple.value) {
    const selected = gridApi.grid.isCheckedByCheckboxRow(row);
    await gridApi.grid.setCheckboxRow(row, !selected);
    selectedRows.value = getMultipleSelectedRows();
    return;
  }
  selectedRows.value = [row];
  await gridApi.grid.setRadioRow(row);
  handleConfirm();
}
/** å›žæ˜¾é¢„选销售订单 */
async function applyPreSelection() {
  if (preSelectedIds.value.length === 0) {
    return;
  }
  const rows = getTableRows();
  for (const row of rows) {
    if (row.id && preSelectedIds.value.includes(row.id)) {
      if (multiple.value) {
        await gridApi.grid.setCheckboxRow(row, true);
      } else {
        await gridApi.grid.setRadioRow(row);
        selectedRows.value = [row];
        return;
      }
    }
  }
  if (multiple.value) {
    selectedRows.value = getMultipleSelectedRows();
  }
}
/** åˆ—表字段 */
function useGridColumns(isMultiple: boolean): VxeTableGridOptions<ErpSaleOrderApi.SaleOrder>['columns'] {
  return [
    { type: isMultiple ? 'checkbox' : 'radio', width: 50 },
    { field: 'no', title: '订单号', minWidth: 140 },
    { field: 'customerName', title: '客户', minWidth: 120 },
    { field: 'orderTime', title: '订单时间', width: 120, formatter: 'formatDate' },
    { field: 'totalCount', title: '合计数量', width: 100 },
    { field: 'totalPrice', title: '合计金额', width: 100 },
  ];
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useGridColumns(true),
    height: 400,
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      reserve: true,
    },
    radioConfig: {
      highlight: true,
      trigger: 'row',
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getSaleOrderPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            status: status.value,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
  } as VxeTableGridOptions<ErpSaleOrderApi.SaleOrder>,
  gridEvents: {
    cellDblclick: handleCellDblclick,
    checkboxAll: handleCheckboxSelectChange,
    checkboxChange: handleCheckboxSelectChange,
    radioChange: ({ row }: { row: ErpSaleOrderApi.SaleOrder }) => {
      handleRadioChange(row);
    },
  },
});
/** é‡ç½®æŸ¥è¯¢å’Œé€‰æ‹©çŠ¶æ€ */
async function resetQueryState() {
  selectedRows.value = [];
  await gridApi.grid.clearCheckboxRow();
  await gridApi.grid.clearCheckboxReserve();
  await gridApi.grid.clearRadioRow();
}
/** æ‰“开销售订单选择弹窗 */
async function openModal(
  selectedIds?: number[],
  options?: { multiple?: boolean; status?: number },
) {
  open.value = true;
  multiple.value = options?.multiple ?? true;
  status.value = options?.status;
  preSelectedIds.value = selectedIds || [];
  await nextTick();
  gridApi.setGridOptions({
    columns: useGridColumns(multiple.value),
  });
  await resetQueryState();
  await gridApi.query();
  await nextTick();
  await applyPreSelection();
}
/** å…³é—­é”€å”®è®¢å•选择弹窗 */
async function closeModal() {
  open.value = false;
  await resetQueryState();
}
/** ç¡®è®¤é€‰æ‹©é”€å”®è®¢å• */
function handleConfirm() {
  const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
  if (rows.length === 0) {
    message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
    return;
  }
  emit('selected', multiple.value ? rows : [rows[0]!]);
  open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
  <Modal
    v-model:open="open"
    title="销售订单选择"
    width="70%"
    :destroy-on-close="true"
    @ok="handleConfirm"
    @cancel="closeModal"
  >
    <Grid table-title="销售订单列表" />
  </Modal>
</template>
src/views/erp/sale/order/components/select.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getSaleOrder } from '#/api/erp/sale/order';
import ErpSaleOrderSelectDialog from './select-dialog.vue';
defineOptions({ name: 'ErpSaleOrderSelect', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    modelValue?: number;
    placeholder?: string;
    status?: number;
  }>(),
  {
    allowClear: true,
    disabled: false,
    modelValue: undefined,
    placeholder: '请选择销售订单',
    status: undefined,
  },
);
const emit = defineEmits(['change', 'update:modelValue']);
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof ErpSaleOrderSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<ErpSaleOrderApi.SaleOrder>();
const displayLabel = computed(() => selectedItem.value?.no ?? '');
const showClear = computed(
  () =>
    props.allowClear &&
    !props.disabled &&
    hovering.value &&
    props.modelValue !== null,
);
async function resolveItemById(id: number | undefined) {
  if (!id) {
    selectedItem.value = undefined;
    return;
  }
  if (selectedItem.value?.id === id) {
    return;
  }
  selectedItem.value = await getSaleOrder(id);
}
watch(
  () => props.modelValue,
  (value) => {
    resolveItemById(value);
  },
  { immediate: true },
);
function clearSelected() {
  selectedItem.value = undefined;
  emit('update:modelValue', undefined);
  emit('change', undefined);
}
function handleClick(event: MouseEvent) {
  if (props.disabled) {
    return;
  }
  const target = event.target as HTMLElement;
  if (showClear.value && target.closest('.ant-input-suffix')) {
    event.stopPropagation();
    clearSelected();
    return;
  }
  const selectedIds = (
    props.modelValue === null ? [] : [props.modelValue]
  ) as number[];
  dialogRef.value?.open(selectedIds, { multiple: false, status: props.status });
}
function handleSelected(rows: ErpSaleOrderApi.SaleOrder[]) {
  const item = rows[0];
  if (!item) {
    return;
  }
  selectedItem.value = item;
  emit('update:modelValue', item.id);
  emit('change', item);
}
</script>
<template>
  <div
    v-bind="attrs"
    class="w-full"
    :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
    @click="handleClick"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
      <template #title>
        <div v-if="selectedItem" class="leading-6">
          <div>订单号:{{ selectedItem.no || '-' }}</div>
          <div>客户:{{ selectedItem.customerName || '-' }}</div>
          <div>订单时间:{{ selectedItem.orderTime || '-' }}</div>
          <div>合计金额:{{ selectedItem.totalPrice || '-' }} å…ƒ</div>
        </div>
      </template>
      <Input
        :disabled="disabled"
        :placeholder="placeholder"
        :value="displayLabel"
        readonly
      >
        <template #suffix>
          <IconifyIcon
            class="size-4"
            :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
          />
        </template>
      </Input>
    </Tooltip>
  </div>
  <ErpSaleOrderSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>
src/views/erp/sale/order/data.ts
@@ -270,7 +270,7 @@
    },
    {
      title: '操作',
      width: 50,
      width: 320,
      fixed: 'right',
      slots: { default: 'actions' },
      visible: !disabled,
src/views/erp/sale/order/index.vue
@@ -19,6 +19,7 @@
  getSaleOrderPage,
  updateSaleOrderStatus,
} from '#/api/erp/sale/order';
import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -136,9 +137,23 @@
function handleViewContract(row: ErpSaleOrderApi.SaleOrder) {
  if (row.contractId) {
    router.push({
      path: '/crm/contract/detail',
      query: { id: row.contractId },
      path: `/crm/contract/detail/${row.contractId}`,
    });
  }
}
/** ç”Ÿæˆå‘货通知单 */
async function handleGenerateNotice(row: ErpSaleOrderApi.SaleOrder) {
  try {
    const noticeId = await generateFromSaleOrder(row.id!);
    message.success('生成发货通知单成功');
    // è·³è½¬åˆ°å‘货通知单页面
    router.push({
      path: '/wls/sales-notice',
      query: { id: noticeId },
    });
  } catch (e) {
    console.error(e);
  }
}
@@ -296,6 +311,14 @@
              },
            },
            {
              label: '生成发货通知单',
              type: 'link',
              icon: 'ant-design:file-protect-outlined',
              auth: ['mes:wm-sales-notice:create'],
              ifShow: () => row.status === 20,
              onClick: handleGenerateNotice.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
src/views/wls/materialstock/components/batch-config-display.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { MdmItemBatchConfigApi } from '#/api/mes/md/item-batch-config';
import { computed, ref, watch } from 'vue';
import { Divider, Tag } from 'ant-design-vue';
import { getItemBatchConfigByItemId } from '#/api/mes/md/item-batch-config';
const props = defineProps<{
  itemId?: number;
  isBatchManaged?: boolean;
}>();
// æ‰¹æ¬¡é…ç½®æ•°æ®
const config = ref<MdmItemBatchConfigApi.ItemBatchConfig | null>(null);
// æ˜¯å¦åŠ è½½ä¸­
const loading = ref(false);
// æ˜¯å¦æ˜¾ç¤ºï¼ˆå¯ç”¨æ‰¹æ¬¡ç®¡ç†æ—¶æ˜¾ç¤ºï¼‰
const visible = computed(() => props.isBatchManaged === true);
// å·²å¯ç”¨çš„æ‰¹æ¬¡å±žæ€§åˆ—表(仅保留7个字段)
const enabledProps = computed(() => {
  if (!config.value) return [];
  const props: { label: string; value: boolean }[] = [];
  if (config.value.produceDateFlag) props.push({ label: '生产日期', value: true });
  if (config.value.expireDateFlag) props.push({ label: '有效期', value: true });
  if (config.value.receiptDateFlag) props.push({ label: '入库日期', value: true });
  if (config.value.vendorFlag) props.push({ label: '供应商', value: true });
  if (config.value.purchaseOrderCodeFlag) props.push({ label: '采购订单号', value: true });
  if (config.value.lotNumberFlag) props.push({ label: '生产批号', value: true });
  if (config.value.qualityStatusFlag) props.push({ label: '质量状态', value: true });
  return props;
});
/** åŠ è½½æ‰¹æ¬¡é…ç½® */
async function loadConfig() {
  if (!props.itemId || !props.isBatchManaged) {
    config.value = null;
    return;
  }
  loading.value = true;
  try {
    const data = await getItemBatchConfigByItemId(props.itemId);
    config.value = data;
  } catch {
    config.value = null;
  } finally {
    loading.value = false;
  }
}
/** é‡ç½® */
function reset() {
  config.value = null;
  loading.value = false;
}
// ç›‘听 itemId å˜åŒ–,加载配置
watch(
  () => [props.itemId, props.isBatchManaged],
  () => {
    if (props.itemId && props.isBatchManaged) {
      loadConfig();
    } else {
      reset();
    }
  },
  { immediate: true },
);
defineExpose({
  loadConfig,
  reset,
});
</script>
<template>
  <div v-if="visible" class="mt-4 rounded border border-gray-200 bg-gray-50 p-4">
    <Divider orientation="left">批次属性</Divider>
    <div v-if="loading" class="text-center text-gray-500">加载中...</div>
    <div v-else-if="enabledProps.length === 0" class="text-center text-gray-500">
      è¯¥ç‰©æ–™æœªé…ç½®æ‰¹æ¬¡å±žæ€§
    </div>
    <div v-else class="flex flex-wrap gap-2">
      <Tag v-for="prop in enabledProps" :key="prop.label" color="blue">
        {{ prop.label }}
      </Tag>
    </div>
    <p class="mt-2 text-xs text-gray-500">
      ä»¥ä¸Šå­—段创建批次时必填
    </p>
  </div>
</template>
src/views/wls/materialstock/components/index.ts
@@ -1,2 +1,3 @@
export { default as WmMaterialStockSelectDialog } from './select-dialog.vue';
export { default as WmMaterialStockSelect } from './select.vue';
export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
src/views/wls/materialstock/components/stock-adjust-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,488 @@
<script lang="ts" setup>
import type {
  MaterialStockAdjustInParams,
  MaterialStockAdjustOutParams,
} from '#/api/mes/wm/materialstock';
import type { MdmItemApi } from '#/api/mdm/item';
import type { MdmItemBatchConfigApi } from '#/api/mes/md/item-batch-config';
import type { VbenFormSchema } from '#/adapter/form';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { z } from '#/adapter/form';
import { useVbenForm } from '#/adapter/form';
import { message } from 'ant-design-vue';
import { markRaw } from 'vue';
import {
  adjustMaterialStockIn,
  adjustMaterialStockOut,
} from '#/api/mes/wm/materialstock';
import { getItem } from '#/api/mdm/item';
import { getItemBatchConfigByItemId } from '#/api/mes/md/item-batch-config';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
import BatchConfigDisplay from './batch-config-display.vue';
import {
  WmWarehouseAreaSelect,
  WmWarehouseLocationSelect,
  WmWarehouseSelect,
} from '#/views/wls/warehouse/components';
const emit = defineEmits<{
  success: [];
}>();
// æ“ä½œç±»åž‹ï¼š1=入库,2=出库
const operateType = ref<1 | 2>(1);
const selectedItem = ref<MdmItemApi.Item | null>(null);
const selectedWarehouse = ref<any>(null);
const selectedLocation = ref<any>(null);
// æ˜¯å¦å¯ç”¨æ‰¹æ¬¡ç®¡ç†
const batchEnabled = ref(false);
// æ˜¯å¦æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·
const manualBatchEntry = ref(false);
// ç‰©æ–™æ‰¹æ¬¡é…ç½®
const batchConfig = ref<MdmItemBatchConfigApi.ItemBatchConfig | null>(null);
// æ‰¹æ¬¡é…ç½®å±•示组件引用
const batchConfigDisplayRef = ref<InstanceType<typeof BatchConfigDisplay>>();
// æ ‡é¢˜
const modalTitle = ref('库存调整');
/** åŠ è½½ç‰©æ–™æ‰¹æ¬¡é…ç½®å¹¶æ›´æ–°å­—æ®µæ˜¾ç¤º */
async function loadBatchConfigAndUpdateFields(itemId: number) {
  try {
    const config = await getItemBatchConfigByItemId(itemId);
    batchConfig.value = config;
  } catch {
    batchConfig.value = null;
  }
  await updateBatchFields();
}
/** æ›´æ–°æ‰¹æ¬¡å­—段显示(根据配置展示已启用的属性) */
async function updateBatchFields() {
  const enabled = selectedItem.value?.isBatchManaged === true;
  batchEnabled.value = enabled;
  if (!enabled) {
    // éšè—æ‰€æœ‰æ‰¹æ¬¡ç›¸å…³å­—段
    formApi.updateSchema([
      { fieldName: 'batchCode', dependencies: { show: () => false } },
      { fieldName: 'produceDate', dependencies: { show: () => false } },
      { fieldName: 'expireDate', dependencies: { show: () => false } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false } },
      { fieldName: 'vendorId', dependencies: { show: () => true } },
    ]);
    return;
  }
  // å¯ç”¨æ‰¹æ¬¡ç®¡ç†ï¼Œæ ¹æ®é…ç½®æ˜¾ç¤ºå­—段
  const config = batchConfig.value;
  const isIn = operateType.value === 1;
  // æ˜¯å¦æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·æŽ§åˆ¶æ‰¹æ¬¡å·å­—æ®µçš„æ˜¾ç¤º
  const showBatchCode = manualBatchEntry.value;
  const showProduceDate = isIn && !!config?.produceDateFlag;
  const showExpireDate = isIn && !!config?.expireDateFlag;
  formApi.updateSchema([
    { fieldName: 'batchCode', dependencies: { show: () => showBatchCode } },
    { fieldName: 'produceDate', dependencies: { show: () => showProduceDate } },
    { fieldName: 'expireDate', dependencies: { show: () => showExpireDate } },
    { fieldName: 'manualBatchEntry', dependencies: { show: () => true } },
  ]);
}
/** ç‰©æ–™é€‰æ‹©å˜åŒ– */
async function handleItemChange(item: MdmItemApi.Item | undefined) {
  selectedItem.value = item || null;
  if (item?.id) {
    const itemDetail = await getItem(item.id);
    selectedItem.value = itemDetail;
    // åŠ è½½ç‰©æ–™æ‰¹æ¬¡é…ç½®å¹¶æ›´æ–°å­—æ®µ
    await loadBatchConfigAndUpdateFields(item.id);
  } else {
    batchConfig.value = null;
    batchEnabled.value = false;
  }
  // æ¸…空批次相关值
  await formApi.setFieldValue('batchCode', undefined);
  await formApi.setFieldValue('produceDate', undefined);
  await formApi.setFieldValue('expireDate', undefined);
  await formApi.setFieldValue('manualBatchEntry', false);
}
/** ä»“库选择变化 */
async function handleWarehouseChange(item: any) {
  selectedWarehouse.value = item;
  await formApi.setFieldValue('locationId', undefined);
  await formApi.setFieldValue('areaId', undefined);
}
/** åº“区选择变化 */
async function handleLocationChange(item: any) {
  selectedLocation.value = item;
  await formApi.setFieldValue('areaId', undefined);
}
/** æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·å˜åŒ– */
async function handleManualBatchEntryChange(value: boolean) {
  manualBatchEntry.value = value;
  await updateBatchFields();
  if (!value) {
    await formApi.setFieldValue('batchCode', undefined);
  }
}
/** æ“ä½œç±»åž‹å˜åŒ– */
async function handleOperateTypeChange(type: 1 | 2) {
  operateType.value = type;
  modalTitle.value = type === 1 ? '库存入库' : '库存出库';
  await formApi.setFieldValue('operateType', type);
  // æ›´æ–°æ‰¹æ¬¡å­—段显示
  await updateBatchFields();
}
/** èŽ·å–è¡¨å• schema */
function getSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'operateType',
      label: '操作类型',
      component: 'RadioGroup',
      defaultValue: 1,
      componentProps: {
        options: [
          { label: '入库', value: 1 },
          { label: '出库', value: 2 },
        ],
        onChange: handleOperateTypeChange,
      },
      rules: 'required',
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdmItemSelect),
      componentProps: {
        placeholder: '请选择物料',
        onChange: handleItemChange,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'warehouseId',
      label: '仓库',
      component: markRaw(WmWarehouseSelect),
      componentProps: {
        placeholder: '请选择仓库',
        onChange: handleWarehouseChange,
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'locationId',
      label: '库区',
      component: markRaw(WmWarehouseLocationSelect),
      componentProps: {
        placeholder: '请选择库区',
        warehouseId: selectedWarehouse.value?.id,
        onChange: handleLocationChange,
      },
      dependencies: {
        triggerFields: ['warehouseId'],
        componentProps: (values: any) => ({
          placeholder: '请选择库区',
          warehouseId: values.warehouseId,
          onChange: handleLocationChange,
        }),
        trigger: (values: any, formApi: any) => {
          if (values.locationId !== undefined) {
            void formApi.setFieldValue('locationId', undefined);
            void formApi.setFieldValue('areaId', undefined);
          }
        },
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'areaId',
      label: '库位',
      component: markRaw(WmWarehouseAreaSelect),
      componentProps: {
        placeholder: '请选择库位',
        locationId: selectedLocation.value?.id,
      },
      dependencies: {
        triggerFields: ['locationId'],
        componentProps: (values: any) => ({
          placeholder: '请选择库位',
          locationId: values.locationId,
        }),
        trigger: (values: any, formApi: any) => {
          if (values.areaId !== undefined) {
            void formApi.setFieldValue('areaId', undefined);
          }
        },
      },
      rules: 'selectRequired',
    },
    // æ˜¯å¦æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·ï¼ˆå¯ç”¨æ‰¹æ¬¡ç®¡ç†æ—¶æ˜¾ç¤ºï¼‰
    {
      fieldName: 'manualBatchEntry',
      label: '手动填写批次号',
      component: 'Switch',
      defaultValue: false,
      componentProps: {
        class: 'w-auto',
        checkedChildren: '是',
        unCheckedChildren: '否',
        onChange: handleManualBatchEntryChange,
      },
      dependencies: {
        triggerFields: ['itemId'],
        show: () => false, // é»˜è®¤éšè—ï¼Œå¯ç”¨æ‰¹æ¬¡ç®¡ç†æ—¶æ˜¾ç¤º
      },
    },
    // æ‰¹æ¬¡å·ï¼ˆæ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·æ—¶æ˜¾ç¤ºï¼‰
    {
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入批次号',
      },
      dependencies: {
        triggerFields: ['itemId', 'manualBatchEntry'],
        show: () => false, // é»˜è®¤éšè—ï¼Œæ ¹æ®æ‰‹åŠ¨å¡«å†™è®¾ç½®æ˜¾ç¤º
      },
      // æ ¹æ®é…ç½®å†³å®šæ˜¯å¦å¿…å¡«
      rules: z.string().optional(),
    },
    // ç”Ÿäº§æ—¥æœŸï¼ˆæ ¹æ®æ‰¹æ¬¡é…ç½®æ˜¾ç¤ºï¼Œä»…入库)
    {
      fieldName: 'produceDate',
      label: '生产日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择生产日期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      dependencies: {
        triggerFields: ['itemId'],
        show: () => false, // é»˜è®¤éšè—
      },
      rules: 'required',
    },
    // æœ‰æ•ˆæœŸï¼ˆæ ¹æ®æ‰¹æ¬¡é…ç½®æ˜¾ç¤ºï¼Œä»…入库)
    {
      fieldName: 'expireDate',
      label: '有效期至',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择有效期',
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      dependencies: {
        triggerFields: ['itemId'],
        show: () => false, // é»˜è®¤éšè—
      },
    },
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(MdVendorSelect),
      componentProps: {
        placeholder: '请选择供应商',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'quantity',
      label: '数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入数量',
        precision: 2,
        min: 0.01,
      },
      rules: z.number().positive('数量必须大于0'),
    },
    {
      fieldName: 'reason',
      label: '原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入原因',
        rows: 3,
      },
    },
  ];
}
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: getSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  fullscreen: false,
  onCancel: () => {
    modalApi.close();
  },
  onConfirm: async () => {
    await handleSubmit();
  },
  onOpenChange: async (isOpen: boolean) => {
    if (!isOpen) {
      selectedItem.value = null;
      selectedWarehouse.value = null;
      selectedLocation.value = null;
      batchEnabled.value = false;
      batchConfig.value = null;
      operateType.value = 1;
      modalTitle.value = '库存调整';
      return;
    }
    // é‡ç½®è¡¨å•
    await formApi.resetForm();
    // é»˜è®¤éšè—æ‰¹æ¬¡ç›¸å…³å­—段
    formApi.updateSchema([
      { fieldName: 'batchCode', dependencies: { show: () => false } },
      { fieldName: 'produceDate', dependencies: { show: () => false } },
      { fieldName: 'expireDate', dependencies: { show: () => false } },
      { fieldName: 'manualBatchEntry', dependencies: { show: () => false } },
    ]);
    // é‡ç½®æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·çŠ¶æ€
    manualBatchEntry.value = false;
    // å¦‚果有传入物料信息,回填表单
    const data = modalApi.getData();
    if (data?.itemId) {
      await formApi.setValues({
        itemId: data.itemId,
        warehouseId: data.warehouseId,
        locationId: data.locationId,
        areaId: data.areaId,
      });
      if (data.itemId) {
        const itemDetail = await getItem(data.itemId);
        selectedItem.value = itemDetail;
        await loadBatchConfigAndUpdateFields(data.itemId);
      }
    }
  },
  title: modalTitle,
});
/** æäº¤è¡¨å• */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid) {
    return;
  }
  const values = await formApi.getValues();
  const {
    itemId,
    warehouseId,
    locationId,
    areaId,
    batchCode,
    produceDate,
    expireDate,
    vendorId,
    quantity,
    reason,
  } = values;
  // å¯ç”¨æ‰¹æ¬¡ç®¡ç†æ—¶çš„æ ¡éªŒ
  if (batchEnabled.value) {
    // æ‰‹åŠ¨å¡«å†™æ‰¹æ¬¡å·æ—¶ï¼Œæ‰¹æ¬¡å·å¿…å¡«
    if (manualBatchEntry.value && !batchCode) {
      message.error('请输入批次号');
      return;
    }
    // æ ¹æ®æ‰¹æ¬¡é…ç½®æ ¡éªŒå¿…填字段(仅入库)
    if (operateType.value === 1) {
      const config = batchConfig.value;
      if (config?.produceDateFlag && !produceDate) {
        message.error('请选择生产日期');
        return;
      }
      if (config?.expireDateFlag && !expireDate) {
        message.error('请选择有效期');
        return;
      }
    }
  }
  try {
    const type = operateType.value;
    if (type === 1) {
      // å…¥åº“
      const params: MaterialStockAdjustInParams = {
        itemId,
        warehouseId,
        locationId,
        areaId,
        batchCode,
        produceDate,
        expireDate,
        vendorId,
        quantity,
        reason,
      };
      await adjustMaterialStockIn(params);
      message.success('入库成功');
    } else {
      // å‡ºåº“
      const params: MaterialStockAdjustOutParams = {
        itemId,
        warehouseId,
        locationId,
        areaId,
        batchCode,
        vendorId,
        quantity,
        reason,
      };
      await adjustMaterialStockOut(params);
      message.success('出库成功');
    }
    emit('success');
    modalApi.close();
  } catch (error) {
    // é”™è¯¯ç”±è¯·æ±‚拦截器处理
  }
}
</script>
<template>
  <Modal class="w-3/5">
    <Form class="mx-4" />
    <BatchConfigDisplay
      ref="batchConfigDisplayRef"
      :item-id="selectedItem?.id"
      :is-batch-managed="batchEnabled"
    />
  </Modal>
</template>
src/views/wls/materialstock/data.ts
@@ -108,6 +108,41 @@
      width: 100,
    },
    {
      field: 'frozenQuantity',
      title: '冻结数量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'reservedQuantity',
      title: '占用量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'inTransitQuantity',
      title: '在途量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'availableQuantity',
      title: '可用量',
      width: 100,
      cellRender: {
        name: 'CellText',
        props: {
          class: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) =>
            (row.availableQuantity || 0) < 0 ? 'text-red-500 font-bold' : '',
          formatter: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) =>
            ((row.availableQuantity || 0) as number).toFixed(2),
        },
      },
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      width: 80,
@@ -288,6 +323,41 @@
      width: 100,
    },
    {
      field: 'frozenQuantity',
      title: '冻结数量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'reservedQuantity',
      title: '占用量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'inTransitQuantity',
      title: '在途量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) =>
        (cellValue || 0).toFixed(2),
    },
    {
      field: 'availableQuantity',
      title: '可用量',
      width: 100,
      cellRender: {
        name: 'CellText',
        props: {
          class: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) =>
            (row.availableQuantity || 0) < 0 ? 'text-red-500 font-bold' : '',
          formatter: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) =>
            ((row.availableQuantity || 0) as number).toFixed(2),
        },
      },
    },
    {
      field: 'receiptTime',
      title: '入库日期',
      width: 180,
src/views/wls/materialstock/index.vue
@@ -19,11 +19,17 @@
import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
import { WmBatchDetail } from '#/views/wls/batch/components';
import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
import { WmStockAdjustModal } from './components';
import { useGridColumns, useGridFormSchema } from './data';
const [AreaModal, areaModalApi] = useVbenModal({
  connectedComponent: AreaForm,
  destroyOnClose: true,
});
const [StockAdjustModal, stockAdjustModalApi] = useVbenModal({
  connectedComponent: WmStockAdjustModal,
  destroyOnClose: true,
});
@@ -56,6 +62,16 @@
    return;
  }
  areaModalApi.setData({ formType: 'detail', id: row.areaId }).open();
}
/** æ‰“开库存调整弹窗 */
function handleOpenStockAdjust() {
  stockAdjustModalApi.setData({}).open();
}
/** åº“存调整成功回调 */
function handleStockAdjustSuccess() {
  gridApi.query();
}
/** æ‰“开批次详情弹窗 */
@@ -122,6 +138,7 @@
<template>
  <Page auto-content-height><AreaModal />
    <WmBatchDetail ref="batchDetailRef" />
    <StockAdjustModal @success="handleStockAdjustSuccess" />
    <div class="flex h-full w-full">
      <!-- å·¦ä¾§ç‰©æ–™åˆ†ç±»æ ‘ -->
@@ -135,6 +152,13 @@
            <TableAction
              :actions="[
                {
                  label: '库存调整',
                  type: 'default',
                  icon: ACTION_ICON.EDIT,
                  auth: ['mes:wm-material-stock:adjust'],
                  onClick: handleOpenStockAdjust,
                },
                {
                  label: $t('ui.actionTitle.export'),
                  type: 'primary',
                  icon: ACTION_ICON.DOWNLOAD,
src/views/wls/salesnotice/data.ts
@@ -5,12 +5,17 @@
import { h, markRaw } from 'vue';
import { DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import {
  DICT_TYPE,
  MesAutoCodeRuleCode,
  MesWmSalesNoticeOutStatusEnum,
} from '@vben/constants';
import { Button } from 'ant-design-vue';
import { Button, Tag } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
@@ -79,11 +84,27 @@
      rules: 'required',
    },
    {
      fieldName: 'salesOrderCode',
      label: '销售订单编号',
      component: 'Input',
      fieldName: 'saleOrderId',
      label: '销售订单',
      component: markRaw(ErpSaleOrderSelect),
      componentProps: {
        placeholder: '请输入销售订单编号',
        placeholder: '请选择销售订单',
        // é€‰æ‹©é”€å”®è®¢å•后,自动回填客户信息
        onChange: (item: any) => {
          if (item?.customerId && formApi) {
            formApi.setFieldValue('clientId', item.customerId);
            formApi.setFieldValue('clientName', item.customerName);
          }
        },
      },
    },
    {
      fieldName: 'clientName',
      label: '',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
@@ -92,6 +113,15 @@
      component: markRaw(MdClientSelect),
      componentProps: {
        placeholder: '请选择客户',
        disabled: true,
      },
      dependencies: {
        triggerFields: ['clientName'],
        componentProps: (values) => ({
          placeholder: '请选择客户',
          disabled: true,
          clientName: values.clientName as string,
        }),
      },
      rules: 'selectRequired',
    },
@@ -240,6 +270,25 @@
      },
    },
    {
      field: 'outStatus',
      title: '出库状态',
      minWidth: 100,
      cellRender: {
        name: 'CellTag',
        props: {
          formatter: ({ row }: { row: MesWmSalesNoticeApi.SalesNotice }) => {
            const statusMap: Record<number, { color: string; label: string }> = {
              [MesWmSalesNoticeOutStatusEnum.NONE]: { color: 'default', label: '未出库' },
              [MesWmSalesNoticeOutStatusEnum.PARTIAL]: { color: 'warning', label: '部分出库' },
              [MesWmSalesNoticeOutStatusEnum.ALL]: { color: 'success', label: '全部出库' },
            };
            const status = statusMap[row.outStatus ?? MesWmSalesNoticeOutStatusEnum.NONE];
            return { color: status?.color, label: status?.label ?? '未出库' };
          },
        },
      },
    },
    {
      title: '操作',
      width: 200,
      fixed: 'right',
@@ -279,6 +328,28 @@
      width: 100,
    },
    {
      field: 'outCount',
      title: '已出库数量',
      width: 100,
      formatter: ({ cellValue }: { cellValue: number }) => (cellValue || 0).toFixed(2),
    },
    {
      field: 'outProgress',
      title: '出库进度',
      width: 120,
      cellRender: {
        name: 'CellText',
        props: {
          formatter: ({ row }: { row: MesWmSalesNoticeLineApi.SalesNoticeLine }) => {
            const quantity = row.quantity || 0;
            const outCount = row.outCount || 0;
            const percent = quantity > 0 ? Math.round((outCount / quantity) * 100) : 0;
            return `${outCount.toFixed(2)} / ${quantity.toFixed(2)} (${percent}%)`;
          },
        },
      },
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
src/views/wls/salesnotice/index.vue
@@ -8,10 +8,13 @@
import { Button, message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteSalesNotice,
  exportSalesNotice,
  generateProductSales,
  getSalesNoticePage,
} from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
@@ -23,6 +26,8 @@
  connectedComponent: Form,
  destroyOnClose: true,
});
const router = useRouter();
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
@@ -47,6 +52,21 @@
/** æ‰§è¡Œå‡ºåº“ */
function handleFinish(row: MesWmSalesNoticeApi.SalesNotice) {
  formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** ç”Ÿæˆé”€å”®å‡ºåº“单 */
async function handleGenerateProductSales(row: MesWmSalesNoticeApi.SalesNotice) {
  try {
    const productSalesId = await generateProductSales(row.id!);
    message.success('生成销售出库单成功');
    // è·³è½¬åˆ°é”€å”®å‡ºåº“单页面
    router.push({
      path: '/wls/product-sales',
      query: { id: productSalesId },
    });
  } catch (e) {
    console.error(e);
  }
}
/** åˆ é™¤å‘货通知单 */
@@ -154,6 +174,14 @@
              },
            },
            {
              label: '生成出库单',
              type: 'link',
              icon: 'ant-design:file-protect-outlined',
              auth: ['mes:wm-product-sales:create'],
              ifShow: row.status === MesWmSalesNoticeStatusEnum.APPROVED,
              onClick: handleGenerateProductSales.bind(null, row),
            },
            {
              label: '执行出库',
              type: 'link',
              auth: ['mes:wm-sales-notice:update'],