2 天以前 1c1af9b0fc10778ae5ac13cc68fd4030affbcfe1
feat(wm): 新增SN码管理和设备入库功能

- 添加MES设备入库单状态枚举和相关常量定义
- 实现SN码状态枚举和在库单件列表的搜索表单及字段配置
- 添加SN码入库登记和出库核销的表单设计
- 集成SN码单件详情的描述字段展示
- 更新盘点计划支持自动生成和周期配置
- 修改杂项出入库和供应商退货单的审批流程
- 移除原有的提交按钮改为审批流程处理
- 优化盘点任务的开始盘点和结果编辑功能
- 添加条码扫码解析和成图预览组件
- 扩展设备台账和条码相关的API接口
- 新增SN码的入库出库及分页查询功能
- 添加设备物料编号字段到设备管理中
已添加21个文件
已修改25个文件
4150 ■■■■■ 文件已修改
src/api/hrm/training/index.ts 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/work-hour-summary/index.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/dv/machinery/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/barcode/index.ts 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/equipmentreceipt/index.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/equipmentreceipt/line/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/miscissue/index.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/miscreceipt/index.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/returnvendor/index.ts 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/sn/index.ts 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/stocktaking/plan/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/stocktaking/task/index.ts 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table-action/icons.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/training/data.ts 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/training/index.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/training/modules/form.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/training/modules/participants.vue 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/workHourSummary/data.ts 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/workHourSummary/index.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/components/scan-modal.vue 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/components/process-select-modal.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/equipmentreceipt/data.ts 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/equipmentreceipt/index.vue 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/equipmentreceipt/modules/form.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/equipmentreceipt/modules/line-form.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/equipmentreceipt/modules/line-list.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscissue/index.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscissue/modules/form.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscreceipt/index.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/miscreceipt/modules/form.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/index.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/returnvendor/modules/form.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/data.ts 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/index.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/location-maps.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/modules/in-stock-modal.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/modules/out-stock-modal.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/modules/sn-detail-modal.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/plan/data.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/task/data.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/task/index.vue 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/task/modules/form.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/transaction/data.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/training/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmTrainingApi {
  /** åŸ¹è®­å‚训人员 */
  export interface Participant {
    id?: number;
    trainingId?: number;
    employeeId?: number;
    employeeNo?: string;
    employeeName?: string;
    attended?: boolean;
    score?: number;
    isPass?: boolean;
    certificateId?: number;
    remark?: string;
    createTime?: string;
  }
  /** åŸ¹è®­æ´»åЍ */
  export interface Training {
    id?: number;
    trainingNo?: string;
    name?: string;
    trainingType?: number;
    trainingTypeName?: string;
    trainer?: string;
    location?: string;
    hours?: number;
    startDate?: string;
    endDate?: string;
    issueCertificate?: boolean;
    certValidityMonths?: number;
    status?: number;
    statusName?: string;
    participantCount?: number;
    participants?: Participant[];
    remark?: string;
    createTime?: string;
  }
  /** åŸ¹è®­æ´»åŠ¨æŸ¥è¯¢å‚æ•° */
  export interface TrainingPageParam extends PageParam {
    trainingNo?: string;
    name?: string;
    trainingType?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢åŸ¹è®­æ´»åŠ¨åˆ†é¡µ */
export function getTrainingPage(params: HrmTrainingApi.TrainingPageParam) {
  return requestClient.get<PageResult<HrmTrainingApi.Training>>(
    '/hrm/training/page',
    { params },
  );
}
/** æŸ¥è¯¢åŸ¹è®­æ´»åŠ¨è¯¦æƒ…ï¼ˆå«å‚è®­äººå‘˜ï¼‰ */
export function getTraining(id: number) {
  return requestClient.get<HrmTrainingApi.Training>(`/hrm/training/get?id=${id}`);
}
/** åˆ›å»ºåŸ¹è®­æ´»åЍ */
export function createTraining(data: ReqTraining) {
  return requestClient.post('/hrm/training/create', data);
}
/** æ›´æ–°åŸ¹è®­æ´»åЍ */
export function updateTraining(data: ReqTraining) {
  return requestClient.put('/hrm/training/update', data);
}
/** åˆ é™¤åŸ¹è®­æ´»åЍ */
export function deleteTraining(ids: number[]) {
  return requestClient.delete('/hrm/training/delete', { params: { ids: ids.join(',') } });
}
/** å¼€å§‹åŸ¹è®­ï¼ˆè‰ç¨¿ â†’ å¾…开展) */
export function startTraining(id: number) {
  return requestClient.post('/hrm/training/start', null, { params: { id } });
}
/** å®ŒæˆåŸ¹è®­ï¼ˆè‰ç¨¿/待开展 â†’ å·²å®Œæˆï¼‰ */
export function completeTraining(id: number) {
  return requestClient.post('/hrm/training/complete', null, { params: { id } });
}
/** å–消培训(草稿/待开展 â†’ å·²å–消) */
export function cancelTraining(id: number) {
  return requestClient.post('/hrm/training/cancel', null, { params: { id } });
}
/** æ‰¹é‡æ·»åŠ å‚è®­å‘˜å·¥ */
export function addTrainingParticipants(trainingId: number, employeeIds: number[]) {
  return requestClient.post('/hrm/training-participant/add', { trainingId, employeeIds });
}
/** æ›´æ–°å‚训记录 */
export function updateTrainingParticipant(data: ReqParticipant) {
  return requestClient.put('/hrm/training-participant/update', data);
}
/** åˆ é™¤å‚训记录 */
export function deleteTrainingParticipants(ids: number[]) {
  return requestClient.delete('/hrm/training-participant/delete', {
    params: { ids: ids.join(',') },
  });
}
/** èŽ·å–æŸæ¬¡åŸ¹è®­çš„å‚è®­äººå‘˜åˆ—è¡¨ */
export function getTrainingParticipants(trainingId: number) {
  return requestClient.get<HrmTrainingApi.Participant[]>(
    `/hrm/training-participant/list?trainingId=${trainingId}`,
  );
}
/** åŸ¹è®­æ´»åŠ¨æ–°å¢ž/更新请求 */
export interface ReqTraining {
  id?: number;
  name?: string;
  trainingType?: number;
  trainer?: string;
  location?: string;
  hours?: number;
  startDate?: string;
  endDate?: string;
  issueCertificate?: boolean;
  certValidityMonths?: number;
  remark?: string;
}
/** å‚训记录更新请求 */
export interface ReqParticipant {
  id?: number;
  attended?: boolean;
  score?: number;
  isPass?: boolean;
  remark?: string;
}
src/api/hrm/work-hour-summary/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmWorkHourSummaryApi {
  /** æœˆåº¦å·¥æ—¶æ±‡æ€» */
  export interface WorkHourSummary {
    id?: number;
    employeeId?: number; // å‘˜å·¥ID
    employeeNo?: string; // å‘˜å·¥ç¼–号
    employeeName?: string; // å‘˜å·¥å§“名
    deptId?: number; // éƒ¨é—¨ID
    deptName?: string; // éƒ¨é—¨åç§°
    period?: string; // æ ¸ç®—周期(yyyy-MM)
    workDays?: number; // å‡ºå‹¤å¤©æ•°
    workHours?: number; // æ­£å¸¸å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
    overtimeHours?: number; // åŠ ç­å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
    remark?: string; // å¤‡æ³¨
    createTime?: string;
  }
}
/** ç”Ÿæˆæœˆåº¦å·¥æ—¶æ±‡æ€» */
export function generateWorkHourSummary(period: string) {
  return requestClient.post('/hrm/work-hour-summary/generate-monthly', null, {
    params: { period },
  });
}
/** èŽ·å–æœˆåº¦å·¥æ—¶æ±‡æ€»åˆ†é¡µ */
export function getWorkHourSummaryPage(params: PageParam) {
  return requestClient.get<PageResult<HrmWorkHourSummaryApi.WorkHourSummary>>(
    '/hrm/work-hour-summary/page',
    { params },
  );
}
src/api/mes/dv/machinery/index.ts
@@ -13,6 +13,7 @@
    specification?: string; // è§„格型号
    machineryTypeId?: number; // è®¾å¤‡ç±»åž‹ç¼–号
    machineryTypeName?: string; // è®¾å¤‡ç±»åž‹åç§°
    itemId?: number; // è®¾å¤‡ç‰©æ–™ç¼–号(MDM ç‰©æ–™ï¼‰
    workshopId?: number; // æ‰€å±žè½¦é—´ç¼–号
    workshopName?: string; // æ‰€å±žè½¦é—´åç§°
    status?: number; // è®¾å¤‡çŠ¶æ€
src/api/mes/wm/barcode/index.ts
@@ -62,9 +62,32 @@
  return requestClient.download('/mes/wm/barcode/export-excel', { params });
}
/** ç”Ÿæˆæ¡ç å†…容 */
export function generateBarcodeContent(bizType: number, bizCode: string) {
/** ç”Ÿæˆæ¡ç å†…容(支持多占位符组合,可传入业务名称) */
export function generateBarcodeContent(
  bizType: number,
  bizCode: string,
  bizName?: string,
) {
  return requestClient.get<string>('/mes/wm/barcode/generate-content', {
    params: { bizCode, bizType },
    params: { bizCode, bizName, bizType },
  });
}
/** ç”Ÿæˆæ¡ç /二维码图片(服务端 zxing æˆå›¾ï¼Œè¿”回 PNG Base64,不落库) */
export function renderBarcodeImage(
  content: string,
  format?: number,
  width?: number,
  height?: number,
) {
  return requestClient.post<string>('/mes/wm/barcode/render', null, {
    params: { content, format, height, width },
  });
}
/** è§£æžæ¡ç å†…容(优先精确命中,其次按启用配置模板反解,返回业务字段) */
export function parseBarcodeContent(content: string) {
  return requestClient.post<Record<string, any>>('/mes/wm/barcode/parse', null, {
    params: { content },
  });
}
src/api/mes/wm/equipmentreceipt/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
import type { MesWmEquipmentReceiptLineApi } from './line';
export namespace MesWmEquipmentReceiptApi {
  /** MES è®¾å¤‡å…¥åº“单 */
  export interface EquipmentReceipt {
    id?: number; // ç¼–号
    code?: string; // å•据编码
    name?: string; // å•据名称
    receiptDate?: number; // å…¥åº“日期
    source?: string; // å…¥åº“来源
    status?: number; // å•据状态
    processInstanceId?: string; // å®¡æ‰¹æµç¨‹å®žä¾‹ç¼–号
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
    lines?: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine[]; // æ˜Žç»†è¡Œ
  }
}
/** æŸ¥è¯¢è®¾å¤‡å…¥åº“单分页 */
export function getEquipmentReceiptPage(params: PageParam) {
  return requestClient.get<
    PageResult<MesWmEquipmentReceiptApi.EquipmentReceipt>
  >('/mes/wm/equipment-receipt/page', { params });
}
/** æŸ¥è¯¢è®¾å¤‡å…¥åº“单详情 */
export function getEquipmentReceipt(id: number) {
  return requestClient.get<MesWmEquipmentReceiptApi.EquipmentReceipt>(
    `/mes/wm/equipment-receipt/get?id=${id}`,
  );
}
/** æ–°å¢žè®¾å¤‡å…¥åº“单 */
export function createEquipmentReceipt(
  data: MesWmEquipmentReceiptApi.EquipmentReceipt,
) {
  return requestClient.post<number>('/mes/wm/equipment-receipt/create', data);
}
/** ä¿®æ”¹è®¾å¤‡å…¥åº“单 */
export function updateEquipmentReceipt(
  data: MesWmEquipmentReceiptApi.EquipmentReceipt,
) {
  return requestClient.put('/mes/wm/equipment-receipt/update', data);
}
/** åˆ é™¤è®¾å¤‡å…¥åº“单 */
export function deleteEquipmentReceipt(id: number) {
  return requestClient.delete(`/mes/wm/equipment-receipt/delete?id=${id}`);
}
/** æŸ¥è¯¢å¯ç”¨çš„设备入库单审批流程定义 */
export function getEquipmentReceiptApproveProcessList() {
  return requestClient.get<{ id: string; name: string; key: string }[]>(
    '/mes/wm/equipment-receipt/approval-process-list',
  );
}
/** æäº¤è®¾å¤‡å…¥åº“单审批 */
export function submitEquipmentReceiptApproval(
  id: number,
  processDefinitionKey: string,
) {
  return requestClient.put(
    `/mes/wm/equipment-receipt/submit-approval?id=${id}&processDefinitionKey=${processDefinitionKey}`,
  );
}
/** æ‰§è¡Œè®¾å¤‡å…¥åº“ */
export function finishEquipmentReceipt(id: number) {
  return requestClient.put(`/mes/wm/equipment-receipt/finish?id=${id}`);
}
/** å–消设备入库单 */
export function cancelEquipmentReceipt(id: number) {
  return requestClient.put(`/mes/wm/equipment-receipt/cancel?id=${id}`);
}
src/api/mes/wm/equipmentreceipt/line/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import { requestClient } from '#/api/request';
export namespace MesWmEquipmentReceiptLineApi {
  /** MES è®¾å¤‡å…¥åº“单行 */
  export interface EquipmentReceiptLine {
    id?: number; // ç¼–号
    receiptId?: number; // å…¥åº“单编号
    machineryId?: number; // è®¾å¤‡å°è´¦ç¼–号
    machineryCode?: string; // è®¾å¤‡ç¼–码
    machineryName?: string; // è®¾å¤‡åç§°
    itemId?: number; // è®¾å¤‡ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    specification?: string; // è§„格型号
    unitMeasureName?: string; // è®¡é‡å•位名称
    quantity?: number; // å…¥åº“数量
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢è®¾å¤‡å…¥åº“单行列表 */
export function getEquipmentReceiptLineListByReceiptId(receiptId: number) {
  return requestClient.get<
    MesWmEquipmentReceiptLineApi.EquipmentReceiptLine[]
  >(`/mes/wm/equipment-receipt-line/list-by-receipt-id?receiptId=${receiptId}`);
}
/** æŸ¥è¯¢è®¾å¤‡å…¥åº“单行详情 */
export function getEquipmentReceiptLine(id: number) {
  return requestClient.get<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>(
    `/mes/wm/equipment-receipt-line/get?id=${id}`,
  );
}
/** æ–°å¢žè®¾å¤‡å…¥åº“单行 */
export function createEquipmentReceiptLine(
  data: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine,
) {
  return requestClient.post('/mes/wm/equipment-receipt-line/create', data);
}
/** ä¿®æ”¹è®¾å¤‡å…¥åº“单行 */
export function updateEquipmentReceiptLine(
  data: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine,
) {
  return requestClient.put('/mes/wm/equipment-receipt-line/update', data);
}
/** åˆ é™¤è®¾å¤‡å…¥åº“单行 */
export function deleteEquipmentReceiptLine(id: number) {
  return requestClient.delete(
    `/mes/wm/equipment-receipt-line/delete?id=${id}`,
  );
}
src/api/mes/wm/miscissue/index.ts
@@ -14,6 +14,7 @@
    sourceDocCode?: string; // æ¥æºå•据编码
    issueDate?: number; // å‡ºåº“日期
    status?: number; // å•据状态
    processInstanceId?: string; // å®¡æ‰¹æµç¨‹å®žä¾‹ç¼–号
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
@@ -49,9 +50,21 @@
  return requestClient.delete(`/mes/wm/misc-issue/delete?id=${id}`);
}
/** æäº¤æ‚项出库单 */
export function submitMiscIssue(id: number) {
  return requestClient.put(`/mes/wm/misc-issue/submit?id=${id}`);
/** æŸ¥è¯¢å¯ç”¨çš„æ‚项出库单审批流程定义 */
export function getMiscIssueApproveProcessList() {
  return requestClient.get<{ id: string; name: string; key: string }[]>(
    '/mes/wm/misc-issue/approval-process-list',
  );
}
/** æäº¤æ‚项出库单审批 */
export function submitMiscIssueApproval(
  id: number,
  processDefinitionKey: string,
) {
  return requestClient.put(
    `/mes/wm/misc-issue/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
  );
}
/** æ‰§è¡Œå‡ºåº“ */
src/api/mes/wm/miscreceipt/index.ts
@@ -14,6 +14,7 @@
    sourceDocCode?: string; // æ¥æºå•据编码
    receiptDate?: number; // å…¥åº“日期
    status?: number; // å•据状态
    processInstanceId?: string; // å®¡æ‰¹æµç¨‹å®žä¾‹ç¼–号
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
@@ -49,9 +50,21 @@
  return requestClient.delete(`/mes/wm/misc-receipt/delete?id=${id}`);
}
/** æäº¤æ‚项入库单 */
export function submitMiscReceipt(id: number) {
  return requestClient.put(`/mes/wm/misc-receipt/submit?id=${id}`);
/** æŸ¥è¯¢å¯ç”¨çš„æ‚项入库单审批流程定义 */
export function getMiscReceiptApproveProcessList() {
  return requestClient.get<{ id: string; name: string; key: string }[]>(
    '/mes/wm/misc-receipt/approval-process-list',
  );
}
/** æäº¤æ‚项入库单审批 */
export function submitMiscReceiptApproval(
  id: number,
  processDefinitionKey: string,
) {
  return requestClient.put(
    `/mes/wm/misc-receipt/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
  );
}
/** æ‰§è¡Œå…¥åº“ */
src/api/mes/wm/returnvendor/index.ts
@@ -19,6 +19,7 @@
    transportCode?: string; // è¿å•号
    transportTelephone?: string; // è”系电话
    status?: number; // å•据状态
    processInstanceId?: string; // å®¡æ‰¹æµç¨‹å®žä¾‹ç¼–号
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
@@ -54,9 +55,21 @@
  return requestClient.delete(`/mes/wm/return-vendor/delete?id=${id}`);
}
/** æäº¤ä¾›åº”商退货单 */
export function submitReturnVendor(id: number) {
  return requestClient.put(`/mes/wm/return-vendor/submit?id=${id}`);
/** æŸ¥è¯¢å¯ç”¨çš„供应商退货单审批流程定义 */
export function getReturnVendorApproveProcessList() {
  return requestClient.get<{ id: string; name: string; key: string }[]>(
    '/mes/wm/return-vendor/approval-process-list',
  );
}
/** æäº¤ä¾›åº”商退货单审批 */
export function submitReturnVendorApproval(
  id: number,
  processDefinitionKey: string,
) {
  return requestClient.put(
    `/mes/wm/return-vendor/submit?id=${id}&processDefinitionKey=${processDefinitionKey}`,
  );
}
/** æ‰§è¡Œæ‹£è´§ */
src/api/mes/wm/sn/index.ts
@@ -29,7 +29,60 @@
    unitName?: string; // å•位名称
    batchCode?: string; // æ‰¹æ¬¡å·
    workOrderId?: number; // ç”Ÿäº§è®¢å•编号
    status?: number; // çŠ¶æ€ï¼ˆ1-在库 2-已出库)
    warehouseId?: number; // åœ¨åº“仓库 ID
    locationId?: number; // åœ¨åº“库区 ID
    areaId?: number; // åœ¨åº“库位 ID
    createTime?: Date; // ç”Ÿæˆæ—¶é—´
  }
  /** MES SN ç å•件详情 */
  export interface SnDetail {
    id?: number; // ç¼–号
    code?: string; // SN ç 
    itemId?: number; // ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    status?: number; // çŠ¶æ€ï¼ˆ1-在库 2-已出库)
    warehouseId?: number; // åœ¨åº“仓库 ID
    locationId?: number; // åœ¨åº“库区 ID
    areaId?: number; // åœ¨åº“库位 ID
    lastTransactionType?: number; // æœ€è¿‘事务类型(1-入库 2-出库)
    lastTransactionTypeName?: string; // æœ€è¿‘事务类型名称
    lastTransactionQuantity?: number; // æœ€è¿‘事务变动数量
    lastTransactionBizType?: number; // æœ€è¿‘事务业务类型
    lastTransactionBizCode?: string; // æœ€è¿‘事务业务单号
    lastTransactionTime?: string; // æœ€è¿‘事务发生时间
  }
  /** SN ç å…¥åº“登记参数 */
  export interface SnInStock {
    code?: string; // SN ç 
    itemId?: number; // ç‰©æ–™ç¼–号
    warehouseId?: number; // ä»“库 ID
    locationId?: number; // åº“区 ID
    areaId?: number; // åº“位 ID
    batchCode?: string; // æ‰¹æ¬¡å·
    remark?: string; // å¤‡æ³¨
  }
  /** SN ç å‡ºåº“核销参数 */
  export interface SnOutStock {
    code?: string; // SN ç 
    itemId?: number; // ç‰©æ–™ç¼–号
    remark?: string; // å¤‡æ³¨
  }
  /** SN ç åœ¨åº“/状态分页查询参数 */
  export interface SnInStockPageParams extends PageParam {
    code?: string; // SN ç 
    itemId?: number; // ç‰©æ–™ç¼–号
    batchCode?: string; // æ‰¹æ¬¡å·
    warehouseId?: number; // ä»“库 ID
    locationId?: number; // åº“区 ID
    areaId?: number; // åº“位 ID
    status?: number; // çŠ¶æ€ï¼ˆ1-在库 2-已出库)
  }
  /** MES SN ç ç”Ÿæˆå‚æ•° */
@@ -77,6 +130,31 @@
  });
}
/** SN ç å…¥åº“登记 */
export function registerSnInStock(data: MesWmSnApi.SnInStock) {
  return requestClient.post<boolean>('/mes/wm/sn/in-stock', data);
}
/** SN ç å‡ºåº“核销 */
export function outboundSn(data: MesWmSnApi.SnOutStock) {
  return requestClient.post<boolean>('/mes/wm/sn/out-stock', data);
}
/** æŸ¥è¯¢ SN ç å•件详情 */
export function getSnDetail(code: string) {
  return requestClient.get<MesWmSnApi.SnDetail>('/mes/wm/sn/detail', {
    params: { code },
  });
}
/** æŸ¥è¯¢ SN ç åœ¨åº“/状态分页 */
export function getSnInStockPage(params: MesWmSnApi.SnInStockPageParams) {
  return requestClient.get<PageResult<MesWmSnApi.Sn>>(
    '/mes/wm/sn/in-stock-page',
    { params },
  );
}
/** å¯¼å‡º SN ç åˆ†ç»„ Excel */
export function exportSnGroupExcel(params: MesWmSnApi.PageParams) {
  return requestClient.download('/mes/wm/sn/group-export-excel', { params });
src/api/mes/wm/stocktaking/plan/index.ts
@@ -13,6 +13,9 @@
    endTime?: number; // ç»“束时间
    blindFlag?: boolean; // æ˜¯å¦ç›²ç›˜
    frozen?: boolean; // æ˜¯å¦å†»ç»“库存
    autoGenerate?: boolean; // æ˜¯å¦è‡ªåŠ¨ç”Ÿæˆç›˜ç‚¹ä»»åŠ¡
    generateCron?: string; // è‡ªåŠ¨ç”Ÿæˆå‘¨æœŸ(Cron)
    lastGenerateTime?: string; // ä¸Šæ¬¡ç”Ÿæˆä»»åŠ¡æ—¶é—´
    status?: number; // çŠ¶æ€
    remark?: string; // å¤‡æ³¨
    createTime?: string; // åˆ›å»ºæ—¶é—´
src/api/mes/wm/stocktaking/task/index.ts
@@ -20,6 +20,7 @@
    startTime?: number; // å¼€å§‹æ—¶é—´
    endTime?: number; // ç»“束时间
    status?: number; // å•据状态
    processInstanceId?: string; // å®¡æ‰¹æµç¨‹å®žä¾‹ç¼–号
    remark?: string; // å¤‡æ³¨
    createTime?: string; // åˆ›å»ºæ—¶é—´
  }
