Prechádzať zdrojové kódy

pms 保养台历 工单 等bug修改

zhangcl 3 mesiacov pred
rodič
commit
dd1d496ca9

+ 6 - 1
src/api/pms/iotsapstock/index.ts

@@ -32,7 +32,7 @@ export interface IotSapStockVO {
 // PMS SAP 库存(通用库存/项目部库存) API
 export const IotSapStockApi = {
   // 查询PMS SAP 库存(通用库存/项目部库存)分页
-  getIotSapStockPage: async (params: any) => {
+  getIotSapStockPage: async (params: PageParam) => {
     return await request.get({ url: `/pms/iot-sap-stock/page`, params })
   },
 
@@ -51,6 +51,11 @@ export const IotSapStockApi = {
     return await request.put({ url: `/pms/iot-sap-stock/update`, data })
   },
 
+  // 批量设置安全库存
+  batchSetSafetyStock: async (data: IotSapStockVO[]) => {
+    return await request.post({ url: `/pms/iot-sap-stock/batchSetSafetyStock`, data })
+  },
+
   // 删除PMS SAP 库存(通用库存/项目部库存)
   deleteIotSapStock: async (id: number) => {
     return await request.delete({ url: `/pms/iot-sap-stock/delete?id=` + id })

+ 50 - 0
src/router/modules/remaining.ts

@@ -249,6 +249,56 @@ const remainingRouter: AppRouteRecordRaw[] = [
     ]
   },
 
+  {
+    path: '/iotpms/iotsapstock',
+    component: Layout,
+    name: 'PmsSapStockCenter',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'sapstock',
+        component: () => import('@/views/pms/iotsapstock/index.vue'),
+        name: 'IotSapStock',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:menu',
+          title: 'SAP库存',
+          activeMenu: '/sapstock/index'
+        }
+      },
+      {
+        path: 'sapstock/config',
+        component: () => import('@/views/pms/iotsapstock/IotSapStockConfig.vue'),
+        name: 'IotSapStockConfig',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: 'SAP安全库存',
+          activeMenu: '/sapstock/config'
+        }
+      },
+      {
+        path: 'sapstock/safe',
+        component: () => import('@/views/pms/iotsapstock/IotConfigSafeStock.vue'),
+        name: 'IotConfigSafeStock',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: '配置安全库存',
+          activeMenu: '/sapstock/safe'
+        }
+      },
+    ]
+  },
+
   {
     path: '/iotpms/iotmaintenanceplan',
     component: Layout,

+ 65 - 24
src/views/pms/iotlockstock/index.vue

@@ -8,32 +8,35 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="工厂" prop="factory">
-        <el-input
-          v-model="queryParams.factory"
-          placeholder="请输入工厂"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+      <el-form-item label="工厂" prop="factoryId">
+        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
+          <el-option
+            v-for="item in factoryList"
+            :key="item.id"
+            :label="item.factoryName"
+            :value="item.id!"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="库存地点" prop="projectDepartment">
-        <el-input
-          v-model="queryParams.projectDepartment"
-          placeholder="请输入库存地点"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+      <el-form-item label="库存地点" prop="storageLocationId">
+        <el-select v-model="queryParams.storageLocationId" clearable placeholder="请选择" class="!w-240px">
+          <el-option
+            v-for="item in storageLocationList"
+            :key="item.id"
+            :label="item.storageLocationName"
+            :value="item.id!"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="成本中心" prop="costCenter">
-        <el-input
-          v-model="queryParams.costCenter"
-          placeholder="请输入成本中心"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
+      <el-form-item label="成本中心" prop="costCenterId">
+        <el-select v-model="queryParams.costCenterId" clearable placeholder="请选择" class="!w-240px">
+          <el-option
+            v-for="item in costCenterList"
+            :key="item.id"
+            :label="item.costCenterName"
+            :value="item.id!"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item label="物料编码" prop="materialCode">
         <el-input
@@ -138,6 +141,7 @@ import { IotLockStockApi, IotLockStockVO } from '@/api/pms/iotlockstock'
 import IotLockStockForm from './IotLockStockForm.vue'
 import {erpPriceTableColumnFormatter} from "@/utils";
 import {DICT_TYPE} from "@/utils/dict";
+import {SapOrgApi, SapOrgVO} from "@/api/system/saporg";
 
 /** PMS 本地 库存 列表 */
 defineOptions({ name: 'IotLockStock' })
@@ -155,8 +159,11 @@ const queryParams = reactive({
   pageSize: 10,
   deptId: undefined,
   factory: undefined,
+  factoryId: undefined,
   projectDepartment: undefined,
+  storageLocationId: undefined,
   costCenter: undefined,
+  costCenterId: undefined,
   pickingListNumber: undefined,
   materialCode: undefined,
   materialName: undefined,
@@ -176,6 +183,15 @@ const queryParams = reactive({
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
+const factoryList = ref([] as SapOrgVO[])   // 工厂列表
+const storageLocationList = ref([] as SapOrgVO[]) // 库存地点列表
+const costCenterList = ref([] as SapOrgVO[]) // SAP成本中心列表
+
+const selectedFactoryReqVO = ref({
+  type: 0, // 类型(1工厂 2成本中心 3库位)
+  factoryCodes: [] // 已经选择的SAP工厂code 列表
+})
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -186,6 +202,12 @@ const getList = async () => {
   } finally {
     loading.value = false
   }
+  // 加载工厂(SAP)列表
+  factoryList.value = await SapOrgApi.getSimpleSapOrgList(1)
+  // 成本中心
+  costCenterList.value = await SapOrgApi.getSimpleSapOrgList(2)
+  // 库存地点
+  storageLocationList.value = await SapOrgApi.getSimpleSapOrgList(3)
 }
 
 /** 搜索按钮操作 */
@@ -223,6 +245,25 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 已经选择了 SAP工厂 */
+const selectedFactoryChange = async (selectedId: number | undefined) => {
+
+  // 获取选中的factoryCode数组
+  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
+  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
+
+  // 获得已经选择的 SAP 工厂 数组
+  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 成本中心
+  selectedFactoryReqVO.value.type = 2
+  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
+  costCenterList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
+
+  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
+  selectedFactoryReqVO.value.type = 3
+  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
+  storageLocationList.value = await SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
+}
+
 /** 导出按钮操作 */
 const handleExport = async () => {
   try {

+ 71 - 10
src/views/pms/iotmaincalendar/index.vue

@@ -20,6 +20,7 @@ import dayGridPlugin from '@fullcalendar/daygrid'
 import interactionPlugin from '@fullcalendar/interaction'
 import '@fullcalendar/core/locales-all'
 import 'bootstrap-icons/font/bootstrap-icons.css'
+const { push } = useRouter() // 路由跳转
 
 defineOptions({ name: 'MaintenanceCalendar' })
 
@@ -29,9 +30,9 @@ const calendar = ref(null)
 
 // 模拟API返回的数据结构
 const mockData = {
-  '2023-08-01': { plans: 3, orders: 2 },
-  '2023-08-15': { plans: 5, orders: 1 },
-  '2023-08-20': { plans: 2, orders: 4 }
+  '2025-05-01': { plans: 3, orders: 2 },
+  '2025-05-15': { plans: 5, orders: 1 },
+  '2025-05-20': { plans: 2, orders: 4 }
 }
 
 // 日历配置
@@ -93,12 +94,12 @@ function renderEventContent(eventInfo) {
       <div class="calendar-day-content">
         <div class="count-item">
           <a class="plan-count" data-date="${eventInfo.event.startStr}">
-            <i class="bi bi-clipboard-check"></i> ${plans}
+            <i class="bi bi-clipboard-check"></i> 保养计划${plans}
           </a>
         </div>
         <div class="count-item">
           <a class="order-count" data-date="${eventInfo.event.startStr}">
-            <i class="bi bi-tools"></i> ${orders}
+            <i class="bi bi-tools"></i> 保养工单${orders}
           </a>
         </div>
       </div>
@@ -110,18 +111,21 @@ function renderEventContent(eventInfo) {
 function handleDayClick(event) {
   if (event.target.classList.contains('plan-count')) {
     const date = event.target.dataset.date
+    /*
     router.push({
-      path: '/maintenance/plans',
+      path: 'IotMaintenancePlan',
       query: { date }
-    })
+    }) */
+    router.push({ name: 'IotMaintenancePlan', params:{} })
   }
 
   if (event.target.classList.contains('order-count')) {
     const date = event.target.dataset.date
-    router.push({
+    /* router.push({
       path: '/maintenance/orders',
       query: { date }
-    })
+    }) */
+    router.push({ name: 'IotMainWorkOrder', params:{} })
   }
 }
 
@@ -145,6 +149,12 @@ onMounted(() => {
   margin: 0 auto;
 }
 
+:deep(.fc-event) {
+  background-color: #e6f7ff !important; /* 浅蓝色背景 */
+  border-color: #e6f7ff !important;     /* 边框颜色同步 */
+  color: #333 !important;               /* 文字颜色加深 */
+}
+
 .loading {
   position: fixed;
   top: 50%;
@@ -175,7 +185,7 @@ onMounted(() => {
 }
 
 :deep(.plan-count) {
-  color: #1890ff;
+  color: #6ea4d5;
   text-decoration: none;
   display: block;
 }
@@ -193,4 +203,55 @@ onMounted(() => {
 :deep(.bi) {
   margin-right: 3px;
 }
+
+/* 精确控制prev/next/today按钮 */
+:deep(.fc-prev-button),
+:deep(.fc-next-button),
+:deep(.fc-today-button) {
+  background-color: #f0f0f0 !important;
+  border-color: #d9d9d9 !important;
+  color: #333 !important;
+  position: relative;
+  padding: 6px 12px !important; /* 增加按钮宽度 */
+}
+
+/* 隐藏默认图标 */
+:deep(.fc-button .fc-icon) {
+  display: none !important;
+}
+
+/* 添加自定义箭头 */
+:deep(.fc-prev-button)::before {
+  content: "<";
+  display: inline-block;
+  font-weight: bold;
+  margin-right: 4px;
+}
+
+:deep(.fc-next-button)::after {
+  content: ">";
+  display: inline-block;
+  font-weight: bold;
+  margin-left: 4px;
+}
+
+/* 调整today按钮间距 */
+:deep(.fc-today-button) {
+  margin: 0 8px !important;
+}
+
+:deep(.fc-prev-button:hover),
+:deep(.fc-next-button:hover),
+:deep(.fc-today-button:hover) {
+  background-color: #e0e0e0 !important;
+  border-color: #c0c0c0 !important;
+}
+
+/* 保持按钮组对齐 */
+:deep(.fc-toolbar-chunk:first-child) {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
 </style>

+ 86 - 1
src/views/pms/iotmainworkorder/IotMainWorkOrder.vue

@@ -32,11 +32,51 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
+          <el-col :span="8">
+            <el-form-item label="实际保养开始时间" prop="actualStartTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualStartTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养开始时间"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="实际保养结束时间" prop="actualEndTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualEndTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养结束时间"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="保养费用" prop="cost">
+              <el-input
+                v-model="formData.cost"
+                placeholder="根据物料消耗自动生成"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="16">
             <el-form-item label="备注" prop="remark">
               <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
             </el-form-item>
           </el-col>
+          <el-col :span="8">
+            <el-form-item label="其他费用" prop="otherCost">
+              <el-input
+                v-model="formData.otherCost"
+                @input="handleInput(formData.otherCost, 'otherCost')"
+                placeholder="其他费用"
+              />
+            </el-form-item>
+          </el-col>
         </el-row>
       </div>
     </el-form>
@@ -511,10 +551,55 @@ const handleView = (nodeId) => {
   console.log('当前bom节点:', currentBomNodeId.value)
 }
 
+// 计算保养金额
+const calculateTotalCost = () => {
+  // 物料总金额 = ∑(单价 * 消耗数量)
+  const materialTotal = materialList.value.reduce((sum, item) => {
+    const price = Number(item.unitPrice) || 0
+    const quantity = Number(item.quantity) || 0
+    return sum + (price * quantity)
+  }, 0)
+
+  // 保养 = 物料总金额
+  formData.value.cost = (materialTotal).toFixed(2)
+}
+
+// 监听物料列表变化
+watch(
+  () => materialList.value,
+  () => {
+    calculateTotalCost()
+  },
+  { deep: true }
+)
+
 const hasMaterial = (bomNodeId: number) => {
   return materialList.value.some(item => item.bomNodeId === bomNodeId)
 }
 
+const handleInput = (value, obj) => {
+  // 1. 过滤非法字符(只允许数字和小数点)
+  let filtered = value.replace(/[^\d.]/g, '')
+
+  // 2. 处理多个小数点的情况
+  filtered = filtered.replace(/\.{2,}/g, '.')
+
+  // 3. 限制小数点后最多两位
+  let decimalParts = filtered.split('.')
+  if (decimalParts.length > 1) {
+    decimalParts = decimalParts.slice(0, 2)
+    filtered = decimalParts.join('.')
+  }
+
+  // 4. 处理以小数点开头的情况(自动补0)
+  if (filtered.startsWith('.')) {
+    filtered = '0' + filtered
+  }
+
+  // 5. 更新绑定值(同时处理连续输入多个0的情况)
+  formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
+}
+
 // 保存配置
 const saveConfig = () => {
   (configFormRef.value as any).validate((valid: boolean) => {

+ 89 - 1
src/views/pms/iotmainworkorder/IotMainWorkOrderAdd.vue

@@ -32,11 +32,51 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="24">
+          <el-col :span="8">
+            <el-form-item label="实际保养开始时间" prop="actualStartTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualStartTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养开始时间"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="实际保养结束时间" prop="actualEndTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualEndTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养结束时间"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="保养费用" prop="cost">
+              <el-input
+                v-model="formData.cost"
+                placeholder="根据物料消耗自动生成"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="16">
             <el-form-item label="备注" prop="remark">
               <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
             </el-form-item>
           </el-col>
+          <el-col :span="8">
+            <el-form-item label="其他费用" prop="otherCost">
+              <el-input
+                v-model="formData.otherCost"
+                @input="handleInput(formData.otherCost, 'otherCost')"
+                placeholder="其他费用"
+              />
+            </el-form-item>
+          </el-col>
         </el-row>
       </div>
     </el-form>
@@ -412,6 +452,9 @@ const formData = ref({
   name: '',
   orderNumber: undefined,
   responsiblePerson: undefined,
+  actualStartTime: undefined,
+  actualEndTime: undefined,
+  cost: undefined,
   remark: undefined,
   status: undefined,
 })
@@ -449,6 +492,28 @@ const configDialog = reactive({
   }
 })
 
+// 监听物料列表变化
+watch(
+  () => materialList.value,
+  () => {
+    calculateTotalCost()
+  },
+  { deep: true }
+)
+
+// 计算保养金额
+const calculateTotalCost = () => {
+  // 物料总金额 = ∑(单价 * 消耗数量)
+  const materialTotal = materialList.value.reduce((sum, item) => {
+    const price = Number(item.unitPrice) || 0
+    const quantity = Number(item.quantity) || 0
+    return sum + (price * quantity)
+  }, 0)
+
+  // 保养 = 物料总金额
+  formData.value.cost = (materialTotal).toFixed(2)
+}
+
 // 打开配置对话框
 const openConfigDialog = (row: IotMainWorkOrderBomVO) => {
   configDialog.current = row
@@ -643,6 +708,29 @@ const saveConfig = () => {
   })
 }
 
+const handleInput = (value, obj) => {
+  // 1. 过滤非法字符(只允许数字和小数点)
+  let filtered = value.replace(/[^\d.]/g, '')
+
+  // 2. 处理多个小数点的情况
+  filtered = filtered.replace(/\.{2,}/g, '.')
+
+  // 3. 限制小数点后最多两位
+  let decimalParts = filtered.split('.')
+  if (decimalParts.length > 1) {
+    decimalParts = decimalParts.slice(0, 2)
+    filtered = decimalParts.join('.')
+  }
+
+  // 4. 处理以小数点开头的情况(自动补0)
+  if (filtered.startsWith('.')) {
+    filtered = '0' + filtered
+  }
+
+  // 5. 更新绑定值(同时处理连续输入多个0的情况)
+  formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
+}
+
 const queryParams = reactive({
   workOrderId: id
 })

+ 34 - 0
src/views/pms/iotmainworkorder/IotMainWorkOrderDetail.vue

@@ -56,6 +56,40 @@
               </el-select>
             </el-form-item>
           </el-col>
+          <el-col :span="8">
+            <el-form-item label="保养费用(元)" prop="cost">
+              <el-input type="text" v-model="formData.cost" disabled/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="实际保养开始时间" prop="actualStartTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualStartTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养开始时间"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="实际保养结束时间" prop="actualEndTime">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.actualEndTime"
+                type="datetime"
+                value-format="x"
+                placeholder="实际保养结束时间"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="其他费用(元)" prop="otherCost">
+              <el-input type="text" v-model="formData.otherCost" disabled/>
+            </el-form-item>
+          </el-col>
           <el-col :span="24">
             <el-form-item label="备注" prop="remark">
               <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" disabled/>

+ 1 - 0
src/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue

@@ -16,6 +16,7 @@
           <el-table-column prop="materialName" label="物料名称" width="180" />
           <el-table-column prop="materialCode" label="物料编码" width="180" />
           <el-table-column prop="unit" label="单位" width="180" />
+          <el-table-column prop="unitPrice" label="单价" width="180" />
           <el-table-column prop="quantity" label="消耗数量" width="180" />
           <el-table-column prop="materialSource" label="库存类型" width="180" />
           <el-table-column label="操作" align="right">

+ 25 - 0
src/views/pms/iotmainworkorder/WorkOrderMaterial.vue

@@ -69,12 +69,14 @@
           :show-overflow-tooltip="true"
         />
         <el-table-column label="单位" align="center" prop="unit" />
+        <el-table-column label="单价" align="center" prop="unitPrice" />
         <el-table-column label="总库存数量" align="center" prop="totalInventoryQuantity" />
         <el-table-column label="来源" align="center" prop="materialSource" />
         <el-table-column label="消耗数量" align="center" prop="quantity">
           <template #default="scope">
             <el-input
               v-model="scope.row.quantity"
+              @input="handleInput(scope.row.quantity, 'quantity')"
               @click.stop=""
               @focus="scope.$el.querySelector('input').focus()"
             />
@@ -229,6 +231,29 @@ const rowClassName = ({ row }: { row: any }) => {
   return className
 }
 
+const handleInput = (value, obj) => {
+  // 1. 过滤非法字符(只允许数字和小数点)
+  let filtered = value.replace(/[^\d.]/g, '')
+
+  // 2. 处理多个小数点的情况
+  filtered = filtered.replace(/\.{2,}/g, '.')
+
+  // 3. 限制小数点后最多两位
+  let decimalParts = filtered.split('.')
+  if (decimalParts.length > 1) {
+    decimalParts = decimalParts.slice(0, 2)
+    filtered = decimalParts.join('.')
+  }
+
+  // 4. 处理以小数点开头的情况(自动补0)
+  if (filtered.startsWith('.')) {
+    filtered = '0' + filtered
+  }
+
+  // 5. 更新绑定值(同时处理连续输入多个0的情况)
+  formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
+}
+
 // 多选 切换行选中状态
 const toggleRow = (row) => {
   const index = selectedRows.value.findIndex((item) => item.materialCode === row.materialCode)

+ 187 - 0
src/views/pms/iotsapstock/IotConfigSafeStock.vue

@@ -0,0 +1,187 @@
+<template>
+  <ContentWrap>
+    <ContentWrap>
+      <!-- 搜索工作栏 -->
+      <el-form
+        class="-mb-15px"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item>
+          <el-button @click="openForm" type="warning"
+            ><Icon icon="ep:plus" class="mr-5px" /> 选择库存</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column
+          type="index"
+          label="序号"
+          width="60"
+          align="center"
+        />
+        <el-table-column label="id" align="center" prop="id" v-if="false"/>
+        <el-table-column label="物料编码" align="center" prop="materialCode" />
+        <el-table-column label="物料名称" align="center" prop="materialName" />
+        <el-table-column label="单位" align="center" prop="unit" />
+        <el-table-column label="单价" align="center" prop="unitPrice" :formatter="erpPriceTableColumnFormatter" />
+        <el-table-column label="数量" align="center" prop="quantity" :formatter="erpPriceTableColumnFormatter" />
+        <el-table-column label="安全库存" align="center" prop="safetyStock" :formatter="erpPriceTableColumnFormatter" />
+        <el-table-column label="操作" align="center" min-width="120px">
+          <template #default="scope">
+            <div style="display: flex; justify-content: center; align-items: center; width: 100%">
+              <div>
+                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
+                <el-button
+                  style="vertical-align: middle"
+                  link
+                  type="danger"
+                  @click="handleDelete(scope.row.code)"
+                >
+                  移除
+                </el-button>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+    </ContentWrap>
+    <!--
+    <MaterialSelect ref="materialFormRef" @choose="materialChoose" /> -->
+    <SelectSapStock ref="sapStockFormRef" @choose="stockChoose" />
+  </ContentWrap>
+  <ContentWrap>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button @click="submitForm" type="primary" :disabled="formLoading">保 存</el-button>
+        <el-button @click="close">取 消</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+
+import { IotLockStockApi, IotLockStockVO } from '@/api/pms/iotlockstock'
+import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { ref } from 'vue'
+import { toRaw } from "vue";
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import MaterialSelect from "@/views/pms/iotlockstock/SelectMaterial.vue";
+import {erpPriceTableColumnFormatter} from "@/utils";
+
+/** 手工入库 表单 */
+defineOptions({ name: 'IotConfigSafeStock' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
+const deptUsers = ref<UserApi.UserVO[]>([]) // 用户列表
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const list = ref<IotLockStockVO[]>([]) // 入库物料的数据
+const { params, name } = useRoute() // 查询参数
+
+const sapStockFormRef = ref()
+const openForm = () => {
+  sapStockFormRef.value.open()
+}
+
+const formRef = ref() // 表单 Ref
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotSapStockConfig', params:{}})
+}
+
+// 多选 物料
+const stockChoose = (selectedStocks) => {
+  // 转换数据结构(根据你的接口定义调整)
+  const newItems = selectedStocks.map(stock => ({
+    id: stock.id,
+    materialCode: stock.materialCode,
+    materialName: stock.materialName,
+    unit: stock.unit,
+    quantity: stock.quantity,
+    safetyStock: stock.safetyStock,  // 安全库存数量
+    unitPrice: stock.unitPrice,     // 单价
+    remark: null,
+    code: stock.materialCode  // 移除操作需要
+  }))
+
+  // 合并到现有列表(去重)
+  newItems.forEach(item => {
+    const exists = list.value.some(
+      existing => existing.materialCode === item.materialCode
+    )
+    if (!exists) {
+      list.value.push(item)
+    }
+  })
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  if (toRaw(list.value).length == 0) {
+    message.error('请选择库存')
+    return
+  }
+  // 校验表单 校验 单价 数量
+  if (list.value.length > 0) {
+    const nullList = list.value.filter((item) => item.quantity===null)
+    if (nullList.length > 0) {
+      message.error('请填写数量')
+      return
+    }
+  }
+  if (list.value.length > 0) {
+    const nullList = list.value.filter((item) => item.unitPrice===null)
+    if (nullList.length > 0) {
+      message.error('请填写单价')
+      return
+    }
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    // await IotLockStockApi.manualWarehouse(list.value)
+    await IotSapStockApi.batchSetSafetyStock(list.value)
+    message.success(t('common.createSuccess'))
+    close()
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formRef.value?.resetFields()
+}
+onMounted(async () => {
+  const deptId = useUserStore().getUser.deptId
+  // deptUsers.value = await UserApi.getDeptUsersByDeptId(deptId)
+})
+
+// 调整删除逻辑
+const handleDelete = (code: string) => {
+  const index = list.value.findIndex(item => item.code === code)
+  if (index > -1) {
+    list.value.splice(index, 1)
+  }
+}
+</script>
+<style scoped>
+.base-expandable-content {
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+</style>

+ 276 - 0
src/views/pms/iotsapstock/IotSapStockConfig.vue

@@ -0,0 +1,276 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="工厂" prop="factoryId">
+        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
+          <el-option
+            v-for="item in factoryList"
+            :key="item.id"
+            :label="item.factoryName"
+            :value="item.id!"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="库存地点" prop="storageLocationId">
+        <el-select v-model="queryParams.storageLocationId" clearable placeholder="请选择" class="!w-240px">
+          <el-option
+            v-for="item in storageLocationList"
+            :key="item.id"
+            :label="item.storageLocationName"
+            :value="item.id!"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="物料编码" prop="materialCode">
+        <el-input
+          v-model="queryParams.materialCode"
+          placeholder="请输入物料编码"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="物料名称" prop="materialName">
+        <el-input
+          v-model="queryParams.materialName"
+          placeholder="请输入物料名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-220px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['pms:iot-sap-stock:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 配置安全库存
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['pms:iot-sap-stock:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="工厂" align="center" prop="factory" />
+      <el-table-column label="库存地点" align="center" prop="projectDepartment" />
+      <el-table-column label="物料编码" align="center" prop="materialCode" />
+      <el-table-column label="物料名称" align="center" prop="materialName" />
+      <el-table-column label="数量" align="center" prop="quantity" />
+      <el-table-column label="单价" align="center" prop="unitPrice" />
+      <el-table-column label="单位" align="center" prop="unit" />
+      <el-table-column label="安全库存" align="center" prop="safetyStock" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" min-width="120px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['pms:iot-sap-stock:update']"
+            v-if="false"
+          >
+            安全库存
+          </el-button>
+          <!--
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['pms:iot-sap-stock:delete']"
+          >
+            删除
+          </el-button>
+          -->
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <IotSapStockForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
+import IotSapStockForm from './IotSapStockForm.vue'
+import * as SapOrgApi from "@/api/system/saporg";
+
+/** PMS SAP 库存(通用库存/项目部库存) 列表 */
+defineOptions({ name: 'IotSapStockConfig' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由跳转
+const factoryList = ref([] as SapOrgApi.SapOrgVO[])   // 工厂列表
+const storageLocationList = ref([] as SapOrgApi.SapOrgVO[]) // 库存地点列表
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotSapStockVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: undefined,
+  factoryId: undefined,
+  factory: undefined,
+  storageLocationId: undefined,
+  projectDepartment: undefined,
+  materialCode: undefined,
+  materialName: undefined,
+  materialGroupName: undefined,
+  materialGroupId: undefined,
+  quantity: undefined,
+  unitPrice: undefined,
+  unit: undefined,
+  safetyStock: undefined,
+  shelvesId: undefined,
+  cargoLocationId: undefined,
+  type: undefined,
+  syncStatus: undefined,
+  syncTime: [],
+  syncError: undefined,
+  sort: undefined,
+  status: undefined,
+  remark: undefined,
+  createTime: [],
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotSapStockApi.getIotSapStockPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+  // 加载工厂(SAP)列表
+  factoryList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(1)
+  // 加载库存地点(SAP)列表
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  // formRef.value.open(type, id)
+  push({ name: 'IotConfigSafeStock', params:{} })
+}
+
+const selectedFactoryReqVO = ref({
+  type: 0, // 类型(1工厂 2成本中心 3库位)
+  factoryCodes: [] // 已经选择的SAP工厂code 列表
+})
+
+/** 已经选择了 SAP工厂 */
+const selectedFactoryChange = async (selectedId: number | undefined) => {
+
+  // 获取选中的factoryCode数组
+  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
+  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
+
+  // 获得已经选择的 SAP 工厂 数组
+  // const factoryIds = formData.value.factoryIds
+  console.log('选择的工厂代码:', selectedFactoryCodes)
+  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
+  selectedFactoryReqVO.value.type = 3
+  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotSapStockApi.deleteIotSapStock(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotSapStockApi.exportIotSapStock(queryParams)
+    download.excel(data, 'PMS SAP 库存(通用库存/项目部库存).xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 234 - 0
src/views/pms/iotsapstock/SelectSapStock.vue

@@ -0,0 +1,234 @@
+<template>
+  <Dialog v-model="dialogVisible"
+          title="选择SAP库存"
+          style="width: 1100px; max-height: 800px" @close="handleClose">
+    <div>
+      <ContentWrap>
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="物料名称" prop="materialName">
+            <el-input
+              v-model="queryParams.materialName"
+              placeholder="请输入物料名称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="物料编码" prop="code">
+            <el-input
+              v-model="queryParams.materialCode"
+              placeholder="请输入物料编码"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+            <el-button @click="handleConfirm" class="custom-green-button"><Icon icon="ep:check" class="mr-5px" /> 确认选择</el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+      <ContentWrap>
+        <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          ref="tableRef"
+          :show-overflow-tooltip="true"
+          @row-click="handleRowClick"
+        >
+          <el-table-column width="60" label="选择">
+            <template #default="{ row }">
+              <el-checkbox
+                :model-value="selectedRows.some(item => item.id === row.id)"
+                @click.stop="toggleRow(row)"
+                class="no-label-radio"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column label="id" align="center" prop="id" width="120" v-if="false"/>
+          <el-table-column
+            label="物料编码"
+            align="center"
+            prop="materialCode"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column
+            label="物料名称"
+            align="center"
+            prop="materialName"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column label="数量" align="center" prop="quantity" width="120" />
+          <el-table-column label="单位" align="center" prop="unit" width="120" />
+          <el-table-column label="单价" align="center" prop="unitPrice" width="120" />
+          <el-table-column label="安全库存" align="center" prop="safetyStock">
+            <template #default="scope">
+              <el-input
+                v-model="scope.row.safetyStock"
+                @click.stop=""
+                @focus="scope.$el.querySelector('input').focus()"
+              />
+            </template>
+          </el-table-column>
+        </el-table>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </div>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DictDataVO } from '@/api/system/dict/dict.data'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MaterialApi from '@/api/pms/material'
+import * as SapStockApi from '@/api/pms/iotsapstock'
+import {DICT_TYPE} from "@/utils/dict";
+import {ContentWrap} from "@/components/ContentWrap";
+import {IotSapStockApi, IotSapStockVO} from "@/api/pms/iotsapstock";
+
+// 调整 emit 类型
+const emit = defineEmits<{
+  (e: 'choose', value: SapStockApi.IotSapStockVO[]): void
+  (e: 'close'): void
+}>()
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const queryFormRef = ref() // 搜索的表单
+const list = ref<DictDataVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const selectedRows = ref<SapStockApi.IotSapStockVO[]>([]); // 多选数据(存储所有选中行的数组)
+const tableRef = ref();
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  label: '',
+  materialName: undefined,
+  materialCode: undefined,
+  status: undefined,
+})
+
+const selectedRow = ref(null)
+
+// 多选 切换行选中状态
+const toggleRow = (row) => {
+  const index = selectedRows.value.findIndex(item => item.id === row.id);
+  if (index > -1) {
+    selectedRows.value.splice(index, 1); // 取消选中
+  } else {
+    selectedRows.value.push(row); // 选中
+  }
+};
+
+// 关闭时清空选择
+const handleClose = () => {
+  tableRef.value?.clearSelection();
+  selectedRows.value = []
+  emit('close')
+};
+
+// 处理单选逻辑
+const selectRow = (row) => {
+  selectedRow.value = selectedRow.value?.id === row.id ? null : row
+  emit('choose', row)
+  dialogVisible.value = false
+}
+
+// 确认选择
+const handleConfirm = () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请至少选择一个库存')
+    return
+  }
+  emit('choose', selectedRows.value.map(row => ({
+    ...row,
+    // 确保返回必要字段
+    code: row.code,
+    name: row.name,
+    unit: row.unit
+  })))
+  dialogVisible.value = false;
+  handleClose()
+};
+
+// 点击整行选中
+const handleRowClick = (row) => {
+  toggleRow(row);
+}
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SapStockApi.IotSapStockApi.getIotSapStockPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const choose = (row: DictDataVO) => {
+  emit('choose', row)
+  dialogVisible.value = false
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+/** 初始化 **/
+
+</script>
+<style lang="scss" scoped>
+.no-label-radio .el-radio__label {
+  display: none;
+}
+.no-label-radio .el-radio__inner {
+  margin-right: 0;
+}
+
+/* 自定义淡绿色按钮 */
+:deep(.custom-green-button) {
+  background-color: #e1f3d8;
+  border-color: #e1f3d8;
+  color: #67c23a;
+}
+
+/* 悬停效果 */
+:deep(.custom-green-button:hover) {
+  background-color: #d1e8c0;
+  border-color: #d1e8c0;
+  color: #5daf34;
+}
+
+/* 点击效果 */
+:deep(.custom-green-button:active) {
+  background-color: #c2dca8;
+  border-color: #c2dca8;
+}
+
+</style>

+ 25 - 1
src/views/pms/iotsapstock/index.vue

@@ -9,7 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="工厂" prop="factoryId">
-        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px">
+        <el-select v-model="queryParams.factoryId" clearable placeholder="请选择" class="!w-240px" @change="selectedFactoryChange">
           <el-option
             v-for="item in factoryList"
             :key="item.id"
@@ -106,9 +106,11 @@
             type="primary"
             @click="openForm('update', scope.row.id)"
             v-hasPermi="['pms:iot-sap-stock:update']"
+            v-if="false"
           >
             安全库存
           </el-button>
+          <!--
           <el-button
             link
             type="danger"
@@ -117,6 +119,7 @@
           >
             删除
           </el-button>
+          -->
         </template>
       </el-table-column>
     </el-table>
@@ -216,6 +219,27 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+const selectedFactoryReqVO = ref({
+  type: 0, // 类型(1工厂 2成本中心 3库位)
+  factoryCodes: [] // 已经选择的SAP工厂code 列表
+})
+
+/** 已经选择了 SAP工厂 */
+const selectedFactoryChange = async (selectedId: number | undefined) => {
+
+  // 获取选中的factoryCode数组
+  const selectedFactory = factoryList.value.find(item => item.id === selectedId)
+  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
+
+  // 获得已经选择的 SAP 工厂 数组
+  // const factoryIds = formData.value.factoryIds
+  console.log('选择的工厂代码:', selectedFactoryCodes)
+  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
+  selectedFactoryReqVO.value.type = 3
+  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 3 - 0
src/views/system/dept/DeptForm.vue

@@ -178,6 +178,9 @@ const open = async (type: string, id?: number) => {
   // stockLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
 
   // 根据已有的SAP工厂值 获取 factoryCode数组
+  if (formData.value.factoryIds === null) {
+    return;
+  }
   selectedFactoryCodes.value = formData.value.factoryIds.map(id => {
     const factory = factoryList.value.find(item => item.id === id)
     return factory ? factory.factoryCode : null