吴羡
22 小时以前 37c7825ba84000caee76476cb734fec6e006569b
feat: 完成好水川装箱、工单成本、留样与发货统计页面
已添加7个文件
已修改23个文件
1352 ■■■■■ 文件已修改
src/api/mes/md/item/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/type/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/vendor/index.ts 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/mps/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/retention-sample/index.ts 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/packages/line/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productsales/line/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/productsales/statistics/index.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/data.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/type/data.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/data.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/components/gantt-chart.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/schedule-form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/components/select-dialog.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/components/select.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/retentionsample/data.ts 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/retentionsample/index.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/retentionsample/modules/form.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/data.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/modules/line-form.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/modules/package-line-list.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/packages/modules/sub-package-list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productsales/data.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/shipmentstatistics/data.ts 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/shipmentstatistics/index.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/md/item/index.ts
@@ -20,6 +20,7 @@
    maxStock?: number; // æœ€é«˜åº“存量
    highValue?: boolean; // æ˜¯å¦é«˜å€¼ç‰©æ–™
    batchFlag?: boolean; // æ˜¯å¦å¯ç”¨æ‰¹æ¬¡ç®¡ç†
    boxQuantity?: number; // æ¯ç®±æ•°é‡
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
src/api/mes/md/item/type/index.ts
@@ -8,6 +8,7 @@
    code?: string; // åˆ†ç±»ç¼–码
    name?: string; // åˆ†ç±»åç§°
    itemOrProduct?: string; // ç‰©æ–™/产品标识
    finishedFlag?: boolean; // æ˜¯å¦æˆå“åˆ†ç±»
    sort?: number; // æ˜¾ç¤ºæŽ’序
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
src/api/mes/md/vendor/index.ts
@@ -1,9 +1,10 @@
import type { PageParam, PageResult } from '@vben/request';
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { requestClient } from '#/api/request';
export namespace MesMdVendorApi {
  /** MES ä¾›åº”商 */
  /** MES ä¾›åº”商(数据来源:SRM ä¾›åº”商档案) */
  export interface Vendor {
    id?: number; // ä¾›åº”商编号
    code?: string; // ä¾›åº”商编码
@@ -29,59 +30,66 @@
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
  /** ä¾›åº”商导入结果 */
  export interface VendorImportRespVO {
    createCodes?: string[]; // æ–°å¢žæˆåŠŸçš„ä¾›åº”å•†ç¼–ç 
    updateCodes?: string[]; // æ›´æ–°æˆåŠŸçš„ä¾›åº”å•†ç¼–ç 
    failureCodes?: Record<string, string>; // å¯¼å…¥å¤±è´¥çš„供应商编码及原因
  }
}
/** æŸ¥è¯¢ä¾›åº”商分页 */
export function getVendorPage(params: PageParam) {
  return requestClient.get<PageResult<MesMdVendorApi.Vendor>>(
    '/mes/md-vendor/page',
    { params },
/** SRM ä¾›åº”商档案返回字段(含前端类型尚未声明的简称、官网) */
interface SrmSupplierRow extends SrmSupplierApi.SupplierVO {
  shortName?: string;
  website?: string;
}
/** SRM ä¾›åº”商分页查询参数 */
interface VendorPageParams extends PageParam {
  code?: string;
  name?: string;
  status?: number;
}
function toVendor(supplier: SrmSupplierRow): MesMdVendorApi.Vendor {
  return {
    id: supplier.id,
    code: supplier.code,
    name: supplier.name,
    nickname: supplier.shortName,
    address: supplier.address,
    website: supplier.website,
    email: supplier.contactEmail,
    telephone: supplier.contactMobile,
    contact1Name: supplier.contactName,
    contact1Telephone: supplier.contactMobile,
    contact1Email: supplier.contactEmail,
    creditCode: supplier.taxNo,
    status: supplier.status,
    remark: supplier.remark,
    createTime: supplier.createTime ? new Date(supplier.createTime) : undefined,
  };
}
/** æŸ¥è¯¢ä¾›åº”商分页(读取 SRM ä¾›åº”商档案) */
export async function getVendorPage(params: VendorPageParams) {
  const page = await requestClient.get<PageResult<SrmSupplierRow>>(
    '/srm/supplier/page',
    {
      params: {
        pageNo: params.pageNo,
        pageSize: params.pageSize,
        code: params.code,
        name: params.name,
        status: params.status,
      },
    },
  );
  return {
    list: page.list.map((supplier) => toVendor(supplier)),
    total: page.total,
  } as PageResult<MesMdVendorApi.Vendor>;
}
/** æŸ¥è¯¢ä¾›åº”商详情 */
export function getVendor(id: number) {
  return requestClient.get<MesMdVendorApi.Vendor>(
    `/mes/md-vendor/get?id=${id}`,
/** æŸ¥è¯¢ä¾›åº”商详情(读取 SRM ä¾›åº”商档案) */
export async function getVendor(id: number) {
  const supplier = await requestClient.get<SrmSupplierRow>(
    '/srm/supplier/get',
    { params: { id } },
  );
}
/** æ–°å¢žä¾›åº”商 */
export function createVendor(data: MesMdVendorApi.Vendor) {
  return requestClient.post('/mes/md-vendor/create', data);
}
/** ä¿®æ”¹ä¾›åº”商 */
export function updateVendor(data: MesMdVendorApi.Vendor) {
  return requestClient.put('/mes/md-vendor/update', data);
}
/** åˆ é™¤ä¾›åº”商 */
export function deleteVendor(id: number) {
  return requestClient.delete(`/mes/md-vendor/delete?id=${id}`);
}
/** å¯¼å‡ºä¾›åº”商 */
export function exportVendor(params: any) {
  return requestClient.download('/mes/md-vendor/export-excel', { params });
}
/** ä¸‹è½½ä¾›åº”商导入模板 */
export function importVendorTemplate() {
  return requestClient.download('/mes/md-vendor/get-import-template');
}
/** å¯¼å…¥ä¾›åº”商 */
export function importVendor(file: File, updateSupport: boolean) {
  return requestClient.upload<MesMdVendorApi.VendorImportRespVO>(
    `/mes/md-vendor/import?updateSupport=${updateSupport}`,
    { file },
  );
  return toVendor(supplier);
}
src/api/mes/pro/mps/index.ts
@@ -38,6 +38,7 @@
    productId?: number;
    clientId?: number;
    status?: number;
    workOrderId?: number;
    requestDate?: number[];
  }
src/api/mes/pro/workorder/index.ts
@@ -21,6 +21,9 @@
    quantityProduced?: number; // å·²ç”Ÿäº§æ•°é‡
    quantityChanged?: number; // è°ƒæ•´æ•°é‡
    quantityScheduled?: number; // å·²æŽ’产数量
    packedQuantity?: number; // å·²è£…箱数量
    packedBoxCount?: number; // å·²è£…箱箱数
    packingStatus?: number; // è£…箱状态:0=未装箱 1=部分已装箱 2=已装箱完成
    clientId?: number; // å®¢æˆ·ç¼–号
    clientName?: string; // å®¢æˆ·åç§°
    vendorId?: number; // ä¾›åº”商编号
@@ -52,6 +55,7 @@
    clientId?: number;
    status?: number;
    type?: number;
    packingStatus?: number;
    requestDate?: number[];
  }
}
src/api/mes/qc/retention-sample/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesQcRetentionSampleApi {
  /** MES ç•™æ ·è®°å½• */
  export interface RetentionSample {
    id?: number;
    code?: string;
    workOrderId?: number;
    workOrderCode?: string;
    saleOrderId?: number;
    saleOrderNo?: string;
    productId?: number;
    productCode?: string;
    productName?: string;
    productSpecification?: string;
    batchCode?: string;
    sampleQuantity?: number;
    sampleDate?: string;
    expiryDate?: string;
    keeperUserId?: number;
    keeperNickname?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢ç•™æ ·è®°å½•分页 */
export function getRetentionSamplePage(params: PageParam) {
  return requestClient.get<PageResult<MesQcRetentionSampleApi.RetentionSample>>(
    '/mes/qc/retention-sample/page',
    { params },
  );
}
/** æŸ¥è¯¢ç•™æ ·è®°å½•详情 */
export function getRetentionSample(id: number) {
  return requestClient.get<MesQcRetentionSampleApi.RetentionSample>(
    `/mes/qc/retention-sample/get?id=${id}`,
  );
}
/** æ–°å¢žç•™æ ·è®°å½• */
export function createRetentionSample(
  data: MesQcRetentionSampleApi.RetentionSample,
) {
  return requestClient.post('/mes/qc/retention-sample/create', data);
}
/** ä¿®æ”¹ç•™æ ·è®°å½• */
export function updateRetentionSample(
  data: MesQcRetentionSampleApi.RetentionSample,
) {
  return requestClient.put('/mes/qc/retention-sample/update', data);
}
/** åˆ é™¤ç•™æ ·è®°å½• */
export function deleteRetentionSample(id: number) {
  return requestClient.delete(`/mes/qc/retention-sample/delete?id=${id}`);
}
src/api/mes/wm/packages/line/index.ts
@@ -14,6 +14,9 @@
    specification?: string; // è§„格型号
    unitMeasureName?: string; // è®¡é‡å•位名称
    quantity?: number; // è£…箱数量
    boxCount?: number; // ç®±æ•°
    tailBoxQuantity?: number; // å°¾ç®±æ•°é‡ï¼ˆæœ€åŽä¸€ç®±å®žé™…装的数量)
    tailBoxFull?: boolean; // å°¾ç®±æ˜¯å¦æ»¡è£…
    workOrderId?: number; // ç”Ÿäº§è®¢å•编号
    workOrderCode?: string; // ç”Ÿäº§è®¢å•编号
    batchCode?: string; // æ‰¹æ¬¡å·
src/api/mes/wm/productsales/line/index.ts
@@ -15,6 +15,7 @@
    specification?: string; // è§„格型号
    unitMeasureName?: string; // å•位
    quantity?: number; // å‡ºåº“数量
    boxCount?: number; // ç®±æ•°
    pickedQuantity?: number; // å·²æ‹£è´§æ•°é‡
    batchId?: number; // æ‰¹æ¬¡ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
src/api/mes/wm/productsales/statistics/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesWmProductSalesStatisticsApi {
  /** MES å‘货统计 */
  export interface Statistics {
    id?: string; // å‰ç«¯è¡Œä¸»é”®ï¼ˆå®¢æˆ·+销售订单号+产品)
    clientId?: number; // å®¢æˆ·ç¼–号
    clientName?: string; // å®¢æˆ·åç§°
    salesOrderCode?: string; // é”€å”®è®¢å•编号
    itemId?: number; // äº§å“ç¼–号
    itemCode?: string; // äº§å“ç¼–码
    itemName?: string; // äº§å“åç§°
    specification?: string; // è§„格型号
    unitMeasureName?: string; // å•位
    totalQuantity?: number; // å‡ºåº“总数量
    totalBoxCount?: number; // æ€»ç®±æ•°
    shipmentCount?: number; // å‘货次数
    lastShipmentDate?: Date; // æœ€åŽå‘è´§æ—¶é—´
  }
  /** å‘货统计分页查询参数 */
  export interface PageParams extends PageParam {
    clientId?: number;
    salesOrderCode?: string;
    itemId?: number;
    shipmentDate?: string[];
  }
}
/** æŸ¥è¯¢å‘货统计分页 */
export function getProductSalesStatisticsPage(
  params: MesWmProductSalesStatisticsApi.PageParams,
) {
  return requestClient.get<
    PageResult<MesWmProductSalesStatisticsApi.Statistics>
  >('/mes/wm/product-sales/statistics-page', { params });
}
src/packages/constants/src/biz-mes-enum.ts
@@ -170,6 +170,7 @@
  QC_IQC_VENDOR_BATCH_CODE: 'QC_IQC_VENDOR_BATCH_CODE',
  QC_IPQC_CODE: 'QC_IPQC_CODE',
  QC_OQC_CODE: 'QC_OQC_CODE',
  QC_RETENTION_SAMPLE_CODE: 'QC_RETENTION_SAMPLE_CODE',
  QC_RQC_CODE: 'QC_RQC_CODE',
  QC_TEMPLATE_CODE: 'QC_TEMPLATE_CODE',
  TM_TOOL_TYPE_CODE: 'TM_TOOL_TYPE_CODE',
src/packages/constants/src/dict-enum.ts
@@ -242,6 +242,7 @@
  MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES ç”Ÿäº§å·¥å•状态
  ORDER_PRODUCTION_STATUS: 'order_production_status', // ç”Ÿäº§è®¢å•状态
  MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES å·¥å•类型
  MES_PRO_WORK_ORDER_PACKING_STATUS: 'mes_pro_work_order_packing_status', // MES å·¥å•装箱状态
  MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES å·¥å•来源类型
  MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES ç”Ÿäº§ä»»åŠ¡çŠ¶æ€
  MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES ç”Ÿäº§æŠ¥å·¥çŠ¶æ€
src/views/mes/md/item/data.ts
@@ -118,6 +118,18 @@
      rules: z.number().default(CommonStatusEnum.DISABLE),
    },
    {
      fieldName: 'boxQuantity',
      label: '每箱数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0.01,
        placeholder: '请输入每箱数量',
        precision: 2,
      },
      help: '仅成品需要填写,装箱与发货时用于自动计算箱数。',
    },
    {
      fieldName: 'highValue',
      label: '高值物料',
      component: 'Switch',
@@ -288,6 +300,12 @@
      align: 'center',
    },
    {
      field: 'boxQuantity',
      title: '每箱数量',
      width: 100,
      align: 'center',
    },
    {
      field: 'itemTypeName',
      title: '物料分类',
      minWidth: 140,
src/views/mes/md/item/type/data.ts
@@ -109,6 +109,17 @@
      rules: z.string().default(MesItemOrProductEnum.ITEM.value),
    },
    {
      fieldName: 'finishedFlag',
      label: '是否成品分类',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '设为成品分类后,该分类下的物料才允许装箱。',
    },
    {
      fieldName: 'sort',
      label: '显示排序',
      component: 'InputNumber',
@@ -194,6 +205,16 @@
      },
    },
    {
      field: 'finishedFlag',
      title: '成品分类',
      width: 110,
      align: 'center',
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'sort',
      title: '排序',
      width: 100,
src/views/mes/md/vendor/data.ts
@@ -25,24 +25,6 @@
        placeholder: '请输入供应商名称',
      },
    },
    {
      fieldName: 'nickname',
      label: '供应商简称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入供应商简称',
      },
    },
    {
      fieldName: 'englishName',
      label: '英文名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入英文名称',
      },
    },
  ];
}
@@ -68,18 +50,9 @@
      width: 120,
    },
    {
      field: 'level',
      title: '供应商等级',
      field: 'contact1Name',
      title: '联系人',
      width: 120,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_VENDOR_LEVEL },
      },
    },
    {
      field: 'score',
      title: '供应商评分',
      width: 110,
    },
    {
      field: 'telephone',
src/views/mes/pro/task/components/gantt-chart.vue
@@ -157,6 +157,26 @@
  ganttInited.value = true;
}
/** è½¬ä¸ºåˆæ³•日期,无法解析时返回 null */
function toValidDate(value: any): Date | null {
  if (value === null || value === undefined || value === '') {
    return null;
  }
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}
/** è¡¥å…¨ç”˜ç‰¹å›¾æ—¶é•¿ï¼ˆå°æ—¶ï¼‰ï¼šç¼ºå¤±æˆ–非法时按起止时间计算 */
function resolveDuration(item: any): number {
  const duration = Number(item.duration);
  if (Number.isFinite(duration) && duration > 0) {
    return duration;
  }
  const hours =
    (item.end_date.getTime() - item.start_date.getTime()) / 3_600_000;
  return Math.max(hours, 1);
}
/** åŠ è½½æ•°æ®åˆ°ç”˜ç‰¹å›¾ */
function loadData(tasks: any[]) {
  if (!ganttInited.value) {
@@ -168,22 +188,93 @@
    [BarcodeBizTypeEnum.WORKORDER]: 'project',
    [BarcodeBizTypeEnum.TASK]: 'task',
  };
  gantt.parse({
    data: tasks.map((item: any) => ({
      ...item,
      type: typeMap[item.type] || item.type,
      start_date: item.startDate ? new Date(item.startDate) : undefined,
      end_date: item.endDate ? new Date(item.endDate) : undefined,
    })),
    links: [],
  // ä»»åŠ¡è¡Œï¼šèµ·æ­¢æ—¶é—´ä¸å®Œæ•´çš„ä¸è¿›ç”˜ç‰¹å›¾ï¼Œé¿å…ç¬¬ä¸‰æ–¹åº“è§£æžç©ºæ—¥æœŸæ—¶æŠ¥é”™
  const taskRows = tasks
    .filter((item: any) => item.type !== BarcodeBizTypeEnum.WORKORDER)
    .map((item: any) => {
      const startDate = toValidDate(item.startDate);
      const endDate = toValidDate(item.endDate);
      if (!startDate || !endDate) {
        return null;
      }
      return {
        ...item,
        type: typeMap[item.type] || item.type,
        start_date: startDate,
        end_date: endDate,
      };
    })
    .filter(Boolean) as any[];
  // å·¥å•行:没有自身时间时用子任务的最早开始/最晚结束补全;没有任何排产任务的工单不显示
  const tasksByParent = new Map<string, any[]>();
  taskRows.forEach((row: any) => {
    if (!row.parent) {
      return;
    }
    const children = tasksByParent.get(row.parent) || [];
    children.push(row);
    tasksByParent.set(row.parent, children);
  });
  const projectRows = tasks
    .filter((item: any) => item.type === BarcodeBizTypeEnum.WORKORDER)
    .map((item: any) => {
      const children = tasksByParent.get(item.id) || [];
      let startDate = toValidDate(item.startDate);
      let endDate = toValidDate(item.endDate);
      if ((!startDate || !endDate) && children.length > 0) {
        startDate = new Date(
          Math.min(...children.map((child) => child.start_date.getTime())),
        );
        endDate = new Date(
          Math.max(...children.map((child) => child.end_date.getTime())),
        );
      }
      if (!startDate || !endDate) {
        return null;
      }
      return {
        ...item,
        type: 'project',
        start_date: startDate,
        end_date: endDate,
      };
    })
    .filter(Boolean) as any[];
  const projectIds = new Set(projectRows.map((row: any) => row.id));
  taskRows.forEach((row: any) => {
    if (row.parent && !projectIds.has(row.parent)) {
      delete row.parent;
    }
  });
  const data = [...projectRows, ...taskRows].map((row: any) => ({
    ...row,
    duration: resolveDuration(row),
  }));
  if (data.length === 0) {
    return;
  }
  try {
    gantt.parse({ data, links: [] });
  } catch (error) {
    // ç¬¬ä¸‰æ–¹åº“渲染异常只影响甘特图自身,不能让整个页面路由切换失败
    console.error('甘特图数据解析失败,已跳过本次渲染', error);
    try {
      gantt.clearAll();
    } catch {
      // å¿½ç•¥æ¸…理失败
    }
  }
}
watch(
  () => props.tasks,
  (val) => {
    if (val?.length && ganttInited.value) {
      loadData(val);
    if (ganttInited.value) {
      loadData(val || []);
    }
  },
  { deep: true },
@@ -197,8 +288,15 @@
});
onBeforeUnmount(() => {
  if (ganttInited.value) {
  if (!ganttInited.value) {
    return;
  }
  try {
    gantt.clearAll();
  } catch {
    // ç¬¬ä¸‰æ–¹ç”˜ç‰¹å›¾æ¸…理异常不应影响路由切换
  } finally {
    ganttInited.value = false;
  }
});
src/views/mes/pro/task/modules/schedule-form.vue
@@ -25,6 +25,9 @@
const currentRouteId = ref(0); // å½“前工艺路线编号
const isReadonly = computed(() => formType.value === 'detail'); // è¯¦æƒ…态只读
const canFinish = computed(
  () => (workOrder.value?.quantityProduced ?? 0) > 0,
); // æœ‰äº§å‡ºæ‰å…è®¸å®Œæˆ
const getTitle = computed(() =>
  formType.value === 'detail' ? '订单详情' : '生产排产',
);
@@ -156,12 +159,18 @@
      <div class="flex flex-auto items-center gap-2">
        <template v-if="formType === 'schedule'">
          <Popconfirm
            v-if="routeProcessList.length > 0"
            v-if="routeProcessList.length > 0 && canFinish"
            title="确认要完成该订单吗?完成后订单下所有任务将标记为已完成。"
            @confirm="handleFinish"
          >
            <Button type="primary">完成</Button>
          </Popconfirm>
          <span
            v-else-if="routeProcessList.length > 0"
            class="text-gray-400 text-sm"
          >
            å½“前订单已生产数量为 0,请先完成报工后再完成订单
          </span>
          <span v-else class="text-gray-400 text-sm">
            å½“前订单未配置工序,请先在工单中维护工序信息后再完成
          </span>
src/views/mes/pro/workorder/components/select-dialog.vue
@@ -19,11 +19,13 @@
const props = withDefaults(
  defineProps<{
    excludeStatuses?: number[];
    status?: number;
    type?: number;
    checkMethod?: (row: MesProWorkOrderApi.WorkOrder) => boolean;
  }>(),
  {
    excludeStatuses: undefined,
    status: undefined,
    type: undefined,
    checkMethod: undefined,
@@ -151,6 +153,9 @@
            ...formValues,
            ...(props.status === undefined ? {} : { status: props.status }),
            ...(props.type === undefined ? {} : { type: props.type }),
            ...(props.excludeStatuses === undefined
              ? {}
              : { excludeStatuses: props.excludeStatuses }),
          });
        },
      },
src/views/mes/pro/workorder/components/select.vue
@@ -17,6 +17,7 @@
  defineProps<{
    allowClear?: boolean;
    disabled?: boolean;
    excludeStatuses?: number[];
    modelValue?: number;
    placeholder?: string;
    status?: number;
@@ -26,6 +27,7 @@
  {
    allowClear: true,
    disabled: false,
    excludeStatuses: undefined,
    modelValue: undefined,
    placeholder: '请选择订单',
    status: undefined,
@@ -142,6 +144,7 @@
    ref="dialogRef"
    :status="status"
    :type="type"
    :exclude-statuses="excludeStatuses"
    :check-method="checkMethod"
    @selected="handleSelected"
  />
src/views/mes/pro/workorder/data.ts
@@ -374,6 +374,19 @@
      },
    },
    {
      fieldName: 'packingStatus',
      label: '装箱状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(
          DICT_TYPE.MES_PRO_WORK_ORDER_PACKING_STATUS,
          'number',
        ),
        placeholder: '请选择装箱状态',
      },
    },
    {
      fieldName: 'requestDate',
      label: '需求日期',
      component: 'RangePicker',
@@ -455,6 +468,25 @@
      width: 100,
    },
    {
      field: 'packedQuantity',
      title: '已装箱数量',
      width: 110,
    },
    {
      field: 'packedBoxCount',
      title: '已装箱箱数',
      width: 110,
    },
    {
      field: 'packingStatus',
      title: '装箱状态',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_PACKING_STATUS },
      },
    },
    {
      field: 'clientName',
      title: '客户名称',
      width: 120,
src/views/mes/qc/retentionsample/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,371 @@
import type { PageParam } from '@vben/request';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesQcRetentionSampleApi } from '#/api/mes/qc/retention-sample';
import { markRaw } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
import { getSaleOrder, getSaleOrderPage } from '#/api/erp/sale/order';
import { getMpsPage } from '#/api/mes/pro/mps';
import { getSimpleUserList } from '#/api/system/user';
import { getDateRangePickerDefaultProps } from '#/utils';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** ç•™æ ·ä¿å­˜æ—¶é•¿ï¼ˆæœˆï¼‰ï¼Œä¸ŽåŽç«¯é»˜è®¤å€¼ä¿æŒä¸€è‡´ */
const RETENTION_MONTHS = 6;
/** æ ¡éªŒé”€å”®è®¢å•是否真实存在,返回存在的订单编号 */
async function resolveExistingSaleOrderId(id: number) {
  const saleOrder = await getSaleOrder(id);
  return saleOrder?.id;
}
/** é€‰æ‹©å·¥å•后带出产品、批次与销售订单 */
async function handleWorkOrderChange(
  item: MesProWorkOrderApi.WorkOrder | undefined,
  formApi?: VbenFormApi,
) {
  if (!formApi) {
    return;
  }
  await formApi.setFieldValue('productName', item?.productName ?? undefined);
  await formApi.setFieldValue('batchCode', item?.batchCode ?? undefined);
  let saleOrderId: number | undefined;
  // 1. å®¢æˆ·è®¢å•来源工单:按来源单号自动匹配销售订单
  if (item?.orderSourceType === 1 && item.orderSourceCode) {
    const params: PageParam & { no?: string } = {
      no: item.orderSourceCode,
      pageNo: 1,
      pageSize: 1,
    };
    const page = await getSaleOrderPage(params);
    saleOrderId = page.list?.[0]?.id;
  }
  // 2. MPS ä¸‹å‘工单:以 MPS è®°å½•的销售订单为准(合并下发可能对应多张)
  if (item?.id) {
    try {
      const page = await getMpsPage({
        pageNo: 1,
        pageSize: 20,
        workOrderId: item.id,
      });
      const mpsSaleOrders = new Map<number, string>();
      for (const mps of page.list ?? []) {
        if (mps.saleOrderId) {
          mpsSaleOrders.set(
            mps.saleOrderId,
            mps.saleOrderNo || `ID=${mps.saleOrderId}`,
          );
        }
      }
      if (mpsSaleOrders.size === 1) {
        const firstMpsSaleOrder = [...mpsSaleOrders.entries()][0];
        if (firstMpsSaleOrder) {
          const [mpsSaleOrderId, mpsSaleOrderNo] = firstMpsSaleOrder;
          const existingId = await resolveExistingSaleOrderId(mpsSaleOrderId);
          if (existingId) {
            saleOrderId = existingId;
          } else {
            saleOrderId = undefined;
            message.warning(
              `该工单由 MPS ä¸‹å‘,但 MPS è®°å½•的销售订单【${mpsSaleOrderNo}】(ID=${mpsSaleOrderId})在系统中不存在,请先修正 MPS æ•°æ®åŽå†ç™»è®°ç•™æ ·`,
            );
          }
        }
      } else if (mpsSaleOrders.size > 1) {
        saleOrderId = undefined;
        message.warning(
          `该工单由 ${mpsSaleOrders.size} å¼ é”€å”®è®¢å•合并下发,请选择本次留样对应的销售订单`,
        );
      }
    } catch {
      // MPS æŸ¥è¯¢å¤±è´¥ï¼ˆå¦‚无查询权限)时不阻断工单选择,保存时由后端兜底校验
    }
  }
  await formApi.setFieldValue('saleOrderId', saleOrderId);
}
/** ç•™æ ·æ—¥æœŸå˜æ›´æ—¶åŒæ­¥åˆ°æœŸæ—¥æœŸ */
function syncExpiryDate(value: string, formApi?: VbenFormApi) {
  if (!formApi) {
    return;
  }
  void formApi.setFieldValue(
    'expiryDate',
    value ? dayjs(value).add(RETENTION_MONTHS, 'month').format('YYYY-MM-DD') : undefined,
  );
}
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(
  formType: 'create' | 'update',
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'workOrderId',
      label: '生产工单',
      component: markRaw(ProWorkOrderSelect),
      componentProps: {
        disabled: formType === 'update',
        excludeStatuses: [3],
        placeholder: '请选择生产工单',
        onChange: (item: MesProWorkOrderApi.WorkOrder | undefined) =>
          handleWorkOrderChange(item, formApi),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'productName',
      label: '产品名称',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '选择工单后自动带出',
      },
      dependencies: {
        triggerFields: ['workOrderId'],
        show: (values) => !!values.workOrderId,
      },
    },
    {
      fieldName: 'batchCode',
      label: '生产批次号',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '选择工单后自动带出',
      },
      dependencies: {
        triggerFields: ['workOrderId'],
        show: (values) => !!values.workOrderId,
      },
    },
    {
      fieldName: 'saleOrderId',
      label: '销售订单',
      component: markRaw(ErpSaleOrderSelect),
      componentProps: {
        placeholder: '库存备货工单可不选;客户订单工单将自动匹配',
      },
    },
    {
      fieldName: 'sampleQuantity',
      label: '留样数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 1,
        placeholder: '请输入留样数量',
        precision: 0,
      },
      rules: 'required',
    },
    {
      fieldName: 'sampleDate',
      label: '留样日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        format: 'YYYY-MM-DD',
        placeholder: '请选择留样日期',
        valueFormat: 'YYYY-MM-DD',
        onChange: (value: string) => syncExpiryDate(value, formApi),
      },
      rules: 'required',
    },
    {
      fieldName: 'expiryDate',
      label: '留样到期日期',
      component: 'DatePicker',
      componentProps: {
        class: '!w-full',
        disabled: true,
        format: 'YYYY-MM-DD',
        valueFormat: 'YYYY-MM-DD',
      },
      help: `按留样日期 + ${RETENTION_MONTHS} ä¸ªæœˆè‡ªåŠ¨è®¡ç®—`,
    },
    {
      fieldName: 'keeperUserId',
      label: '留样人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '不选则默认为当前登录用户',
        showSearch: true,
        optionFilterProp: 'label',
        valueField: 'id',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '留样编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入留样编号',
      },
    },
    {
      fieldName: 'workOrderCode',
      label: '生产工单编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入生产工单编号',
      },
    },
    {
      fieldName: 'saleOrderNo',
      label: '销售订单号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入销售订单号',
      },
    },
    {
      fieldName: 'productId',
      label: '产品',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择产品',
      },
    },
    {
      fieldName: 'batchCode',
      label: '生产批次号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入生产批次号',
      },
    },
    {
      fieldName: 'sampleDate',
      label: '留样日期',
      component: 'RangePicker',
      componentProps: {
        ...getDateRangePickerDefaultProps(),
      },
    },
    {
      fieldName: 'expiryDate',
      label: '到期日期',
      component: 'RangePicker',
      componentProps: {
        ...getDateRangePickerDefaultProps(),
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesQcRetentionSampleApi.RetentionSample>['columns'] {
  return [
    {
      field: 'code',
      title: '留样编号',
      width: 160,
    },
    {
      field: 'workOrderCode',
      title: '生产工单编号',
      width: 150,
    },
    {
      field: 'saleOrderNo',
      title: '销售订单号',
      width: 150,
    },
    {
      field: 'productCode',
      title: '产品编码',
      width: 130,
    },
    {
      field: 'productName',
      title: '产品名称',
      minWidth: 160,
    },
    {
      field: 'batchCode',
      title: '生产批次号',
      width: 140,
    },
    {
      field: 'sampleQuantity',
      title: '留样数量',
      width: 100,
    },
    {
      field: 'sampleDate',
      title: '留样日期',
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'expiryDate',
      title: '到期日期',
      width: 120,
      formatter: 'formatDate',
    },
    {
      title: '留样状态',
      width: 100,
      slots: { default: 'expiryStatus' },
    },
    {
      field: 'keeperNickname',
      title: '留样人',
      width: 100,
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/qc/retentionsample/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcRetentionSampleApi } from '#/api/mes/qc/retention-sample';
import { Page, useVbenModal } from '@vben/common-ui';
import { message, Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteRetentionSample,
  getRetentionSamplePage,
} from '#/api/mes/qc/retention-sample';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'RetentionSampleIndex' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»º */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑 */
function handleEdit(row: MesQcRetentionSampleApi.RetentionSample) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤ */
async function handleDelete(row: MesQcRetentionSampleApi.RetentionSample) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.code]),
    duration: 0,
  });
  try {
    await deleteRetentionSample(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ˜¯å¦å·²åˆ°æœŸ */
function isExpired(row: MesQcRetentionSampleApi.RetentionSample) {
  return row.expiryDate ? dayjs(row.expiryDate).isBefore(dayjs(), 'day') : false;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getRetentionSamplePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesQcRetentionSampleApi.RetentionSample>,
});
</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: ['mes:qc-retention-sample:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #expiryStatus="{ row }">
        <Tag v-if="isExpired(row)" color="red">已到期</Tag>
        <Tag v-else color="green">留样中</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:qc-retention-sample:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:qc-retention-sample:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/qc/retentionsample/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
<script lang="ts" setup>
import type { MesQcRetentionSampleApi } from '#/api/mes/qc/retention-sample';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import {
  createRetentionSample,
  getRetentionSample,
  updateRetentionSample,
} from '#/api/mes/qc/retention-sample';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
defineOptions({ name: 'RetentionSampleForm' });
const emit = defineEmits(['success']);
const formData = ref<MesQcRetentionSampleApi.RetentionSample>();
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: 110,
  },
  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 MesQcRetentionSampleApi.RetentionSample;
    try {
      await (formData.value?.id
        ? updateRetentionSample(data)
        : createRetentionSample(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;
    }
    formData.value = undefined;
    const data = modalApi.getData<MesQcRetentionSampleApi.RetentionSample>();
    const isEdit = !!data?.id;
    formApi.setState({ schema: useFormSchema(isEdit ? 'update' : 'create', formApi) });
    await formApi.resetForm();
    if (!isEdit) {
      await formApi.setValues({
        sampleDate: dayjs().format('YYYY-MM-DD'),
        expiryDate: dayjs().add(6, 'month').format('YYYY-MM-DD'),
        sampleQuantity: 2,
      });
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getRetentionSample(data!.id!);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/wls/packages/data.ts
@@ -13,6 +13,7 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getItem } from '#/api/mes/md/item';
import { getSimpleUserList } from '#/api/system/user';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
import { MdClientSelect } from '#/views/mes/md/client/components';
@@ -429,8 +430,36 @@
  ];
}
/** ç»„装尾箱预览文案:整除时显示每箱数量并标记满装 */
function buildTailPreview(quantity?: number, boxQuantity?: number): string {
  if (!quantity || quantity <= 0 || !boxQuantity || boxQuantity <= 0) {
    return '输入装箱数量后自动计算';
  }
  const boxCount = Math.ceil(quantity / boxQuantity - 1e-9);
  const tail = Number((quantity - (boxCount - 1) * boxQuantity).toFixed(2));
  const full = Math.abs(tail - boxQuantity) < 1e-6;
  return `预计装 ${boxCount} ç®±ï¼Œå°¾ç®± ${full ? boxQuantity : tail} ä»¶${
    full ? '(满装)' : ''
  }`;
}
/** åˆ·æ–°è£…箱预览(按表单中的装箱数量与每箱数量实时计算) */
export async function refreshTailPreview(formApi?: VbenFormApi) {
  if (!formApi) {
    return;
  }
  const values = (await formApi.getValues()) as {
    boxQuantity?: number;
    quantity?: number;
  };
  await formApi.setFieldValue(
    'tailPreview',
    buildTailPreview(values.quantity, values.boxQuantity),
  );
}
/** è£…箱明细的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
  return [
    {
      fieldName: 'workOrderId',
@@ -438,6 +467,16 @@
      component: markRaw(ProWorkOrderSelect),
      componentProps: {
        placeholder: '请选择生产订单',
        // é€‰æ‹©ç”Ÿäº§è®¢å•后,自动带出该订单的产品物料
        onChange: async (order?: { productId?: number }) => {
          await formApi?.setFieldValue('itemId', order?.productId);
          // å¸¦å‡ºäº§å“ç‰©æ–™çš„æ¯ç®±æ•°é‡ï¼Œç”¨äºŽè®¡ç®—尾箱
          const item = order?.productId
            ? await getItem(order.productId).catch(() => undefined)
            : undefined;
          await formApi?.setFieldValue('boxQuantity', item?.boxQuantity);
          await refreshTailPreview(formApi);
        },
      },
      rules: 'selectRequired',
    },
@@ -446,9 +485,19 @@
      label: '产品物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择产品物料',
        disabled: true,
        placeholder: '选择生产订单后自动带出',
      },
      rules: 'selectRequired',
      help: '自动取生产订单的产品,无需手动选择。',
    },
    {
      fieldName: 'boxQuantity',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'quantity',
@@ -459,8 +508,21 @@
        min: 0.01,
        placeholder: '请输入装箱数量',
        precision: 2,
        onChange: () => {
          refreshTailPreview(formApi);
        },
      },
      rules: 'required',
    },
    {
      fieldName: 'tailPreview',
      label: '装箱预览',
      component: 'Input',
      formItemClass: 'col-span-2',
      componentProps: {
        disabled: true,
        placeholder: '输入装箱数量后自动计算',
      },
    },
    {
      fieldName: 'expireDate',
@@ -514,6 +576,17 @@
      width: 100,
    },
    {
      field: 'boxCount',
      title: '箱数',
      width: 80,
    },
    {
      field: 'tailBoxQuantity',
      title: '尾箱数量',
      width: 110,
      slots: { default: 'tailBoxQuantity' },
    },
    {
      field: 'workOrderCode',
      title: '生产订单编号',
      minWidth: 140,
src/views/wls/packages/index.vue
@@ -137,7 +137,7 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:wm-package:update'],
              ifShow: row.status === MesWmPackageStatusEnum.PREPARE,
              disabled: row.status !== MesWmPackageStatusEnum.PREPARE,
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -146,7 +146,7 @@
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:wm-package:delete'],
              ifShow: row.status === MesWmPackageStatusEnum.PREPARE,
              disabled: row.status !== MesWmPackageStatusEnum.PREPARE,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, row),
src/views/wls/packages/modules/line-form.vue
@@ -8,6 +8,7 @@
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { getItem } from '#/api/mes/md/item';
import {
  createPackageLine,
  getPackageLine,
@@ -15,7 +16,7 @@
} from '#/api/mes/wm/packages/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
import { refreshTailPreview, useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmPackageLineApi.PackageLine>();
@@ -35,7 +36,7 @@
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: useLineFormSchema(),
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
@@ -48,8 +49,14 @@
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = (await formApi.getValues()) as MesWmPackageLineApi.PackageLine;
    const data = (await formApi.getValues()) as MesWmPackageLineApi.PackageLine & {
      boxQuantity?: number;
      tailPreview?: string;
    };
    data.packageId = packageId.value;
    // ä»…用于预览的字段不提交
    delete data.boxQuantity;
    delete data.tailPreview;
    try {
      await (formData.value?.id
        ? updatePackageLine({ ...data, id: formData.value.id })
@@ -70,6 +77,7 @@
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{ id?: number; packageId: number }>();
    packageId.value = data.packageId;
    formApi.setState({ schema: useLineFormSchema(formApi) });
    if (!data.id) {
      return;
    }
@@ -78,6 +86,14 @@
      formData.value = await getPackageLine(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
      // å¸¦å‡ºæ¯ç®±æ•°é‡å¹¶åˆ·æ–°å°¾ç®±é¢„览
      if (formData.value.itemId) {
        const item = await getItem(formData.value.itemId).catch(
          () => undefined,
        );
        await formApi.setFieldValue('boxQuantity', item?.boxQuantity);
      }
      await refreshTailPreview(formApi);
    } finally {
      modalApi.unlock();
    }
src/views/wls/packages/modules/package-line-list.vue
@@ -103,6 +103,12 @@
          ]"
        />
      </template>
      <template #tailBoxQuantity="{ row }">
        <span v-if="row.tailBoxQuantity !== undefined && row.tailBoxQuantity !== null">
          {{ row.tailBoxQuantity }}{{ row.tailBoxFull ? '(满装)' : '' }}
        </span>
        <span v-else>—</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
@@ -110,7 +116,7 @@
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              ifShow: editable,
              disabled: !editable,
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -118,7 +124,7 @@
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              ifShow: editable,
              disabled: !editable,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
                confirm: handleDelete.bind(null, row),
src/views/wls/packages/modules/sub-package-list.vue
@@ -112,7 +112,7 @@
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              ifShow: editable,
              disabled: !editable,
              popConfirm: {
                title: '确认将该装箱单从子箱列表中移除?',
                confirm: handleRemoveChild.bind(null, row),
src/views/wls/productsales/data.ts
@@ -433,6 +433,11 @@
      width: 100,
    },
    {
      field: 'boxCount',
      title: '箱数',
      width: 80,
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
src/views/wls/shipmentstatistics/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesStatisticsApi } from '#/api/mes/wm/productsales/statistics';
import { markRaw } from 'vue';
import { getDateRangePickerDefaultProps } from '#/utils';
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'clientId',
      label: '客户',
      component: markRaw(MdClientSelect),
      componentProps: {
        placeholder: '请选择客户',
      },
    },
    {
      fieldName: 'salesOrderCode',
      label: '销售订单编号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入销售订单编号',
      },
    },
    {
      fieldName: 'itemId',
      label: '产品',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择产品',
      },
    },
    {
      fieldName: 'shipmentDate',
      label: '发货日期',
      component: 'RangePicker',
      componentProps: {
        ...getDateRangePickerDefaultProps(),
        allowClear: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductSalesStatisticsApi.Statistics>['columns'] {
  return [
    {
      field: 'clientName',
      title: '客户名称',
      minWidth: 140,
    },
    {
      field: 'salesOrderCode',
      title: '销售订单编号',
      minWidth: 160,
    },
    {
      field: 'itemCode',
      title: '产品编码',
      minWidth: 130,
    },
    {
      field: 'itemName',
      title: '产品名称',
      minWidth: 160,
    },
    {
      field: 'specification',
      title: '规格型号',
      minWidth: 130,
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      width: 90,
      align: 'center',
    },
    {
      field: 'totalQuantity',
      title: '出库总数量',
      width: 120,
      align: 'right',
    },
    {
      field: 'totalBoxCount',
      title: '总箱数',
      width: 100,
      align: 'right',
    },
    {
      field: 'shipmentCount',
      title: '发货次数',
      width: 100,
      align: 'center',
    },
    {
      field: 'lastShipmentDate',
      title: '最后发货时间',
      width: 180,
      formatter: 'formatDate',
    },
  ];
}
src/views/wls/shipmentstatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesStatisticsApi } from '#/api/mes/wm/productsales/statistics';
import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSalesStatisticsPage } from '#/api/mes/wm/productsales/statistics';
import { useGridColumns, useGridFormSchema } from './data';
const [Grid] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          const result = await getProductSalesStatisticsPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
          result.list = result.list.map(row => ({
            ...row,
            id: `${row.clientId ?? ''}_${row.salesOrderCode ?? ''}_${row.itemId ?? ''}`,
          }));
          return result;
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesWmProductSalesStatisticsApi.Statistics>,
});
</script>
<template>
  <Page auto-content-height>
    <Grid table-title="发货统计" />
  </Page>
</template>