@@ -59,7 +60,7 @@
  return requestClient.delete(`/mes/wm/stocktaking-task/delete?id=${id}`);
}
/** æäº¤ç›˜ç‚¹ä»»åŠ¡ */
/** å¼€å§‹ç›˜ç‚¹ï¼ˆæäº¤ç›˜ç‚¹ä»»åŠ¡ï¼‰ */
export function submitStockTaking(id: number) {
  return requestClient.put('/mes/wm/stocktaking-task/submit', null, {
    params: { id },
@@ -73,9 +74,21 @@
  });
}
/** æ‰§è¡Œç›˜ç‚¹ä»»åŠ¡ */
export function finishStockTaking(id: number) {
  return requestClient.put('/mes/wm/stocktaking-task/finish', { id });
/** æŸ¥è¯¢å¯ç”¨çš„盘点结果审批流程定义 */
export function getStockTakingApproveProcessList() {
  return requestClient.get<{ id: string; name: string; key: string }[]>(
    '/mes/wm/stocktaking-task/approval-process-list',
  );
}
/** æäº¤ç›˜ç‚¹ç»“果审批 */
export function submitStockTakingApproval(
  id: number,
  processDefinitionKey: string,
) {
  return requestClient.put(
    `/mes/wm/stocktaking-task/submit-approval?id=${id}&processDefinitionKey=${processDefinitionKey}`,
  );
}
/** å¯¼å‡ºç›˜ç‚¹ä»»åŠ¡ */
src/components/table-action/icons.ts
@@ -14,4 +14,5 @@
  BOOK: 'lucide:book',
  AUDIT: 'lucide:file-check',
  CALC: 'lucide:calculator',
  SCAN: 'lucide:scan-line',
};
src/packages/constants/src/biz-mes-enum.ts
@@ -187,6 +187,7 @@
  PRODUCTRECPT_CODE: 'PRODUCTRECPT_CODE',
  WM_MISC_ISSUE_CODE: 'WM_MISC_ISSUE_CODE',
  WM_MISC_RECEIPT_CODE: 'WM_MISC_RECEIPT_CODE',
  WM_EQUIP_RECEIPT_CODE: 'WM_EQUIP_RECEIPT_CODE',
  WM_OUTSOURCE_ISSUE_CODE: 'WM_OUTSOURCE_ISSUE_CODE',
  WM_OUTSOURCE_RECEIPT_CODE: 'WM_OUTSOURCE_RECEIPT_CODE',
  WM_PACKAGE_CODE: 'WM_PACKAGE_CODE',
@@ -316,6 +317,7 @@
/** MES æ‚项出库单状态枚举 */
export const MesWmMiscIssueStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
  APPROVING: MesOrderStatusConstants.APPROVING,
  APPROVED: MesOrderStatusConstants.APPROVED,
  FINISHED: MesOrderStatusConstants.FINISHED,
  CANCELED: MesOrderStatusConstants.CANCELLED,
@@ -324,6 +326,7 @@
/** MES æ‚项入库单状态枚举 */
export const MesWmMiscReceiptStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
  APPROVING: MesOrderStatusConstants.APPROVING,
  APPROVED: MesOrderStatusConstants.APPROVED,
  FINISHED: MesOrderStatusConstants.FINISHED,
  CANCELED: MesOrderStatusConstants.CANCELLED,
@@ -631,3 +634,18 @@
  USER = 604,
  CLIENT = 605,
}
/** MES SN ç çŠ¶æ€æžšä¸¾ */
export enum MesWmSnStatusEnum {
  IN_STOCK = 1, // åœ¨åº“
  OUT_STOCK = 2, // å·²å‡ºåº“
}
/** MES è®¾å¤‡å…¥åº“单状态枚举 */
export const MesWmEquipmentReceiptStatusEnum = {
  PREPARE: MesOrderStatusConstants.DRAFT,
  APPROVING: MesOrderStatusConstants.APPROVING,
  APPROVED: MesOrderStatusConstants.APPROVED,
  FINISHED: MesOrderStatusConstants.FINISHED,
  CANCELED: MesOrderStatusConstants.CANCELLED,
} as const;
src/views/hrm/training/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTrainingApi } from '#/api/hrm/training';
/** åŸ¹è®­ç±»åž‹é€‰é¡¹ */
export const trainingTypeOptions = [
  { label: '安全培训', value: 1 },
  { label: '技能培训', value: 2 },
  { label: '管理培训', value: 3 },
  { label: '其他', value: 4 },
];
/** åŸ¹è®­çŠ¶æ€é€‰é¡¹ */
export const trainingStatusOptions = [
  { label: '草稿', value: 0 },
  { label: '待开展', value: 10 },
  { label: '已完成', value: 20 },
  { label: '已取消', value: 30 },
];
/** è¡¨å•类型 */
export type FormType = 'create' | 'edit' | 'detail';
/** è¡¨å•的配置项 */
export function useFormSchema(formType: FormType): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'name',
      label: '培训名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入培训名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'trainingType',
      label: '培训类型',
      component: 'Select',
      componentProps: {
        options: trainingTypeOptions,
        placeholder: '请选择培训类型',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'trainer',
      label: '讲师',
      component: 'Input',
      componentProps: {
        placeholder: '请输入讲师',
      },
    },
    {
      fieldName: 'location',
      label: '培训地点',
      component: 'Input',
      componentProps: {
        placeholder: '请输入培训地点',
      },
    },
    {
      fieldName: 'hours',
      label: '培训学时',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入培训学时',
        min: 0,
        precision: 1,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'startDate',
      label: '开始日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择开始日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'endDate',
      label: '结束日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '选择结束日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'issueCertificate',
      label: '自动发证',
      component: 'Select',
      componentProps: {
        options: [
          { label: '是', value: 1 },
          { label: '否', value: 0 },
        ],
        placeholder: '是否自动发证',
      },
    },
    {
      fieldName: 'certValidityMonths',
      label: '证书有效期(月)',
      component: 'InputNumber',
      componentProps: {
        placeholder: '自动发证时填写',
        min: 0,
        precision: 0,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 2 },
      },
      formItemClass: 'col-span-2',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'trainingNo',
      label: '培训编号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入培训编号',
      },
    },
    {
      fieldName: 'name',
      label: '培训名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入培训名称',
      },
    },
    {
      fieldName: 'trainingType',
      label: '培训类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: trainingTypeOptions,
        placeholder: '请选择培训类型',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: trainingStatusOptions,
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmTrainingApi.Training>['columns'] {
  return [
    {
      field: 'trainingNo',
      title: '培训编号',
      width: 140,
    },
    {
      field: 'name',
      title: '培训名称',
      minWidth: 180,
    },
    {
      field: 'trainingTypeName',
      title: '培训类型',
      width: 100,
    },
    {
      field: 'trainer',
      title: '讲师',
      width: 100,
    },
    {
      field: 'location',
      title: '培训地点',
      minWidth: 120,
    },
    {
      field: 'hours',
      title: '学时',
      width: 80,
    },
    {
      field: 'startDate',
      title: '开始日期',
      width: 105,
      formatter: 'formatDate',
    },
    {
      field: 'endDate',
      title: '结束日期',
      width: 105,
      formatter: 'formatDate',
    },
    {
      field: 'participantCount',
      title: '参训人数',
      width: 90,
    },
    {
      field: 'statusName',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      field: 'action',
      title: '操作',
      width: 240,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/training/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmTrainingApi } from '#/api/hrm/training';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelTraining,
  completeTraining,
  deleteTraining,
  getTrainingPage,
  startTraining,
} from '#/api/hrm/training';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import Participants from './modules/participants.vue';
/** HRM åŸ¹è®­ç®¡ç†åˆ—表 */
defineOptions({ name: 'HrmTraining' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ParticipantsModal, participantsModalApi] = useVbenModal({
  connectedComponent: Participants,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: HrmTrainingApi.Training) {
  formModalApi.setData({ formType: 'edit', id: row.id }).open();
}
function handleDetail(row: HrmTrainingApi.Training) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
function handleParticipants(row: HrmTrainingApi.Training) {
  participantsModalApi.setData({ trainingId: row.id }).open();
}
async function handleDelete(ids: number[]) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting'),
    duration: 0,
  });
  try {
    await deleteTraining(ids);
    message.success($t('ui.actionMessage.deleteSuccess'));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å¼€å§‹åŸ¹è®­ï¼šè‰ç¨¿ â†’ å¾…开展 */
function handleStart(row: HrmTrainingApi.Training) {
  Modal.confirm({
    title: '开始培训',
    content: `是否开始培训「${row.name}」?`,
    onOk: async () => {
      await startTraining(row.id!);
      message.success('开始培训成功');
      handleRefresh();
    },
  });
}
/** å®ŒæˆåŸ¹è®­ï¼šè‰ç¨¿/待开展 â†’ å·²å®Œæˆ */
function handleComplete(row: HrmTrainingApi.Training) {
  Modal.confirm({
    title: '完成培训',
    content: `是否完成培训「${row.name}」?`,
    onOk: async () => {
      await completeTraining(row.id!);
      message.success('完成培训成功');
      handleRefresh();
    },
  });
}
/** å–消培训:草稿/待开展 â†’ å·²å–消 */
function handleCancel(row: HrmTrainingApi.Training) {
  Modal.confirm({
    title: '取消培训',
    content: `是否取消培训「${row.name}」?`,
    onOk: async () => {
      await cancelTraining(row.id!);
      message.success('取消培训成功');
      handleRefresh();
    },
  });
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
  records,
}: {
  records: HrmTrainingApi.Training[];
}) {
  checkedIds.value = records.map((item) => item.id!);
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getTrainingPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmTrainingApi.Training>,
  gridEvents: {
    checkboxAll: handleRowCheckboxChange,
    checkboxChange: handleRowCheckboxChange,
  },
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ParticipantsModal @success="handleRefresh" />
    <Grid table-title="培训管理列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['培训']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:training:create'],
              onClick: handleCreate,
            },
            {
              label: '批量删除',
              type: 'primary',
              danger: true,
              disabled: isEmpty(checkedIds),
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:training:delete'],
              popConfirm: {
                title: '是否删除所选中数据?',
                confirm: handleDelete.bind(null, checkedIds),
              },
            },
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag v-if="row.status === 0" color="default">草稿</Tag>
        <Tag v-else-if="row.status === 10" color="processing">待开展</Tag>
        <Tag v-else-if="row.status === 20" color="success">已完成</Tag>
        <Tag v-else-if="row.status === 30" color="error">已取消</Tag>
        <Tag v-else color="default">-</Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.detail'),
              type: 'link',
              icon: ACTION_ICON.VIEW,
              auth: ['hrm:training:query'],
              onClick: handleDetail.bind(null, row),
            },
            {
              label: '编辑',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:training:update'],
              ifShow: () => row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '参训人员',
              type: 'link',
              icon: 'lucide:users',
              auth: ['hrm:training:update'],
              ifShow: () => row.status === 0 || row.status === 10 || row.status === 20,
              onClick: handleParticipants.bind(null, row),
            },
            {
              label: '开始',
              type: 'link',
              icon: 'lucide:play',
              auth: ['hrm:training:update'],
              ifShow: () => row.status === 0,
              onClick: handleStart.bind(null, row),
            },
            {
              label: '完成',
              type: 'link',
              icon: 'lucide:check',
              auth: ['hrm:training:update'],
              ifShow: () => row.status === 0 || row.status === 10,
              onClick: handleComplete.bind(null, row),
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
              icon: ACTION_ICON.CLOSE,
              auth: ['hrm:training:update'],
              ifShow: () => row.status === 0 || row.status === 10,
              popConfirm: {
                title: '是否取消培训?',
                confirm: handleCancel.bind(null, row),
              },
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:training:delete'],
              ifShow: () => row.status === 0,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, [row.id!]),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/training/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { HrmTrainingApi, ReqTraining } from '#/api/hrm/training';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createTraining, getTraining, updateTraining } from '#/api/hrm/training';
import { $t } from '#/locales';
import type { FormType } from '../data';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<HrmTrainingApi.Training>();
const formType = ref<FormType>('create');
const getTitle = computed(() => {
  if (formType.value === 'create') {
    return $t('ui.actionTitle.create', ['培训']);
  } else if (formType.value === 'edit') {
    return $t('ui.actionTitle.edit', ['培训']);
  }
  return '培训详情';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-3',
  layout: 'vertical',
  schema: useFormSchema(formType.value),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const values = await formApi.getValues<ReqTraining>();
    // è‡ªåŠ¨å‘è¯å­—æ®µï¼šè¡¨å•ç”¨ 0/1 ä¸‹æ‹‰ï¼Œæäº¤æ—¶è½¬å¸ƒå°”
    const data = {
      ...values,
      issueCertificate: Boolean(values.issueCertificate),
    } as ReqTraining;
    try {
      await (formType.value === 'create' ? createTraining(data) : updateTraining(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;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getTraining(data.id);
      await formApi.setValues({
        ...formData.value,
        // å¸ƒå°” â†’ 0/1 ä¸‹æ‹‰å€¼
        issueCertificate: formData.value.issueCertificate ? 1 : 0,
      });
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'">
    <Form class="mx-3" />
  </Modal>
</template>
src/views/hrm/training/modules/participants.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,278 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeApi } from '#/api/hrm/employee';
import type { HrmTrainingApi } from '#/api/hrm/training';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Modal as AntModal, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  addTrainingParticipants,
  deleteTrainingParticipants,
  getTrainingParticipants,
  updateTrainingParticipant,
} from '#/api/hrm/training';
import { $t } from '#/locales';
import { HrmEmployeeSelectDialog } from '#/views/hrm/employee/components';
defineOptions({ name: 'HrmTrainingParticipants' });
const emit = defineEmits(['success']);
const trainingId = ref<number>();
/** å‚训人员编辑表单 */
const participantFormData = ref<HrmTrainingApi.Participant>();
const editVisible = ref(false);
const editSaving = ref(false);
const [EditForm, editFormApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 120,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'vertical',
  schema: [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'attended',
      label: '是否参训',
      component: 'Select',
      componentProps: {
        options: [
          { label: '是', value: 1 },
          { label: '否', value: 0 },
        ],
        placeholder: '是否参训',
      },
    },
    {
      fieldName: 'score',
      label: '成绩',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入成绩',
        min: 0,
        precision: 1,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'isPass',
      label: '是否合格',
      component: 'Select',
      componentProps: {
        options: [
          { label: '是', value: 1 },
          { label: '否', value: 0 },
        ],
        placeholder: '是否合格',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        autoSize: { minRows: 1, maxRows: 2 },
      },
      formItemClass: 'col-span-2',
    },
  ],
  showDefaultActions: false,
});
/** æ‰“开编辑弹窗 */
function openEditForm(row: HrmTrainingApi.Participant) {
  participantFormData.value = row;
  editFormApi.setValues({
    id: row.id,
    attended: row.attended ? 1 : 0,
    score: row.score,
    isPass: row.isPass ? 1 : 0,
    remark: row.remark,
  });
  editVisible.value = true;
}
/** ä¿å­˜å‚训记录 */
async function handleEditSave() {
  const { valid } = await editFormApi.validate();
  if (!valid) {
    return;
  }
  editSaving.value = true;
  const values = await editFormApi.getValues<HrmTrainingApi.Participant>();
  try {
    await updateTrainingParticipant({
      id: values.id,
      attended: Boolean(values.attended),
      score: values.score,
      isPass: Boolean(values.isPass),
      remark: values.remark,
    } as HrmTrainingApi.Participant);
    message.success($t('ui.actionMessage.operationSuccess'));
    editVisible.value = false;
    await loadParticipants();
  } finally {
    editSaving.value = false;
  }
}
/** åˆ é™¤å‚训记录 */
async function handleDelete(row: HrmTrainingApi.Participant) {
  await deleteTrainingParticipants([row.id!]);
  message.success($t('ui.actionMessage.deleteSuccess'));
  await loadParticipants();
}
/** å‘˜å·¥é€‰æ‹©å¼¹çª—(多选) */
const employeeDialogRef = ref<InstanceType<typeof HrmEmployeeSelectDialog>>();
function openEmployeeSelect() {
  employeeDialogRef.value?.open([], { multiple: true });
}
async function handleEmployeesSelected(rows: HrmEmployeeApi.Employee[]) {
  const employeeIds = rows
    .map((item) => item.id)
    .filter((id): id is number => id !== undefined && id !== null);
  if (employeeIds.length === 0) {
    return;
  }
  try {
    await addTrainingParticipants(trainingId.value!, employeeIds);
    message.success('添加参训人员成功');
    await loadParticipants();
    emit('success');
  } catch (e) {
    // åŽç«¯å…œåº•提示已弹出
  }
}
async function loadParticipants() {
  if (!trainingId.value) {
    return;
  }
  const list = await getTrainingParticipants(trainingId.value);
  gridApi.grid.loadData(list);
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: [
      { type: 'seq', title: '序号', width: 60 },
      { field: 'employeeNo', title: '员工编号', width: 130 },
      { field: 'employeeName', title: '员工姓名', minWidth: 120 },
      {
        field: 'attended',
        title: '是否参训',
        width: 100,
        slots: { default: 'attended' },
      },
      {
        field: 'score',
        title: '成绩',
        width: 90,
      },
      {
        field: 'isPass',
        title: '是否合格',
        width: 100,
        slots: { default: 'isPass' },
      },
      { field: 'remark', title: '备注', minWidth: 120 },
      {
        field: 'action',
        title: '操作',
        width: 140,
        fixed: 'right',
        slots: { default: 'actions' },
      },
    ],
    height: 'auto',
    keepSource: true,
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: false,
      search: false,
    },
  } as VxeTableGridOptions<HrmTrainingApi.Participant>,
});
const [Modal, modalApi] = useVbenModal({
  onOpenChange(isOpen) {
    if (!isOpen) {
      trainingId.value = undefined;
      return;
    }
    const data = modalApi.getData<{ trainingId?: number }>();
    trainingId.value = data?.trainingId;
    if (trainingId.value) {
      loadParticipants();
    }
  },
});
</script>
<template>
  <Modal title="参训人员管理" class="w-[900px]" :show-confirm-button="false">
    <HrmEmployeeSelectDialog
      ref="employeeDialogRef"
      @selected="handleEmployeesSelected"
    />
    <Grid table-title="参训人员列表">
      <template #toolbar-tools>
        <a-button type="primary" @click="openEmployeeSelect">
          <i-ant-design-plus-outlined class="mr-1" />添加参训人员
        </a-button>
      </template>
      <template #attended="{ row }">
        <Tag v-if="row.attended" color="success">是</Tag>
        <Tag v-else color="default">否</Tag>
      </template>
      <template #isPass="{ row }">
        <Tag v-if="row.isPass" color="success">合格</Tag>
        <Tag v-else-if="row.attended" color="error">不合格</Tag>
        <Tag v-else color="default">-</Tag>
      </template>
      <template #actions="{ row }">
        <a-button type="link" size="small" @click="openEditForm(row)">编辑</a-button>
        <a-popconfirm
          :title="$t('ui.actionMessage.deleteConfirm', [row.employeeName ?? row.id])"
          @confirm="handleDelete(row)"
        >
          <a-button type="link" danger size="small">删除</a-button>
        </a-popconfirm>
      </template>
    </Grid>
    <AntModal
      v-model:open="editVisible"
      title="编辑参训记录"
      width="480px"
      :confirm-loading="editSaving"
      @ok="handleEditSave"
    >
      <EditForm class="pt-4" />
    </AntModal>
  </Modal>
</template>
src/views/hrm/workHourSummary/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmWorkHourSummaryApi } from '#/api/hrm/work-hour-summary';
import { getDeptList } from '#/api/system/dept';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { handleTree } from '#/packages/utils/src';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'period',
      label: '核算周期',
      component: 'DatePicker',
      componentProps: {
        picker: 'month',
        allowClear: true,
        format: 'YYYY-MM',
        valueFormat: 'YYYY-MM',
        style: { width: '100%' },
        placeholder: '请选择核算周期',
      },
    },
    {
      fieldName: 'employeeId',
      label: '员工',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择员工',
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmWorkHourSummaryApi.WorkHourSummary>['columns'] {
  return [
    {
      field: 'period',
      title: '核算周期',
      width: 100,
    },
    {
      field: 'employeeNo',
      title: '员工编号',
      width: 130,
    },
    {
      field: 'employeeName',
      title: '员工姓名',
      minWidth: 110,
    },
    {
      field: 'deptName',
      title: '部门',
      minWidth: 120,
    },
    {
      field: 'workDays',
      title: '出勤天数',
      width: 90,
    },
    {
      field: 'workHours',
      title: '正常工时(h)',
      width: 110,
    },
    {
      field: 'overtimeHours',
      title: '加班工时(h)',
      width: 110,
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 120,
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 100,
      formatter: 'formatDate',
    },
  ];
}
src/views/hrm/workHourSummary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmWorkHourSummaryApi } from '#/api/hrm/work-hour-summary';
import { ref } from 'vue';
import { Page } from '#/packages/effects/common-ui/src';
import { message, DatePicker, Form, FormItem, Modal } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  generateWorkHourSummary,
  getWorkHourSummaryPage,
} from '#/api/hrm/work-hour-summary';
import { useGridColumns, useGridFormSchema } from './data';
/** HRM æœˆåº¦å·¥æ—¶æ±‡æ€» */
defineOptions({ name: 'HrmWorkHourSummary' });
// ç”Ÿæˆæœˆåº¦æ±‡æ€»å¼¹çª—
const generateVisible = ref(false);
const generatePeriod = ref<string>();
const generateLoading = ref(false);
/** æ‰“开生成弹窗,带出搜索条件中的核算周期 */
function openGenerateModal() {
  void gridApi.formApi.getValues().then((values) => {
    generatePeriod.value = values.period;
  });
  generateVisible.value = true;
}
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰§è¡Œç”Ÿæˆ */
async function handleGenerate() {
  if (!generatePeriod.value) {
    message.warning('请选择核算周期');
    return;
  }
  generateLoading.value = true;
  try {
    await generateWorkHourSummary(generatePeriod.value);
    message.success('月度工时汇总生成成功');
    generateVisible.value = false;
    handleRefresh();
  } finally {
    generateLoading.value = false;
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getWorkHourSummaryPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmWorkHourSummaryApi.WorkHourSummary>,
});
</script>
<template>
  <Page auto-content-height>
    <!-- ç”Ÿæˆæœˆåº¦å·¥æ—¶æ±‡æ€»å¼¹çª— -->
    <Modal
      v-model:open="generateVisible"
      title="生成月度工时汇总"
      width="420px"
      :confirm-loading="generateLoading"
      @ok="handleGenerate"
    >
      <Form layout="vertical" class="pt-4">
        <FormItem label="核算周期" required>
          <DatePicker
            v-model:value="generatePeriod"
            picker="month"
            format="YYYY-MM"
            valueFormat="YYYY-MM"
            placeholder="请选择核算周期"
            style="width: 100%"
          />
        </FormItem>
      </Form>
    </Modal>
    <Grid table-title="月度工时汇总列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '生成月度汇总',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:work-hour:generate'],
              onClick: openGenerateModal,
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/wls/barcode/components/index.ts
@@ -1,3 +1,4 @@
export { default as Barcode } from './barcode.vue';
export { default as BarcodeDetail } from './detail.vue';
export { default as PrinterLabel } from './printer-label.vue';
export { default as ScanModal } from './scan-modal.vue';
src/views/wls/barcode/components/scan-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import {
  Button,
  Descriptions,
  Divider,
  Empty,
  Image,
  Input,
  message,
  Modal,
  Tag,
} from 'ant-design-vue';
import {
  parseBarcodeContent,
  renderBarcodeImage,
} from '#/api/mes/wm/barcode';
import { DictTag } from '#/components/dict-tag';
defineOptions({ name: 'WlsBarcodeScanModal' });
const open = ref(false);
const content = ref('');
const parseResult = ref<Record<string, any>>();
const imageBase64 = ref('');
const parsing = ref(false);
const rendering = ref(false);
function openModal() {
  open.value = true;
  content.value = '';
  parseResult.value = undefined;
  imageBase64.value = '';
}
/** è§£æžæ¡ç å†…容 */
async function handleParse() {
  if (!content.value?.trim()) {
    message.warning('请先扫码枪扫入或手动输入条码内容');
    return;
  }
  parsing.value = true;
  try {
    parseResult.value = await parseBarcodeContent(content.value.trim());
  } finally {
    parsing.value = false;
  }
}
/** æœåŠ¡ç«¯æˆå›¾é¢„è§ˆï¼ˆPNG Base64,不落库) */
async function handleRender() {
  if (!content.value?.trim()) {
    message.warning('请先扫码枪扫入或手动输入条码内容');
    return;
  }
  rendering.value = true;
  try {
    const base64 = await renderBarcodeImage(
      content.value.trim(),
      parseResult.value?.format,
    );
    imageBase64.value = base64 ? `data:image/png;base64,${base64}` : '';
    if (!base64) {
      message.warning('成图失败,请检查条码内容是否符合所选格式');
    }
  } finally {
    rendering.value = false;
  }
}
/** åŒ¹é…æ–¹å¼æ–‡æ¡ˆ */
const matchedLabel = computed(() => {
  if (parseResult.value?.matched) {
    return '精确命中';
  }
  if (parseResult.value?.fields) {
    return '模板反解';
  }
  return '未匹配';
});
const matchedColor = computed(() => {
  if (parseResult.value?.matched) {
    return 'green';
  }
  if (parseResult.value?.fields) {
    return 'blue';
  }
  return 'orange';
});
defineExpose({ open: openModal });
</script>
<template>
  <Modal v-model:open="open" title="扫码解析 / æ¡ç é¢„览" width="720px">
    <div class="space-y-4">
      <div class="flex gap-2">
        <Input
          v-model:value="content"
          placeholder="扫码枪扫入或手动输入条码内容,回车解析"
          @press-enter="handleParse"
        />
        <Button type="primary" :loading="parsing" @click="handleParse">
          è§£æž
        </Button>
        <Button :loading="rendering" @click="handleRender">成图预览</Button>
      </div>
      <div v-if="parseResult">
        <Divider orientation="left">解析结果</Divider>
        <Descriptions
          v-if="parseResult.bizType != null || parseResult.bizCode != null"
          bordered
          :column="2"
          size="small"
        >
          <Descriptions.Item label="匹配方式">
            <Tag :color="matchedColor">{{ matchedLabel }}</Tag>
          </Descriptions.Item>
          <Descriptions.Item v-if="parseResult.format != null" label="条码格式">
            <DictTag
              :type="DICT_TYPE.MES_WM_BARCODE_FORMAT"
              :value="parseResult.format"
            />
          </Descriptions.Item>
          <Descriptions.Item v-if="parseResult.bizType != null" label="业务类型">
            <DictTag
              :type="DICT_TYPE.MES_WM_BARCODE_BIZ_TYPE"
              :value="parseResult.bizType"
            />
          </Descriptions.Item>
          <Descriptions.Item v-if="parseResult.barcodeId != null" label="条码编号">
            {{ parseResult.barcodeId }}
          </Descriptions.Item>
          <Descriptions.Item label="业务编码">
            {{ parseResult.bizCode || '-' }}
          </Descriptions.Item>
          <Descriptions.Item label="业务名称">
            {{ parseResult.bizName || '-' }}
          </Descriptions.Item>
          <Descriptions.Item v-if="parseResult.bizId != null" label="业务编号">
            {{ parseResult.bizId }}
          </Descriptions.Item>
        </Descriptions>
        <div v-if="parseResult.fields" class="mt-2">
          <div class="mb-1 text-sm font-medium">模板字段</div>
          <div
            v-for="(value, key) in parseResult.fields"
            :key="key"
            class="mb-1 flex items-center gap-2"
          >
            <Tag>{{ key }}</Tag>
            <span>{{ value }}</span>
          </div>
        </div>
        <Empty
          v-else-if="parseResult.bizType == null && parseResult.bizCode == null"
          description="未匹配到条码记录或条码配置模板"
        />
      </div>
      <div v-if="imageBase64">
        <Divider orientation="left">成图预览</Divider>
        <div class="flex justify-center rounded bg-gray-100 p-4">
          <Image :src="imageBase64" :width="220" />
        </div>
      </div>
    </div>
    <template #footer>
      <Button @click="open = false">关闭</Button>
    </template>
  </Modal>
</template>
src/views/wls/barcode/index.vue
@@ -18,7 +18,7 @@
} from '#/api/mes/wm/barcode';
import { $t } from '#/locales';
import { Barcode, BarcodeDetail } from './components';
import { Barcode, BarcodeDetail, ScanModal } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -30,6 +30,12 @@
});
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
const scanModalRef = ref<InstanceType<typeof ScanModal>>();
/** æ‰«ç è§£æž / æˆå›¾é¢„览 */
function handleScan() {
  scanModalRef.value?.open();
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
@@ -145,12 +151,20 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <BarcodeDetail ref="barcodeDetailRef" />
    <ScanModal ref="scanModalRef" />
    <Grid table-title="条码列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '扫码解析',
              type: 'primary',
              icon: ACTION_ICON.SCAN,
              auth: ['mes:wm-barcode:query'],
              onClick: handleScan,
            },
            {
              label: $t('ui.actionTitle.create', ['条码']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
src/views/wls/components/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
interface ProcessDefinition {
  id: string;
  name: string;
  key: string;
}
type ProcessSubmitFn = (id: number, processDefinitionKey: string) => Promise<unknown>;
const emit = defineEmits(['success']);
const bizId = ref<number>();
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: p.name,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await modalApi.getData<{ submitFn: ProcessSubmitFn }>().submitFn(
        bizId.value!,
        selectedProcessKey.value,
      );
      await modalApi.close();
      emit('success');
      message.success('提交审批成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      bizId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{
      id: number;
      processList: ProcessDefinition[];
      submitFn: ProcessSubmitFn;
    }>();
    bizId.value = data.id;
    processList.value = data.processList || [];
    // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªæµç¨‹
    const first = processList.value[0];
    if (first) {
      selectedProcessKey.value = first.key;
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">审批流程:</span>
      <Select
        v-model:value="selectedProcessKey"
        :options="processOptions"
        style="width: 200px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>
src/views/wls/equipmentreceipt/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,342 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
import { h, markRaw } from 'vue';
import { MesAutoCodeRuleCode, MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import { DvMachinerySelect } from '#/views/mes/dv/machinery/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'update';
/** è¡¨å•头部是否只读(详情、执行入库态) */
function isHeaderReadonly(formType: FormType): boolean {
  return formType === 'detail' || formType === 'finish';
}
/** è®¾å¤‡å…¥åº“单状态展示映射 */
export const EQUIPMENT_RECEIPT_STATUS_MAP: Record<
  number,
  { text: string; color: string }
> = {
  [MesWmEquipmentReceiptStatusEnum.PREPARE]: {
    text: '草稿',
    color: 'default',
  },
  [MesWmEquipmentReceiptStatusEnum.APPROVING]: {
    text: '审批中',
    color: 'processing',
  },
  [MesWmEquipmentReceiptStatusEnum.APPROVED]: {
    text: '已审批',
    color: 'success',
  },
  [MesWmEquipmentReceiptStatusEnum.FINISHED]: {
    text: '已完成',
    color: 'cyan',
  },
  [MesWmEquipmentReceiptStatusEnum.CANCELED]: {
    text: '已取消',
    color: 'warning',
  },
};
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(
  formType: FormType,
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  const headerReadonly = isHeaderReadonly(formType);
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'status',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: '单据编码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单据编码',
      },
      rules: 'required',
      suffix: headerReadonly
        ? undefined
        : () =>
            h(
              Button,
              {
                type: 'default',
                onClick: async () => {
                  const code = await generateAutoCode(
                    MesAutoCodeRuleCode.WM_EQUIP_RECEIPT_CODE,
                  );
                  await formApi?.setFieldValue('code', code);
                },
              },
              { default: () => '生成' },
            ),
    },
    {
      fieldName: 'name',
      label: '单据名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入单据名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'receiptDate',
      label: '入库日期',
      component: 'DatePicker',
      componentProps: {
        format: 'YYYY-MM-DD',
        placeholder: '请选择入库日期',
        valueFormat: 'YYYY-MM-DD',
      },
      rules: 'required',
    },
    {
      fieldName: 'source',
      label: '入库来源',
      component: 'Input',
      componentProps: {
        placeholder: '请输入入库来源',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: '单据编码',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入单据编码',
      },
    },
    {
      fieldName: 'name',
      label: '单据名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入单据名称',
      },
    },
    {
      fieldName: 'source',
      label: '入库来源',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入入库来源',
      },
    },
    {
      fieldName: 'receiptDate',
      label: '入库日期',
      component: 'RangePicker',
      componentProps: {
        ...getRangePickerDefaultProps(),
        allowClear: true,
      },
    },
    {
      fieldName: 'status',
      label: '单据状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: Object.entries(EQUIPMENT_RECEIPT_STATUS_MAP).map(
          ([value, item]) => ({
            label: item.text,
            value: Number(value),
          }),
        ),
        placeholder: '请选择单据状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmEquipmentReceiptApi.EquipmentReceipt>['columns'] {
  return [
    {
      field: 'code',
      title: '单据编码',
      minWidth: 160,
      slots: { default: 'code' },
    },
    {
      field: 'name',
      title: '单据名称',
      minWidth: 150,
    },
    {
      field: 'source',
      title: '入库来源',
      minWidth: 120,
    },
    {
      field: 'receiptDate',
      title: '入库日期',
      width: 180,
      formatter: 'formatDate',
    },
    {
      field: 'status',
      title: '单据状态',
      minWidth: 100,
      slots: { default: 'status' },
    },
    {
      title: '操作',
      width: 240,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
/** è®¾å¤‡å…¥åº“单行子表的字段 */
export function useLineGridColumns(
  editable = true,
): VxeTableGridOptions<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>['columns'] {
  return [
    {
      field: 'machineryCode',
      title: '设备编码',
      minWidth: 120,
    },
    {
      field: 'machineryName',
      title: '设备名称',
      minWidth: 140,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 120,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 140,
    },
    {
      field: 'specification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'unitMeasureName',
      title: '单位',
      width: 80,
    },
    {
      field: 'quantity',
      title: '入库数量',
      width: 100,
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 120,
    },
    ...(editable
      ? [
          {
            title: '操作',
            width: 160,
            fixed: 'right',
            slots: { default: 'actions' },
          } as const,
        ]
      : []),
  ];
}
/** è®¾å¤‡å…¥åº“单行新增/修改的表单 */
export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
  return [
    {
      fieldName: 'itemId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'machineryId',
      label: '设备',
      component: markRaw(DvMachinerySelect),
      componentProps: {
        placeholder: '请选择设备',
        onChange: async (machinery?: { itemId?: number }) => {
          await formApi?.setFieldValue('itemId', machinery?.itemId);
        },
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'quantity',
      label: '入库数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0.01,
        defaultValue: 1,
        placeholder: '请输入入库数量',
        precision: 2,
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-3',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
src/views/wls/equipmentreceipt/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
import { Button, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  cancelEquipmentReceipt,
  deleteEquipmentReceipt,
  getEquipmentReceiptApproveProcessList,
  getEquipmentReceiptPage,
  submitEquipmentReceiptApproval,
} from '#/api/mes/wm/equipmentreceipt';
import { $t } from '#/locales';
import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
import { EQUIPMENT_RECEIPT_STATUS_MAP, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºè®¾å¤‡å…¥åº“单 */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è®¾å¤‡å…¥åº“单 */
function handleDetail(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** ç¼–辑设备入库单 */
function handleEdit(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** æ‰§è¡Œå…¥åº“ */
function handleFinish(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmitApproval(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getEquipmentReceiptApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi
      .setData({
        id: row.id,
        processList,
        submitFn: submitEquipmentReceiptApproval,
      })
      .open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  if (!row.processInstanceId) {
    message.warning('该单据尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** åˆ é™¤è®¾å¤‡å…¥åº“单 */
async function handleDelete(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.code]),
    duration: 0,
  });
  try {
    await deleteEquipmentReceipt(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å–消设备入库单 */
async function handleCancel(row: MesWmEquipmentReceiptApi.EquipmentReceipt) {
  await cancelEquipmentReceipt(row.id!);
  message.success('取消成功');
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getEquipmentReceiptPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesWmEquipmentReceiptApi.EquipmentReceipt>,
});
const StatusEnum = MesWmEquipmentReceiptStatusEnum;
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="设备入库单列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['设备入库单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:wm:equipment-receipt:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #code="{ row }">
        <Button type="link" @click="handleDetail(row)">
          {{ row.code }}
        </Button>
      </template>
      <template #status="{ row }">
        <Tag :color="EQUIPMENT_RECEIPT_STATUS_MAP[row.status ?? 0]?.color">
          {{ EQUIPMENT_RECEIPT_STATUS_MAP[row.status ?? 0]?.text ?? row.status }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:wm:equipment-receipt:update'],
              ifShow: row.status === StatusEnum.PREPARE,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:wm:equipment-receipt:delete'],
              ifShow: row.status === StatusEnum.PREPARE,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: '提交审批',
              type: 'link',
              auth: ['mes:wm:equipment-receipt:submit'],
              ifShow: row.status === StatusEnum.PREPARE,
              onClick: handleSubmitApproval.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:wm:equipment-receipt:query'],
              ifShow: row.status === StatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '执行入库',
              type: 'link',
              auth: ['mes:wm:equipment-receipt:finish'],
              ifShow: row.status === StatusEnum.APPROVED,
              onClick: handleFinish.bind(null, row),
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
              auth: ['mes:wm:equipment-receipt:cancel'],
              ifShow: row.status === StatusEnum.APPROVED,
              popConfirm: {
                title: '确认取消该设备入库单吗?',
                confirm: handleCancel.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/wls/equipmentreceipt/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmEquipmentReceiptApi } from '#/api/mes/wm/equipmentreceipt';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { MesWmEquipmentReceiptStatusEnum } from '@vben/constants';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEquipmentReceipt,
  finishEquipmentReceipt,
  getEquipmentReceipt,
  updateEquipmentReceipt,
} from '#/api/mes/wm/equipmentreceipt';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmEquipmentReceiptApi.EquipmentReceipt>();
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() =>
  // æ˜¯å¦ä¸ºç¼–辑模式(可保存)
  ['create', 'update'].includes(formType.value),
);
const isFinish = computed(() => formType.value === 'finish'); // æ˜¯å¦ä¸ºæ‰§è¡Œå…¥åº“模式
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['设备入库单']);
  }
  if (formType.value === 'finish') {
    return '执行入库';
  }
  return formType.value === 'update'
    ? $t('ui.actionTitle.edit', ['设备入库单'])
    : $t('ui.actionTitle.create', ['设备入库单']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
/** æ‰§è¡Œå…¥åº“ */
async function handleFinish() {
  if (!formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    await finishEquipmentReceipt(formData.value.id);
    message.success('入库成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!isEditable.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data =
      (await formApi.getValues()) as MesWmEquipmentReceiptApi.EquipmentReceipt;
    try {
      if (formData.value?.id) {
        await updateEquipmentReceipt({ ...formData.value, ...data });
        formData.value = { ...formData.value, ...data };
      } else {
        const id = await createEquipmentReceipt(data);
        formData.value = {
          ...data,
          id,
          status: MesWmEquipmentReceiptStatusEnum.PREPARE,
        };
        await formApi.setFieldValue('id', id);
        await formApi.setFieldValue('status', formData.value.status);
        formType.value = 'update';
      }
      originalSnapshot.value = JSON.stringify(await formApi.getValues());
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      originalSnapshot.value = '';
      return;
    }
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({ schema: useFormSchema(formType.value, formApi) });
    formApi.setDisabled(
      formType.value === 'detail' || formType.value === 'finish',
    );
    modalApi.setState({ showConfirmButton: isEditable.value });
    if (data?.id) {
      modalApi.lock();
      try {
        formData.value = await getEquipmentReceipt(data.id);
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
    }
    originalSnapshot.value = JSON.stringify(await formApi.getValues());
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
    <!-- éžæ–°å»ºæ¨¡å¼å±•示设备明细 -->
    <template v-if="formData?.id">
      <Divider>设备明细</Divider>
      <div class="mx-4">
        <LineList :form-type="formType" :receipt-id="formData.id" />
      </div>
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="isFinish"
          title="确认执行入库?执行后将设备计入设备仓库存台账。"
          @confirm="handleFinish"
        >
          <Button type="primary">执行入库</Button>
        </Popconfirm>
      </div>
    </template>
  </Modal>
</template>
src/views/wls/equipmentreceipt/modules/line-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
<script lang="ts" setup>
import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEquipmentReceiptLine,
  getEquipmentReceiptLine,
  updateEquipmentReceiptLine,
} from '#/api/mes/wm/equipmentreceipt/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>();
const receiptId = ref<number>(); // æ‰€å±žå…¥åº“单编号
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-1',
    labelWidth: 90,
  },
  layout: 'horizontal',
  schema: useLineFormSchema(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data =
      (await formApi.getValues()) as MesWmEquipmentReceiptLineApi.EquipmentReceiptLine;
    data.receiptId = receiptId.value;
    try {
      await (formData.value?.id
        ? updateEquipmentReceiptLine({ ...data, id: formData.value.id })
        : createEquipmentReceiptLine(data));
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useLineFormSchema(formApi) });
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      id?: number;
      receiptId: number;
    }>();
    receiptId.value = data.receiptId;
    if (!data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getEquipmentReceiptLine(data.id);
      // è®¾ç½®åˆ° values
      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/equipmentreceipt/modules/line-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmEquipmentReceiptLineApi } from '#/api/mes/wm/equipmentreceipt/line';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteEquipmentReceiptLine,
  getEquipmentReceiptLineListByReceiptId,
} from '#/api/mes/wm/equipmentreceipt/line';
import { $t } from '#/locales';
import { useLineGridColumns } from '../data';
import LineForm from './line-form.vue';
const props = defineProps<{
  formType: FormType;
  receiptId: number;
}>();
const isEditable = computed(() =>
  // æ˜¯å¦å¯ç¼–辑明细行
  ['create', 'update'].includes(props.formType),
);
const [LineFormModal, lineFormModalApi] = useVbenModal({
  connectedComponent: LineForm,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ·»åŠ è®¾å¤‡ */
function handleCreate() {
  lineFormModalApi.setData({ receiptId: props.receiptId }).open();
}
/** ç¼–辑设备 */
function handleEdit(row: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine) {
  lineFormModalApi.setData({ id: row.id, receiptId: props.receiptId }).open();
}
/** åˆ é™¤è®¾å¤‡ */
async function handleDelete(row: MesWmEquipmentReceiptLineApi.EquipmentReceiptLine) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.machineryName]),
    duration: 0,
  });
  try {
    await deleteEquipmentReceiptLine(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.machineryName]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useLineGridColumns(isEditable.value),
    height: 360,
    keepSource: true,
    pagerConfig: {
      enabled: false,
    },
    proxyConfig: {
      ajax: {
        query: async () => {
          if (!props.receiptId) {
            return { list: [], total: 0 };
          }
          const list = await getEquipmentReceiptLineListByReceiptId(
            props.receiptId,
          );
          return { list, total: list.length };
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
    },
  } as VxeTableGridOptions<MesWmEquipmentReceiptLineApi.EquipmentReceiptLine>,
});
</script>
<template>
  <div>
    <LineFormModal @success="handleRefresh" />
    <Grid table-title="设备明细">
      <template v-if="isEditable" #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '添加设备',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              ifShow: isEditable,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              ifShow: isEditable,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.machineryName]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </div>
</template>
src/views/wls/miscissue/index.vue
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMiscIssueApi } from '#/api/mes/wm/miscissue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmMiscIssueStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
  cancelMiscIssue,
  deleteMiscIssue,
  exportMiscIssue,
  getMiscIssueApproveProcessList,
  getMiscIssuePage,
  submitMiscIssueApproval,
} from '#/api/mes/wm/miscissue';
import { $t } from '#/locales';
import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -48,6 +60,43 @@
/** æ‰§è¡Œå‡ºåº“ */
function handleFinish(row: MesWmMiscIssueApi.MiscIssue) {
  formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmitApproval(row: MesWmMiscIssueApi.MiscIssue) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getMiscIssueApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi
      .setData({
        id: row.id,
        processList,
        submitFn: submitMiscIssueApproval,
      })
      .open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesWmMiscIssueApi.MiscIssue) {
  if (!row.processInstanceId) {
    message.warning('该单据尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** åˆ é™¤æ‚项出库单 */
@@ -111,6 +160,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="杂项出库单列表">
      <template #toolbar-tools>
@@ -162,6 +212,20 @@
              },
            },
            {
              label: '提交审批',
              type: 'link',
              auth: ['mes:wm-misc-issue:update'],
              ifShow: row.status === MesWmMiscIssueStatusEnum.PREPARE,
              onClick: handleSubmitApproval.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:wm-misc-issue:query'],
              ifShow: row.status === MesWmMiscIssueStatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '执行出库',
              type: 'link',
              auth: ['mes:wm-misc-issue:finish'],
src/views/wls/miscissue/modules/form.vue
@@ -15,7 +15,6 @@
  createMiscIssue,
  finishMiscIssue,
  getMiscIssue,
  submitMiscIssue,
  updateMiscIssue,
} from '#/api/mes/wm/miscissue';
import { $t } from '#/locales';
@@ -32,12 +31,6 @@
  ['create', 'update'].includes(formType.value),
);
const isFinish = computed(() => formType.value === 'finish'); // æ˜¯å¦ä¸ºæ‰§è¡Œå‡ºåº“模式
const canSubmit = computed(
  () =>
    // æ˜¯å¦å¯æäº¤
    formType.value === 'update' &&
    formData.value?.status === MesWmMiscIssueStatusEnum.PREPARE,
);
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['杂项出库单']);
@@ -63,29 +56,6 @@
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
/** æäº¤å‡ºåº“单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid || !formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesWmMiscIssueApi.MiscIssue;
      await updateMiscIssue({ ...formData.value, ...data });
      originalSnapshot.value = current;
    }
    await submitMiscIssue(formData.value.id);
    message.success('提交成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
/** æ‰§è¡Œå‡ºåº“ */
async function handleFinish() {
@@ -179,13 +149,6 @@
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该杂项出库单?【提交后将不能修改】"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
        </Popconfirm>
        <Popconfirm
          v-if="isFinish"
          title="确认执行出库?执行后将更新库存台账。"
src/views/wls/miscreceipt/index.vue
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMiscReceiptApi } from '#/api/mes/wm/miscreceipt';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmMiscReceiptStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
  cancelMiscReceipt,
  deleteMiscReceipt,
  exportMiscReceipt,
  getMiscReceiptApproveProcessList,
  getMiscReceiptPage,
  submitMiscReceiptApproval,
} from '#/api/mes/wm/miscreceipt';
import { $t } from '#/locales';
import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -48,6 +60,43 @@
/** æ‰§è¡Œå…¥åº“ */
function handleFinish(row: MesWmMiscReceiptApi.MiscReceipt) {
  formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmitApproval(row: MesWmMiscReceiptApi.MiscReceipt) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getMiscReceiptApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi
      .setData({
        id: row.id,
        processList,
        submitFn: submitMiscReceiptApproval,
      })
      .open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesWmMiscReceiptApi.MiscReceipt) {
  if (!row.processInstanceId) {
    message.warning('该单据尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** åˆ é™¤æ‚项入库单 */
@@ -111,6 +160,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="杂项入库单列表">
      <template #toolbar-tools>
@@ -162,6 +212,20 @@
              },
            },
            {
              label: '提交审批',
              type: 'link',
              auth: ['mes:wm:misc-receipt:submit'],
              ifShow: row.status === MesWmMiscReceiptStatusEnum.PREPARE,
              onClick: handleSubmitApproval.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:wm:misc-receipt:query'],
              ifShow: row.status === MesWmMiscReceiptStatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '执行入库',
              type: 'link',
              auth: ['mes:wm-misc-receipt:finish'],
src/views/wls/miscreceipt/modules/form.vue
@@ -15,7 +15,6 @@
  createMiscReceipt,
  finishMiscReceipt,
  getMiscReceipt,
  submitMiscReceipt,
  updateMiscReceipt,
} from '#/api/mes/wm/miscreceipt';
import { $t } from '#/locales';
@@ -32,12 +31,6 @@
  ['create', 'update'].includes(formType.value),
);
const isFinish = computed(() => formType.value === 'finish'); // æ˜¯å¦ä¸ºæ‰§è¡Œå…¥åº“模式
const canSubmit = computed(
  () =>
    // æ˜¯å¦å¯æäº¤
    formType.value === 'update' &&
    formData.value?.status === MesWmMiscReceiptStatusEnum.PREPARE,
);
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['杂项入库单']);
@@ -63,30 +56,6 @@
  showDefaultActions: false,
  wrapperClass: 'grid-cols-3',
});
/** æäº¤å…¥åº“单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid || !formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data =
        (await formApi.getValues()) as MesWmMiscReceiptApi.MiscReceipt;
      await updateMiscReceipt({ ...formData.value, ...data });
      originalSnapshot.value = current;
    }
    await submitMiscReceipt(formData.value.id);
    message.success('提交成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
/** æ‰§è¡Œå…¥åº“ */
async function handleFinish() {
@@ -180,13 +149,6 @@
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该杂项入库单?【提交后将不能修改】"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
        </Popconfirm>
        <Popconfirm
          v-if="isFinish"
          title="确认执行入库?执行后将更新库存台账。"
src/views/wls/returnvendor/index.vue
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmReturnVendorApi } from '#/api/mes/wm/returnvendor';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmReturnVendorStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
@@ -13,15 +15,25 @@
  cancelReturnVendor,
  deleteReturnVendor,
  exportReturnVendor,
  getReturnVendorApproveProcessList,
  getReturnVendorPage,
  submitReturnVendorApproval,
} from '#/api/mes/wm/returnvendor';
import { $t } from '#/locales';
import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -48,6 +60,43 @@
/** æ‰§è¡Œæ‹£è´§ */
function handleStock(row: MesWmReturnVendorApi.ReturnVendor) {
  formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmitApproval(row: MesWmReturnVendorApi.ReturnVendor) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getReturnVendorApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi
      .setData({
        id: row.id,
        processList,
        submitFn: submitReturnVendorApproval,
      })
      .open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesWmReturnVendorApi.ReturnVendor) {
  if (!row.processInstanceId) {
    message.warning('该单据尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** å®Œæˆé€€è´§ */
@@ -116,6 +165,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="供应商退货单列表">
      <template #toolbar-tools>
@@ -147,6 +197,13 @@
        <TableAction
          :actions="[
            {
              label: '提交审批',
              type: 'link',
              auth: ['mes:wm-return-vendor:update'],
              ifShow: row.status === MesWmReturnVendorStatusEnum.PREPARE,
              onClick: handleSubmitApproval.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
@@ -167,6 +224,13 @@
              },
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:wm-return-vendor:query'],
              ifShow: row.status === MesWmReturnVendorStatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '执行拣货',
              type: 'link',
              auth: ['mes:wm-return-vendor:update'],
src/views/wls/returnvendor/modules/form.vue
@@ -18,7 +18,6 @@
  finishReturnVendor,
  getReturnVendor,
  stockReturnVendor,
  submitReturnVendor,
  updateReturnVendor,
} from '#/api/mes/wm/returnvendor';
import { $t } from '#/locales';
@@ -36,12 +35,6 @@
);
const isStock = computed(() => formType.value === 'stock'); // æ˜¯å¦ä¸ºæ‹£è´§æ¨¡å¼
const isFinish = computed(() => formType.value === 'finish'); // æ˜¯å¦ä¸ºå®Œæˆé€€è´§æ¨¡å¼
const canSubmit = computed(
  () =>
    // æ˜¯å¦å¯æäº¤
    formType.value === 'update' &&
    formData.value?.status === MesWmReturnVendorStatusEnum.PREPARE,
);
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['供应商退货单']);
@@ -80,30 +73,6 @@
    return formatDate(value, 'YYYY-MM-DD HH:mm:ss');
  }
  return undefined;
}
/** æäº¤é€€è´§å•:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid || !formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data =
        (await formApi.getValues()) as MesWmReturnVendorApi.ReturnVendor;
      await updateReturnVendor({ ...formData.value, ...data });
      originalSnapshot.value = current;
    }
    await submitReturnVendor(formData.value.id);
    message.success('提交成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
/** æ‰§è¡Œæ‹£è´§ï¼šæ‹£è´§æ•°é‡ä¸Žé€€è´§æ•°é‡ä¸ä¸€è‡´æ—¶äºŒæ¬¡ç¡®è®¤ */
@@ -228,13 +197,6 @@
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit"
          title="确认提交该退货单?【提交后将不能修改】"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
        </Popconfirm>
        <Button v-if="isStock" type="primary" @click="handleStock">
          æ‰§è¡Œæ‹£è´§
        </Button>
src/views/wls/sn/data.ts
@@ -1,11 +1,26 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSnApi } from '#/api/mes/wm/sn';
import type { DescriptionItemSchema } from '#/components/description';
import { markRaw } from 'vue';
import { MesWmSnStatusEnum } from '@vben/constants';
import { formatDateTime } from '@vben/utils';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import {
  WmWarehouseAreaSelect,
  WmWarehouseLocationSelect,
  WmWarehouseSelect,
} from '#/views/wls/warehouse/components';
/** SN çŠ¶æ€é€‰é¡¹ï¼ˆæ— ç‹¬ç«‹å­—å…¸ï¼Œä½¿ç”¨å¸¸é‡æ˜ å°„ï¼‰ */
export const SN_STATUS_OPTIONS = [
  { label: '在库', value: MesWmSnStatusEnum.IN_STOCK },
  { label: '已出库', value: MesWmSnStatusEnum.OUT_STOCK },
];
/** ç”Ÿæˆ SN ç çš„表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -181,3 +196,275 @@
    },
  ];
}
/** åœ¨åº“单件列表的搜索表单 */
export function useInStockGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: 'SN ç ',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入 SN ç ',
      },
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择物料',
      },
    },
    {
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入批次号',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: SN_STATUS_OPTIONS,
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åœ¨åº“单件列表的字段 */
export function useInStockGridColumns(): VxeTableGridOptions<MesWmSnApi.Sn>['columns'] {
  return [
    {
      field: 'code',
      title: 'SN ç ',
      minWidth: 180,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 120,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 150,
    },
    {
      field: 'specification',
      title: '规格型号',
      minWidth: 120,
    },
    {
      field: 'unitName',
      title: '单位',
      width: 80,
    },
    {
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      field: 'warehouseId',
      title: '仓库',
      minWidth: 140,
      slots: { default: 'locations' },
    },
    {
      field: 'createTime',
      title: '生成时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 140,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
/** SN ç å…¥åº“登记的表单 */
export function useInStockFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: 'SN ç ',
      component: 'Input',
      componentProps: {
        placeholder: '扫码枪扫入或输入 SN ç ',
      },
      rules: 'required',
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择物料(须启用序列号管理)',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'warehouseId',
      label: '仓库',
      component: markRaw(WmWarehouseSelect),
      componentProps: {
        onChange: () =>
          formApi?.setValues({ areaId: undefined, locationId: undefined }),
        placeholder: '请选择仓库',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'locationId',
      label: '库区',
      component: markRaw(WmWarehouseLocationSelect),
      dependencies: {
        triggerFields: ['warehouseId'],
        show: (values) => !!values.warehouseId,
        componentProps: (values) => ({
          onChange: () => formApi?.setFieldValue('areaId', undefined),
          placeholder: '请选择库区',
          warehouseId: values.warehouseId,
        }),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'areaId',
      label: '库位',
      component: markRaw(WmWarehouseAreaSelect),
      dependencies: {
        triggerFields: ['locationId'],
        show: (values) => !!values.locationId,
        componentProps: (values) => ({
          locationId: values.locationId,
          placeholder: '请选择库位',
        }),
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'batchCode',
      label: '批次号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入批次号',
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** SN ç å‡ºåº“核销的表单 */
export function useOutStockFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: 'SN ç ',
      component: 'Input',
      componentProps: {
        placeholder: '扫码枪扫入或输入 SN ç ',
      },
      rules: 'required',
    },
    {
      fieldName: 'itemId',
      label: '物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择物料',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** SN ç å•件详情的描述字段 */
export function useSnDetailSchema(): DescriptionItemSchema[] {
  return [
    {
      field: 'code',
      label: 'SN ç ',
      render: (value) => value || '-',
    },
    {
      field: 'itemCode',
      label: '物料编码',
      render: (value) => value || '-',
    },
    {
      field: 'itemName',
      label: '物料名称',
      render: (value) => value || '-',
    },
    {
      field: 'batchCode',
      label: '批次号',
      render: (value) => value || '-',
    },
    {
      field: 'status',
      label: '状态',
      render: (value) =>
        value === MesWmSnStatusEnum.OUT_STOCK
          ? '已出库'
          : value === MesWmSnStatusEnum.IN_STOCK
            ? '在库'
            : '-',
    },
    {
      field: 'warehouseId',
      label: '在库位置',
      slot: 'location',
    },
    {
      field: 'lastTransactionTypeName',
      label: '最近流水',
      slot: 'lastTransaction',
    },
    {
      field: 'lastTransactionTime',
      label: '最近流水时间',
      render: (value) => formatDateTime(value) || '-',
    },
  ];
}
src/views/wls/sn/index.vue
@@ -2,10 +2,12 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSnApi } from '#/api/mes/wm/sn';
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { message, Tabs, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -13,12 +15,28 @@
  exportSnDetailExcel,
  exportSnGroupExcel,
  getSnGroupPage,
  getSnInStockPage,
} from '#/api/mes/wm/sn';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import {
  useGridColumns,
  useGridFormSchema,
  useInStockGridColumns,
  useInStockGridFormSchema,
} from './data';
import {
  formatWmLocation,
  getWmLocationMaps,
  type WmLocationMaps,
} from './location-maps';
import Detail from './modules/detail.vue';
import Form from './modules/form.vue';
import InStockModal from './modules/in-stock-modal.vue';
import OutStockModal from './modules/out-stock-modal.vue';
import SnDetailModal from './modules/sn-detail-modal.vue';
const activeKey = ref('group');
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -30,9 +48,25 @@
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
const [InStockModalRef, inStockModalApi] = useVbenModal({
  connectedComponent: InStockModal,
  destroyOnClose: true,
});
const [OutStockModalRef, outStockModalApi] = useVbenModal({
  connectedComponent: OutStockModal,
  destroyOnClose: true,
});
const snDetailModalRef = ref<InstanceType<typeof SnDetailModal>>();
/** åˆ·æ–°å½“前页签 */
function handleRefresh() {
  if (activeKey.value === 'group') {
  gridApi.query();
  } else {
    inStockGridApi.query();
  }
}
/** ç”Ÿæˆ SN ç  */
@@ -74,6 +108,34 @@
  }
}
/** å…¥åº“登记 */
function handleInStock() {
  inStockModalApi.open();
}
/** å‡ºåº“核销 */
function handleOutStock() {
  outStockModalApi.open();
}
/** æŸ¥çœ‹å•件详情 */
function handleSnDetail(row: MesWmSnApi.Sn) {
  snDetailModalRef.value?.open(row.code);
}
/** åœ¨åº“单件列表位置名称映射(懒加载,首次访问时初始化) */
const locationMapsRef = ref<Awaited<ReturnType<typeof getWmLocationMaps>>>();
const EMPTY_MAPS: WmLocationMaps = {
  warehouseNameMap: {},
  locationNameMap: {},
  areaNameMap: {},
};
async function ensureLocationMaps() {
  if (!locationMapsRef.value) {
    locationMapsRef.value = await getWmLocationMaps();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
@@ -103,12 +165,51 @@
    },
  } as VxeTableGridOptions<MesWmSnApi.SnGroup>,
});
const [InStockGrid, inStockGridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useInStockGridFormSchema(),
  },
  gridOptions: {
    columns: useInStockGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getSnInStockPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesWmSnApi.Sn>,
});
// é¦–次挂载时预热位置名称映射,供列表展示
void ensureLocationMaps();
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <DetailModal />
    <InStockModalRef @success="handleRefresh" />
    <OutStockModalRef @success="handleRefresh" />
    <SnDetailModal ref="snDetailModalRef" />
    <Tabs v-model:active-key="activeKey" class="px-4 pt-1" type="card">
      <Tabs.TabPane key="group" tab="SN ç åˆ†ç»„">
    <Grid table-title="SN ç åˆ—表">
      <template #toolbar-tools>
        <TableAction
@@ -173,5 +274,62 @@
        />
      </template>
    </Grid>
      </Tabs.TabPane>
      <Tabs.TabPane key="in-stock" tab="在库单件">
        <InStockGrid table-title="SN å•件状态列表">
          <template #toolbar-tools>
            <TableAction
              :actions="[
                {
                  label: '入库登记',
                  type: 'primary',
                  icon: ACTION_ICON.ADD,
                  auth: ['mes:wm-sn:update'],
                  onClick: handleInStock,
                },
                {
                  label: '出库核销',
                  type: 'primary',
                  danger: true,
                  icon: ACTION_ICON.DELETE,
                  auth: ['mes:wm-sn:update'],
                  onClick: handleOutStock,
                },
              ]"
            />
          </template>
          <template #status="{ row }">
            <Tag :color="row.status === 1 ? 'green' : 'orange'">
              {{ row.status === 1 ? '在库' : '已出库' }}
            </Tag>
          </template>
          <template #locations="{ row }">
            <span v-if="row.status === 1">
              {{
                formatWmLocation(
                  locationMapsRef ?? EMPTY_MAPS,
                  row.warehouseId,
                  row.locationId,
                  row.areaId,
                )
              }}
            </span>
            <span v-else>-</span>
          </template>
          <template #actions="{ row }">
            <TableAction
              :actions="[
                {
                  label: '详情',
                  type: 'link',
                  auth: ['mes:wm-sn:query'],
                  onClick: handleSnDetail.bind(null, row),
                },
              ]"
            />
          </template>
        </InStockGrid>
      </Tabs.TabPane>
    </Tabs>
  </Page>
</template>
src/views/wls/sn/location-maps.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location';
export interface WmLocationMaps {
  warehouseNameMap: Record<number, string>;
  locationNameMap: Record<number, string>;
  areaNameMap: Record<number, string>;
}
let cached: WmLocationMaps | null = null;
/** åŠ è½½ä»“åº“/库区/库位 ID -> åç§°æ˜ å°„(模块级缓存,仅首次加载) */
export async function getWmLocationMaps(): Promise<WmLocationMaps> {
  if (cached) {
    return cached;
  }
  const [warehouses, locations, areas] = await Promise.all([
    getWarehouseSimpleList(),
    getWarehouseLocationSimpleList(),
    getWarehouseAreaSimpleList(),
  ]);
  cached = {
    warehouseNameMap: Object.fromEntries(
      warehouses.map((w) => [w.id!, w.name || w.code || '']),
    ),
    locationNameMap: Object.fromEntries(
      locations.map((l) => [l.id!, l.name || l.code || '']),
    ),
    areaNameMap: Object.fromEntries(
      areas.map((a) => [a.id!, a.name || a.code || '']),
    ),
  };
  return cached;
}
/** æ ¹æ® ID æ¸²æŸ“ ä»“库/库区/库位 æ–‡æœ¬ï¼ˆID ç¼ºå¤±æ—¶è¿”回空串) */
export function formatWmLocation(
  maps: WmLocationMaps,
  warehouseId?: number,
  locationId?: number,
  areaId?: number,
): string {
  return [
    warehouseId ? maps.warehouseNameMap[warehouseId] : '',
    locationId ? maps.locationNameMap[locationId] : '',
    areaId ? maps.areaNameMap[areaId] : '',
  ]
    .filter(Boolean)
    .join(' / ');
}
src/views/wls/sn/modules/in-stock-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import type { MesWmSnApi } from '#/api/mes/wm/sn';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { registerSnInStock } from '#/api/mes/wm/sn';
import { useInStockFormSchema } from '../data';
defineOptions({ name: 'WlsSnInStockModal' });
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesWmSnApi.SnInStock;
    try {
      await registerSnInStock(data);
      await modalApi.close();
      emit('success');
      message.success('入库登记成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    formApi.setState({ schema: useInStockFormSchema(formApi) });
    await formApi.resetForm();
  },
});
</script>
<template>
  <Modal title="SN ç å…¥åº“登记" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/wls/sn/modules/out-stock-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<script lang="ts" setup>
import type { MesWmSnApi } from '#/api/mes/wm/sn';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { outboundSn } from '#/api/mes/wm/sn';
import { useOutStockFormSchema } from '../data';
defineOptions({ name: 'WlsSnOutStockModal' });
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesWmSnApi.SnOutStock;
    try {
      await outboundSn(data);
      await modalApi.close();
      emit('success');
      message.success('出库核销成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    formApi.setState({ schema: useOutStockFormSchema() });
    await formApi.resetForm();
  },
});
</script>
<template>
  <Modal title="SN ç å‡ºåº“核销" class="w-1/2">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/wls/sn/modules/sn-detail-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
<script lang="ts" setup>
import type { MesWmSnApi } from '#/api/mes/wm/sn';
import { computed, ref } from 'vue';
import { Button, Empty, Input, message, Modal } from 'ant-design-vue';
import { getSnDetail } from '#/api/mes/wm/sn';
import { useDescription } from '#/components/description';
import { useSnDetailSchema } from '../data';
import { formatWmLocation, getWmLocationMaps } from '../location-maps';
defineOptions({ name: 'WlsSnDetailModal' });
const open = ref(false);
const code = ref('');
const detail = ref<MesWmSnApi.SnDetail>();
const loading = ref(false);
const locationMaps = ref<Awaited<ReturnType<typeof getWmLocationMaps>>>();
const locationText = computed(() =>
  detail.value
    ? formatWmLocation(
        locationMaps.value ?? {
          warehouseNameMap: {},
          locationNameMap: {},
          areaNameMap: {},
        },
        detail.value.warehouseId,
        detail.value.locationId,
        detail.value.areaId,
      )
    : '-',
);
const lastTransactionText = computed(() => {
  const d = detail.value;
  if (!d) {
    return '-';
  }
  const direction = d.lastTransactionTypeName || '';
  const quantity = d.lastTransactionQuantity ?? '';
  const bizCode = d.lastTransactionBizCode || '-';
  return [direction, quantity, bizCode].filter(Boolean).join(' ');
});
async function loadDetail(snCode: string) {
  loading.value = true;
  try {
    detail.value = await getSnDetail(snCode);
    if (!locationMaps.value) {
      locationMaps.value = await getWmLocationMaps();
    }
  } finally {
    loading.value = false;
  }
}
async function handleQuery() {
  if (!code.value?.trim()) {
    message.warning('请输入或扫码获取 SN ç ');
    return;
  }
  await loadDetail(code.value.trim());
}
function openModal(snCode?: string) {
  open.value = true;
  code.value = snCode ?? '';
  detail.value = undefined;
  if (snCode) {
    void loadDetail(snCode);
  }
}
defineExpose({ open: openModal });
const [Descriptions] = useDescription({
  bordered: true,
  column: 1,
  schema: useSnDetailSchema(),
  useCard: false,
  labelStyle: { width: '80px', whiteSpace: 'nowrap' },
});
</script>
<template>
  <Modal v-model:open="open" title="SN ç å•件详情" width="720px">
    <div class="flex gap-2">
      <Input
        v-model:value="code"
        placeholder="输入或扫码 SN ç åŽæŸ¥è¯¢"
        @press-enter="handleQuery"
      />
      <Button type="primary" :loading="loading" @click="handleQuery">
        æŸ¥è¯¢
      </Button>
    </div>
    <div class="mt-4">
      <Descriptions v-if="detail" :data="detail">
        <template #location>
          {{ locationText }}
        </template>
        <template #lastTransaction>
          {{ lastTransactionText }}
        </template>
      </Descriptions>
      <Empty
        v-else
        :description="loading ? '查询中...' : '暂无数据,请输入 SN ç æŸ¥è¯¢'"
      />
    </div>
    <template #footer>
      <Button @click="open = false">关闭</Button>
    </template>
  </Modal>
</template>
src/views/wls/stocktaking/plan/data.ts
@@ -133,6 +133,40 @@
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'autoGenerate',
      label: '自动生成',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'generateCron',
      label: '生成周期(Cron)',
      component: 'Input',
      componentProps: {
        placeholder: '请输入 Cron è¡¨è¾¾å¼ï¼Œå¦‚ 0 0 2 * * ?',
        helpMessage:
          '启用自动生成后,需在「系统管理->定时任务」配置 handler=MesWmStockTakingPlanAutoJob çš„ cron ä»»åŠ¡ï¼Œåˆ°æœŸè‡ªåŠ¨æŒ‰æœ¬æ–¹æ¡ˆç”Ÿæˆç›˜ç‚¹ä»»åŠ¡',
      },
      dependencies: {
        triggerFields: ['autoGenerate'],
        show: (values) => !!values.autoGenerate,
      },
    },
    {
      fieldName: 'lastGenerateTime',
      label: '上次生成时间',
      component: 'Input',
      disabled: true,
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
@@ -238,6 +272,21 @@
      },
    },
    {
      field: 'autoGenerate',
      title: '自动生成',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'lastGenerateTime',
      title: '上次生成时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      field: 'status',
      title: '状态',
      width: 120,
src/views/wls/stocktaking/task/data.ts
@@ -29,13 +29,11 @@
} from '#/views/wls/warehouse/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'execute' | 'submit' | 'update';
export type FormType = 'create' | 'detail' | 'submit' | 'update';
/** è¡¨å•头部是否只读(提交、执行盘点、详情态) */
/** è¡¨å•头部是否只读(提交、详情态) */
function isHeaderReadonly(formType: FormType): boolean {
  return (
    formType === 'detail' || formType === 'execute' || formType === 'submit'
  );
  return formType === 'detail' || formType === 'submit';
}
/** æ–°å¢ž/修改的表单 */
src/views/wls/stocktaking/task/index.vue
@@ -3,6 +3,7 @@
import type { MesWmStockTakingTaskApi } from '#/api/mes/wm/stocktaking/task';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesWmStockTakingTaskStatusEnum } from '@vben/constants';
@@ -15,7 +16,9 @@
  cancelStockTaking,
  deleteStockTaking,
  exportStockTaking,
  getStockTakingApproveProcessList,
  getStockTakingPage,
  submitStockTakingApproval,
} from '#/api/mes/wm/stocktaking/task';
import {
  importStockTakingQuantity,
@@ -23,11 +26,19 @@
} from '#/api/wls/stocktaking/task/line';
import { $t } from '#/locales';
import ProcessSelectModal from '#/views/wls/components/process-select-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -51,14 +62,46 @@
  formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** æäº¤ç›˜ç‚¹ä»»åŠ¡ */
/** å¼€å§‹ç›˜ç‚¹ */
function handleSubmit(row: MesWmStockTakingTaskApi.StockTakingTask) {
  formModalApi.setData({ formType: 'submit', id: row.id }).open();
}
/** æ‰§è¡Œç›˜ç‚¹ */
function handleExecute(row: MesWmStockTakingTaskApi.StockTakingTask) {
  formModalApi.setData({ formType: 'execute', id: row.id }).open();
/** æäº¤ç»“果审批 - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmitApproval(row: MesWmStockTakingTaskApi.StockTakingTask) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getStockTakingApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    processModalApi
      .setData({
        id: row.id,
        processList,
        submitFn: submitStockTakingApproval,
      })
      .open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesWmStockTakingTaskApi.StockTakingTask) {
  if (!row.processInstanceId) {
    message.warning('该任务尚未提交结果审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
/** å–消盘点任务 */
@@ -159,6 +202,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="盘点任务列表">
      <template #toolbar-tools>
@@ -205,7 +249,7 @@
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交',
              label: '开始盘点',
              type: 'link',
              auth: ['mes:wm-stock-taking-task:update'],
              ifShow: row.status === StatusEnum.PREPARE,
@@ -224,13 +268,6 @@
              },
            },
            {
              label: '执行盘点',
              type: 'link',
              auth: ['mes:wm-stock-taking-task:update'],
              ifShow: row.status === StatusEnum.APPROVING,
              onClick: handleExecute.bind(null, row),
            },
            {
              label: '导入实盘数',
              type: 'link',
              icon: ACTION_ICON.UPLOAD,
@@ -239,6 +276,20 @@
              onClick: handleImport.bind(null, row),
            },
            {
              label: '提交结果审批',
              type: 'link',
              auth: ['mes:wm-stock-taking-task:update'],
              ifShow: row.status === StatusEnum.APPROVING,
              onClick: handleSubmitApproval.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:wm-stock-taking-task:query'],
              ifShow: row.status === StatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '取消',
              type: 'link',
              danger: true,
src/views/wls/stocktaking/task/modules/form.vue
@@ -14,7 +14,6 @@
import { useVbenForm } from '#/adapter/form';
import {
  createStockTaking,
  finishStockTaking,
  getStockTaking,
  submitStockTaking,
  updateStockTaking,
@@ -35,29 +34,23 @@
const isEditable = computed(() =>
  ['create', 'update'].includes(formType.value),
);
const isExecute = computed(() => formType.value === 'execute'); // æ˜¯å¦æ‰§è¡Œç›˜ç‚¹æ¨¡å¼
const isSubmit = computed(() => formType.value === 'submit'); // æ˜¯å¦æäº¤æ¨¡å¼
const canSubmit = computed(
  () =>
    formType.value === 'update' &&
    formData.value?.status === MesWmStockTakingTaskStatusEnum.PREPARE,
const isSubmit = computed(() => formType.value === 'submit'); // æ˜¯å¦å¼€å§‹ç›˜ç‚¹æ¨¡å¼
const isResultEditable = computed(
  // ç›˜ç‚¹ä¸­ï¼ˆå®¡æ‰¹ä¸­ï¼‰å¯ç»´æŠ¤ç›˜ç‚¹ç»“æžœ
  () => formData.value?.status === MesWmStockTakingTaskStatusEnum.APPROVING,
);
const showLineTab = computed(() => !formData.value?.blindFlag); // ç›²ç›˜ä¸å±•示盘点清单
const showResultTab = computed(
  () =>
    isExecute.value ||
    (!!formData.value?.status &&
      formData.value.status !== MesWmStockTakingTaskStatusEnum.PREPARE),
    !!formData.value?.status &&
    formData.value.status !== MesWmStockTakingTaskStatusEnum.PREPARE,
);
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return $t('ui.actionTitle.view', ['盘点任务']);
  }
  if (formType.value === 'execute') {
    return '执行盘点';
  }
  if (formType.value === 'submit') {
    return '提交盘点任务';
    return '开始盘点';
  }
  return formType.value === 'update'
    ? $t('ui.actionTitle.edit', ['盘点任务'])
@@ -78,7 +71,7 @@
  wrapperClass: 'grid-cols-3',
});
/** æäº¤ç›˜ç‚¹ä»»åŠ¡ï¼šè¡¨å•æœ‰ä¿®æ”¹æ—¶å…ˆä¿å­˜ï¼Œå†è°ƒç”¨æäº¤æŽ¥å£ */
/** å¼€å§‹ç›˜ç‚¹ï¼šè¡¨å•有修改时先保存,再提交 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid || !formData.value?.id) {
@@ -94,23 +87,7 @@
      originalSnapshot.value = current;
    }
    await submitStockTaking(formData.value.id);
    message.success('提交成功');
    await modalApi.close();
    emit('success');
  } finally {
    modalApi.unlock();
  }
}
/** æ‰§è¡Œç›˜ç‚¹ */
async function handleExecute() {
  if (!formData.value?.id) {
    return;
  }
  modalApi.lock();
  try {
    await finishStockTaking(formData.value.id);
    message.success('执行盘点成功');
    message.success('开始盘点成功');
    await modalApi.close();
    emit('success');
  } finally {
@@ -163,7 +140,7 @@
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    subTabsName.value = data.formType === 'execute' ? 'results' : 'lines';
    subTabsName.value = 'lines';
    formApi.setState({ schema: useFormSchema(formType.value, formApi) });
    formApi.setDisabled(!isEditable.value);
    modalApi.setState({ showConfirmButton: isEditable.value });
@@ -199,7 +176,7 @@
      </Tabs.TabPane>
      <Tabs.TabPane v-if="showResultTab" key="results" tab="盘点结果">
        <ResultList
          :form-type="isExecute ? 'execute' : 'detail'"
          :form-type="isResultEditable ? 'execute' : 'detail'"
          :task-id="formData.id"
        />
      </Tabs.TabPane>
@@ -207,18 +184,11 @@
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
        <Popconfirm
          v-if="canSubmit || isSubmit"
          title="确认提交该盘点任务?【提交后将不能修改】"
          v-if="isSubmit"
          title="确认开始盘点?开始后将冻结库存并可录入/导入实盘数。"
          @confirm="handleSubmit"
        >
          <Button type="primary">提交</Button>
        </Popconfirm>
        <Popconfirm
          v-if="isExecute"
          title="确认执行盘点操作?"
          @confirm="handleExecute"
        >
          <Button type="primary">执行盘点</Button>
          <Button type="primary">开始盘点</Button>
        </Popconfirm>
      </div>
    </template>
src/views/wls/transaction/data.ts
@@ -30,6 +30,11 @@
  { label: '销售退货', value: 120 },
  { label: '外协发料', value: 121 },
  { label: '外协入库', value: 122 },
  { label: '盘点调整', value: 107 },
  { label: 'SN å‡ºå…¥åº“', value: 109 },
  { label: '采购入库红冲', value: 111 },
  { label: '设备台账入库', value: 400 },
  { label: '设备入库', value: 401 },
];
/** åˆ—表的搜索表单 */