فهرست منبع

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/views/pms/iotmainworkorder/WorkOrderMaterial.vue
zhangcl 3 ماه پیش
والد
کامیت
f330113615
49فایلهای تغییر یافته به همراه3962 افزوده شده و 266 حذف شده
  1. 1 0
      package.json
  2. 3 1
      src/api/pms/device/index.ts
  3. 2 1
      src/api/pms/deviceattrmodel/index.ts
  4. 52 0
      src/api/pms/iotopeationfill/index.ts
  5. 54 0
      src/api/pms/maintain/materials/index.ts
  6. 80 0
      src/api/pms/modelattrtemplate/index.ts
  7. 59 0
      src/api/pms/modeltemplate/index.ts
  8. 22 1
      src/router/modules/remaining.ts
  9. 1 0
      src/utils/dict.ts
  10. 69 54
      src/views/pms/device/IotDeviceForm.vue
  11. 2 3
      src/views/pms/failure/DeviceList.vue
  12. 3 3
      src/views/pms/failure/IotFailureReportForm.vue
  13. 7 7
      src/views/pms/failure/index.vue
  14. 2 0
      src/views/pms/inspect/item/IotInspectItemForm.vue
  15. 1 6
      src/views/pms/inspect/order/InspectOrderDetail.vue
  16. 6 2
      src/views/pms/inspect/order/WriteOrder.vue
  17. 15 8
      src/views/pms/inspect/order/index.vue
  18. 1 1
      src/views/pms/inspect/plan/InspectRouteList.vue
  19. 12 8
      src/views/pms/inspect/plan/IotInspectPlan.vue
  20. 3 3
      src/views/pms/inspect/route/InspectItemList.vue
  21. 3 1
      src/views/pms/inspect/route/IotInspectRoute.vue
  22. 85 47
      src/views/pms/iotmainworkorder/WorkOrderMaterial.vue
  23. 178 0
      src/views/pms/iotopeationfill/IotOpeationFillForm.vue
  24. 247 0
      src/views/pms/iotopeationfill/index.vue
  25. 263 0
      src/views/pms/iotopeationmodel/IotOpeationModelForm.vue
  26. 306 0
      src/views/pms/iotopeationmodel/index.vue
  27. 149 64
      src/views/pms/maintain/IotMaintain.vue
  28. 76 46
      src/views/pms/maintain/IotMaintainDetail.vue
  29. 7 7
      src/views/pms/maintain/index.vue
  30. 241 0
      src/views/pms/maintain/material/ChooseMaintain.vue
  31. 3 0
      src/views/pms/maintain/material/MaterialSelect.vue
  32. 2 2
      src/views/pms/maintenance/MainPlanDeviceList.vue
  33. 125 0
      src/views/pms/modeltemplate/ModelCategoryTree.vue
  34. 168 0
      src/views/pms/modeltemplate/TemplateForm.vue
  35. 34 0
      src/views/pms/modeltemplate/detail/TemplateDetailsHeader.vue
  36. 218 0
      src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelForm.vue
  37. 171 0
      src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelProperty.vue
  38. 182 0
      src/views/pms/modeltemplate/detail/attrsModel/config.ts
  39. 173 0
      src/views/pms/modeltemplate/detail/attrsModel/index.vue
  40. 48 0
      src/views/pms/modeltemplate/detail/components/DataDefinition.vue
  41. 3 0
      src/views/pms/modeltemplate/detail/components/index.ts
  42. 52 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelArrayDataSpecs.vue
  43. 163 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelEnumDataSpecs.vue
  44. 142 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelNumberDataSpecs.vue
  45. 170 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelStructDataSpecs.vue
  46. 11 0
      src/views/pms/modeltemplate/detail/dataSpecs/index.ts
  47. 52 0
      src/views/pms/modeltemplate/detail/index.vue
  48. 294 0
      src/views/pms/modeltemplate/index.vue
  49. 1 1
      src/views/system/tree/PmsTree.vue

+ 1 - 0
package.json

@@ -81,6 +81,7 @@
     "vue-types": "^5.1.1",
     "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
+    "vuex": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"
   },

+ 3 - 1
src/api/pms/device/index.ts

@@ -52,7 +52,9 @@ export const IotDeviceApi = {
   deviceAssociateBomList: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/deviceAssociateBomList`, params })
   },
-
+  deviceAssociateBomListPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-device/deviceAssociateBomListPage`, params })
+  },
   // 查询设备台账详情
   getIotDevice: async (id: number) => {
     return await request.get({ url: `/rq/iot-device/get?id=` + id })

+ 2 - 1
src/api/pms/deviceattrmodel/index.ts

@@ -47,7 +47,8 @@ export const DeviceAttrModelApi = {
   // 获得设备属性
   getDeviceAttrModelListByDeviceCategoryId: async (params: any) => {
     return await request.get({
-      url: `/pms/iot-device-category-template-attrs/list-by-device-category-id?deviceCategoryId=`+params
+      url: `/pms/iot-device-category-template-attrs/list-by-device-category-id`,
+      params
     })
   },
 

+ 52 - 0
src/api/pms/iotopeationfill/index.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+// 运行记录填报 VO
+export interface IotOpeationFillVO {
+  id: number // 主键id
+  deviceCode: string // 资产编号
+  deviceName: string // 设备名称
+  fillContent: string // 填写内容
+  deviceType: string // 设备类别
+  deviceComponent: string // 设备部件
+  deptId: number // 公司id
+  orgName: string // 所属组织
+  proId: number // 项目部id
+  proName: string // 所属项目部
+  teamId: number // 小队id
+  teamName: string // 所属小队
+  dutyName: string // 设备负责人
+  creDate: Date // 填写日期
+}
+
+// 运行记录填报 API
+export const IotOpeationFillApi = {
+  // 查询运行记录填报分页
+  getIotOpeationFillPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-opeation-fill/page`, params })
+  },
+
+  // 查询运行记录填报详情
+  getIotOpeationFill: async (id: number) => {
+    return await request.get({ url: `/rq/iot-opeation-fill/get?id=` + id })
+  },
+
+  // 新增运行记录填报
+  createIotOpeationFill: async (data: IotOpeationFillVO) => {
+    return await request.post({ url: `/rq/iot-opeation-fill/create`, data })
+  },
+
+  // 修改运行记录填报
+  updateIotOpeationFill: async (data: IotOpeationFillVO) => {
+    return await request.put({ url: `/rq/iot-opeation-fill/update`, data })
+  },
+
+  // 删除运行记录填报
+  deleteIotOpeationFill: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-opeation-fill/delete?id=` + id })
+  },
+
+  // 导出运行记录填报 Excel
+  exportIotOpeationFill: async (params) => {
+    return await request.download({ url: `/rq/iot-opeation-fill/export-excel`, params })
+  },
+}

+ 54 - 0
src/api/pms/maintain/materials/index.ts

@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+// 维修工单对应物料 VO
+export interface IotMaintainMaterialsVO {
+  id: number // 主键
+  maintainId: number // 维修工单id
+  bomId: number // 维修项id
+  bomNodeId: number // 保养工单明细设备BOM节点id
+  materialId: number // 物料id
+  materialCode: string // 物料编码
+  materialName: string // 物料名称
+  quantity: number // 消耗数量
+  unitPrice: number // 单价(元)
+  totalPrice: number // 总金额(元)
+  totalInventoryQuantity: number // 总数量
+  materialSource: string // 物料来源 (SAP库存 本地库存...)
+  inventoryAddress: string // 库存地点
+  sort: number // 排序
+  status: number // 状态 0启用  1停用
+  remark: string // 备注
+}
+
+// 维修工单对应物料 API
+export const IotMaintainMaterialsApi = {
+  // 查询维修工单对应物料分页
+  getIotMaintainMaterialsPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-maintain-materials/page`, params })
+  },
+
+  // 查询维修工单对应物料详情
+  getIotMaintainMaterials: async (id: number) => {
+    return await request.get({ url: `/rq/iot-maintain-materials/get?id=` + id })
+  },
+
+  // 新增维修工单对应物料
+  createIotMaintainMaterials: async (data: IotMaintainMaterialsVO) => {
+    return await request.post({ url: `/rq/iot-maintain-materials/create`, data })
+  },
+
+  // 修改维修工单对应物料
+  updateIotMaintainMaterials: async (data: IotMaintainMaterialsVO) => {
+    return await request.put({ url: `/rq/iot-maintain-materials/update`, data })
+  },
+
+  // 删除维修工单对应物料
+  deleteIotMaintainMaterials: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-maintain-materials/delete?id=` + id })
+  },
+
+  // 导出维修工单对应物料 Excel
+  exportIotMaintainMaterials: async (params) => {
+    return await request.download({ url: `/rq/iot-maintain-materials/export-excel`, params })
+  },
+}

+ 80 - 0
src/api/pms/modelattrtemplate/index.ts

@@ -0,0 +1,80 @@
+import request from '@/config/axios'
+
+/**
+ * 设备分类属性 模型
+ */
+export interface DeviceAttrModelData {
+  id?: number // 设备属性id
+  code?: string // 设备属性编码
+  name?: string // 设备属性名称
+  defaultValue?: string // 默认值
+  description?: string // 属性描述
+  deviceCategoryId?: number // 设备分类id
+  deviceId?: number // 设备id
+  templateId?: number // 设备分类模板id
+  requiredFlag?: number // 是否必填
+  dataType: string // 数据类型,与 dataSpecs 的 dataType 保持一致
+  type: string // 属性字段类型
+  selectOptions: ModelTemplateAttrs // 设备属性
+  isCollection?:number,
+  modelAttr?:string,
+}
+
+/**
+ * 设备 模拟属性
+ */
+export interface SimulatorData extends DeviceAttrModelData {
+  simulateValue?: string | number // 用于存储模拟值
+}
+
+/**
+ * ModelTemplateAttrs 类型
+ */
+export interface ModelTemplateAttrs {
+  [key: string]: any
+}
+
+// 设备(分类)属性 模型 API
+export const DeviceAttrModelApi = {
+  // 查询 设备属性 分页
+  getDeviceAttrModelPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-model-template-attrs/page`, params })
+  },
+
+  // 获得设备属性列表
+  getDeviceAttrModelList: async (params: any) => {
+    return await request.get({ url: `/rq/iot-model-template-attrs/list`, params })
+  },
+
+  // 获得设备属性
+  getDeviceAttrModelListByDeviceCategoryId: async (params: any) => {
+    return await request.get({
+      url: `/rq/iot-model-template-attrs/list-by-model-category-id?deviceCategoryId=`+params
+    })
+  },
+
+  // 查询设备属性详情
+  getDeviceAttrModel: async (id: number) => {
+    return await request.get({ url: `/rq/iot-model-template-attrs/get?id=` + id })
+  },
+
+  getThingsModelAttr: async (deviceCategoryName: string) => {
+    return await request.get({ url: `/rq/iot-model-template-attrs/getAttrs?deviceCategoryName=` + deviceCategoryName })
+  },
+
+
+  // 新增设备属性
+  createDeviceAttrModel: async (data: DeviceAttrModelData) => {
+    return await request.post({ url: `/rq/iot-model-template-attrs/create`, data })
+  },
+
+  // 修改设备属性
+  updateDeviceAttrModel: async (data: DeviceAttrModelData) => {
+    return await request.put({ url: `/rq/iot-model-template-attrs/update`, data })
+  },
+
+  // 删除设备属性
+  deleteDeviceAttrModel: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-model-template-attrs/delete?id=` + id })
+  }
+}

+ 59 - 0
src/api/pms/modeltemplate/index.ts

@@ -0,0 +1,59 @@
+import request from '@/config/axios'
+
+export interface ModelAttrTemplateVO {
+  id?: number
+  name: string
+  deviceCategoryId: number
+  code: string
+  attrs: ModelTemplateAttrs
+  description: string
+  status: number
+  sort: number
+  remark: string
+}
+
+/**
+ * 设备专有属性 类型
+ */
+export interface ModelTemplateAttrs {
+  [key: string]: any
+}
+
+// 根据设备分类id查询设备专有属性模板
+export const getAttrTemplateByModelCategoryId = async (params: any): Promise<any> => {
+  return await request.get({ url: '/rq/iot-model-template/list-by-model-category-id?deviceCategoryId='+ params })
+}
+
+// 查询 设备属性模板 列表
+export const getModelTemplatePage = async (params: PageParam) => {
+  return await request.get({ url: '/rq/iot-model-template/page', params })
+}
+
+// 查询 设备属性模板 详情
+export const getModelTemplate = async (id: number) => {
+  return await request.get({ url: '/rq/iot-model-template/get?id=' + id })
+}
+
+// 新增 设备属性模板
+export const createModelTemplate = async (data: ModelAttrTemplateVO) => {
+  return await request.post({ url: '/rq/iot-model-template/create', data: data })
+}
+
+// 修改 设备属性模板
+export const updateModelTemplate = async (params: ModelAttrTemplateVO) => {
+  return await request.put({ url: '/rq/iot-model-template/update', data: params })
+}
+
+// 设备属性模板状态修改
+export const updateModelTemplateStatus = (id: number, status: number) => {
+  const data = {
+    id,
+    status
+  }
+  return request.put({ url: '/rq/iot-model-template/update-status', data: data })
+}
+
+// 删除 设备属性模板
+export const deleteModelTemplate = async (id: number) => {
+  return await request.delete({ url: '/rq/iot-model-template/delete?id=' + id })
+}

+ 22 - 1
src/router/modules/remaining.ts

@@ -92,6 +92,28 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/modelattrstemplate',
+    component: Layout,
+    name: 'ModelAttrsCenter',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'template/detail/:id',
+        component: () => import('@/views/pms/modeltemplate/detail/attrsModel/index.vue'),
+        name: 'ModelAttrTemplate',
+        meta: {
+          title: '填写信息详情',
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/template/info'
+        }
+      }
+    ]
+  },
   {
     path: '/iotpms/iotdevicepms', // 商品中心
     component: Layout,
@@ -338,7 +360,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
-
   {
     path: '/iotpms/iotmaintain',
     component: Layout,

+ 1 - 0
src/utils/dict.ts

@@ -266,4 +266,5 @@ export enum DICT_TYPE {
   // ========== PMS模块  ==========
   PMS_BOM_NODE_EXT_ATTR = 'BOM_NODE_EXT_ATTR', // BOM节点扩展属性 维护 or 保养
   PMS_MAIN_WORK_ORDER_TYPE = 'pms_main_work_order_type', // 保养工单类型
+  RQ_IOT_ISCOLLECTION = 'rq_iot_isCollection',//是否数采
 }

+ 69 - 54
src/views/pms/device/IotDeviceForm.vue

@@ -15,19 +15,20 @@
       </div>
       <div class="base-expandable-content" :class="{ 'is-expanded': baseIsExpanded }">
         <el-row>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="资产编码" prop="deviceCode">
               <el-input v-model="formData.deviceCode" placeholder="请输入资产编码" />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="设备名称" prop="deviceName">
               <el-input v-model="formData.deviceName" placeholder="请输入设备名称" />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="品牌" prop="brand">
               <el-select
+                clearable
                 v-model="formData.brand"
                 :model-value="brandLabel"
                 placeholder="请输入品牌"
@@ -35,19 +36,21 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="所在部门" prop="deptId">
               <el-tree-select
+                clearable
                 v-model="formData.deptId"
                 :data="deptList"
                 :props="defaultProps"
                 check-strictly
                 node-key="id"
+                filterable
                 placeholder="请选择所在部门"
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="设备类别" prop="assetClass">
               <el-tree-select
                 v-model="formData.assetClass"
@@ -57,12 +60,13 @@
                 node-key="id"
                 placeholder="请选择设备类别"
                 @change="assetclasschange"
+                filterable
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="设备状态" prop="deviceStatus">
-              <el-select v-model="formData.deviceStatus" placeholder="请选择">
+              <el-select v-model="formData.deviceStatus" placeholder="请选择" clearable>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
                   :key="dict.label"
@@ -72,9 +76,9 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="资产性质" prop="deviceStatus">
-              <el-select v-model="formData.assetProperty" placeholder="请选择">
+              <el-select v-model="formData.assetProperty" placeholder="请选择" clearable>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
                   :key="dict.id"
@@ -84,21 +88,24 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="6" style="display: flex;flex-direction: row">
-            <el-form-item label="规格型号" prop="model" style="width: 83%">
-              <el-input
-                v-model="formData.model"
-                placeholder="请输入规格型号"
-              />
-            </el-form-item>
-            <el-button type="info" @click="openModelForm">请选择</el-button>
+          <el-col :span="8" >
+            <div style="display: flex;flex-direction: row">
+              <el-form-item label="规格型号" prop="model" style="width: 85%">
+                <el-input
+                  clearable
+                  v-model="formData.model"
+                  placeholder="请输入规格型号"
+                />
+              </el-form-item>
+              <el-button type="info" @click="openModelForm">请选择</el-button>
+            </div>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="图片" prop="picUrl">
               <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="60px" />
             </el-form-item>
           </el-col>
-          <el-col :span="18">
+          <el-col :span="24">
             <el-form-item label="备注" prop="remark">
               <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
             </el-form-item>
@@ -119,17 +126,17 @@
       </div>
       <div class="zz-expandable-content" :class="{ 'is-expanded': zzIsExpanded }">
         <el-row>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="制造商" prop="manufacturerId">
               <el-select
                 v-model="formData.manufacturerId"
                 :model-value="zzLabel"
                 placeholder="请输入制造商"
-                @click="openCustomer"
+                @click="openCustomerZz"
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="生产日期" prop="manDate">
               <el-date-picker
                 style="width: 150%"
@@ -140,17 +147,17 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="供应商" prop="supplierId">
               <el-select
                 v-model="formData.supplierId"
                 placeholder="请输入供应商"
                 :model-value="supplierLabel"
-                @click="openCustomer"
+                @click="openCustomerSupplier"
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="质保到期" prop="expires">
               <el-date-picker
                 style="width: 150%"
@@ -161,7 +168,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="16">
             <el-form-item label="铭牌信息" prop="nameplate">
               <el-input v-model="formData.nameplate" type="textarea" placeholder="请输入铭牌信息" />
             </el-form-item>
@@ -182,7 +189,7 @@
       </div>
       <div class="cw-expandable-content" :class="{ 'is-expanded': cwIsExpanded }">
         <el-row>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'采购价格':'租赁价格'" prop="plPrice">
               <el-input
                 v-model="formData.plPrice"
@@ -191,7 +198,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'采购日期':'租赁日期'" prop="plDate">
               <el-date-picker
                 style="width: 150%"
@@ -202,12 +209,12 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'折旧年限':'租赁年限'" prop="plYear">
               <el-input v-model="formData.plYear" type="number" :placeholder="formData.assetProperty==='zy'?'请输入折旧年限':'请输入租赁年限'" />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'折旧开始日期':'租赁开始日期'" prop="plStartDate">
               <el-date-picker
                 style="width: 150%"
@@ -218,7 +225,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'已提折旧月数':'已租赁月数'" prop="plMonthed">
               <el-input
                 v-model="formData.plMonthed"
@@ -227,7 +234,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item :label="formData.assetProperty==='zy'?'已提折旧金额':'已租赁金额'" prop="plAmounted">
               <el-input
                 v-model="formData.plAmounted"
@@ -236,7 +243,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="6">
+          <el-col :span="8">
             <el-form-item label="剩余金额" prop="remainAmount">
               <el-input
                 v-model="formData.remainAmount"
@@ -261,7 +268,7 @@
       </div>
       <div class="qt-expandable-content" :class="{ 'is-expanded': qtIsExpanded }">
         <el-row>
-          <el-col v-for="field in list" :key="field.sort" :span="6">
+          <el-col v-for="field in list" :key="field.sort" :span="8">
             <el-form-item :label="field.name" :prop="field.code" :rules="field.rules">
               <!-- 文本输入 -->
               <el-input
@@ -326,7 +333,8 @@
   </ContentWrap>
   <BrandList ref="brandFormRef" @choose="brandChoose" />
   <ModelList ref="modelFormRef" @choose="modelChoose" :brand = "formData.brand" />
-  <CustomerList ref="customerFormRef" @choose="customerChoose" />
+  <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
+  <CustomerList ref="customerSupplierFormRef" @choose="customerSupplierChoose" />
 </template>
 <script setup lang="ts">
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
@@ -342,7 +350,7 @@ import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
 
 
 /** 设备台账 表单 */
-defineOptions({ name: 'IotDeviceForm' })
+defineOptions({ name: 'DeviceDetailAdd' })
 const baseIsExpanded = ref(true) // 控制表单是否展开的变量
 const zzIsExpanded = ref(true) // 控制表单是否展开的变量
 const cwIsExpanded = ref(true) // 控制表单是否展开的变量
@@ -353,6 +361,7 @@ const productClassifyList = ref<Tree[]>([]) // 树形结构
 const { delView } = useTagsViewStore() // 视图操作
 const { params, name } = useRoute() // 查询参数
 const { currentRoute, push } = useRouter()
+
 const id = params.id
 const isDetail = params.isDetail
 const { t } = useI18n() // 国际化
@@ -436,13 +445,13 @@ const brandChoose = (row) => {
 const modelChoose = (row) => {
   formData.value.model = row.name
 }
-const customerChoose = (row) => {
-  formData.value[row.customerNature + 'Id'] = row.id
-  if (row.customerNature == 'supplier') {
-    supplierLabel.value = row.name
-  } else {
-    zzLabel.value = row.name
-  }
+const customerSupplierChoose = (row) => {
+  formData.value.supplierId = row.id
+  supplierLabel.value = row.name
+}
+const customerZzChoose = (row) => {
+  formData.value.manufacturerId = row.id
+  zzLabel.value = row.name
 }
 /** 添加/修改操作 */
 const brandFormRef = ref()
@@ -453,9 +462,13 @@ const modelFormRef = ref()
 const openModelForm = () =>{
   modelFormRef.value.open()
 }
-const customerFormRef = ref()
-const openCustomer = () => {
-  customerFormRef.value.open()
+const customerSupplierFormRef = ref()
+const openCustomerSupplier = () => {
+  customerSupplierFormRef.value.open()
+}
+const customerZzFormRef = ref()
+const openCustomerZz = () => {
+  customerZzFormRef.value.open()
 }
 const allshouqi = () => {
   baseIsExpanded.value = false
@@ -495,12 +508,14 @@ const handleInput = (value, obj) => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({
-    name: 'IotDevicePms',
-    query: {
-      date: new Date().getTime()
-    }
-  })
+  push({ name: 'IotDevicePms', params:{}})
+  // delView(unref(currentRoute))
+  // push({
+  //   name: 'IotDevicePms',
+  //   query: {
+  //     date: new Date().getTime()
+  //   }
+  // })
 }
 const baseInfoClick = () => {
   baseIsExpanded.value = !baseIsExpanded.value // 切换展开状态
@@ -627,7 +642,7 @@ const resetForm = () => {
 }
 
 .base-expandable-content.is-expanded {
-  max-height: 200px; /* 或者根据内容设定一个合适的最大高度 */
+  min-height: 260px; /* 或者根据内容设定一个合适的最大高度 */
 }
 .zz-expandable-content {
   max-height: 0; /* 初始高度为0 */
@@ -636,7 +651,7 @@ const resetForm = () => {
 }
 
 .zz-expandable-content.is-expanded {
-  max-height: 200px; /* 或者根据内容设定一个合适的最大高度 */
+  min-height: 130px; /* 或者根据内容设定一个合适的最大高度 */
 }
 .cw-expandable-content {
   max-height: 0; /* 初始高度为0 */

+ 2 - 3
src/views/pms/failure/DeviceList.vue

@@ -117,7 +117,6 @@ const open = async (classify) => {
   dialogVisible.value = true
   queryParams.assetClass = classify
   selectedRow.value = ''
-  debugger
   queryParams.deviceName = undefined
   queryParams.deviceCode = undefined
   queryParams.pageNo = 1
@@ -128,8 +127,8 @@ const { wsCache } = useCache()
 const getList = async () => {
   loading.value = true
   try {
-    const user = wsCache.get(CACHE_KEY.USER)
-    queryParams.deptId = user.user.deptId
+    // const user = wsCache.get(CACHE_KEY.USER)
+    // queryParams.deptId = user.user.deptId
     const data = await IotDeviceApi.getIotDevicePage(queryParams)
     list.value = data.list
     total.value = data.total

+ 3 - 3
src/views/pms/failure/IotFailureReportForm.vue

@@ -20,7 +20,7 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="是否解决" prop="ifDeal">
-            <el-select v-model="formData.ifDeal" placeholder="请选择" @change="dealChange">
+            <el-select v-model="formData.ifDeal" placeholder="请选择" @change="dealChange" clearable>
               <el-option
                 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                 :key="dict.label"
@@ -32,7 +32,7 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="是否协助" prop="needHelp">
-            <el-select v-model="formData.needHelp" placeholder="请选择" @change="helpChange">
+            <el-select v-model="formData.needHelp" placeholder="请选择" @change="helpChange" clearable>
               <el-option
                 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                 :key="dict.label"
@@ -60,7 +60,7 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="是否停机" prop="ifStop">
-            <el-select v-model="formData.ifStop" placeholder="请选择">
+            <el-select v-model="formData.ifStop" placeholder="请选择" clearable>
               <el-option
                 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
                 :key="dict.label"

+ 7 - 7
src/views/pms/failure/index.vue

@@ -185,13 +185,13 @@
         <el-table-column label="图片" align="center" prop="pic" />
   <!--      <el-table-column label="解决办法" align="center" prop="solution" />-->
   <!--      <el-table-column label="备注" align="center" prop="remark" />-->
-        <el-table-column
-          label="创建时间"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-          width="180px"
-        />
+<!--        <el-table-column-->
+<!--          label="创建时间"-->
+<!--          align="center"-->
+<!--          prop="createTime"-->
+<!--          :formatter="dateFormatter"-->
+<!--          width="180px"-->
+<!--        />-->
         <el-table-column label="操作" align="center" min-width="120px">
           <template #default="scope">
 

+ 2 - 0
src/views/pms/inspect/item/IotInspectItemForm.vue

@@ -11,12 +11,14 @@
         <el-col :span="12">
         <el-form-item label="设备类别" prop="deviceClassify">
           <el-tree-select
+            filterable
             v-model="formData.deviceClassify"
             :data="productClassifyList"
             :props="defaultProps"
             check-strictly
             node-key="id"
             placeholder="请选择设备类别"
+            clearable
           />
         </el-form-item>
           </el-col>

+ 1 - 6
src/views/pms/inspect/order/InspectOrderDetail.vue

@@ -203,12 +203,7 @@ const openForm = () => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({
-    name: 'IotInspectOrder',
-    query: {
-      date: new Date().getTime()
-    }
-  })
+  push({ name: 'IotInspectOrder', params:{}})
 }
 const { wsCache } = useCache()
 /** 提交表单 */

+ 6 - 2
src/views/pms/inspect/order/WriteOrder.vue

@@ -118,13 +118,16 @@
 </template>
 
 <script setup>
-const { push } = useRouter() // 路由
+import {useTagsViewStore} from "@/store/modules/tagsView";
 
+defineOptions({ name: 'InspectOrderWrite' })
 const { t } = useI18n() // 国际化
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import {IotInspectOrderApi} from "@/api/pms/inspect/order";
 import {DICT_TYPE, getBoolDictOptions} from "@/utils/dict";
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter()
 const message = useMessage() // 消息弹窗
 
 const tabs = ref([])
@@ -227,7 +230,8 @@ const submitForm =  (tabIndex,current,type) => {
     IotInspectOrderApi.writeIotInspectOrder(formData[tabIndex][current], id)
     if (type === 'finish') {
       message.success(t('common.createSuccess'))
-      push({ name: 'IotInspectOrder' })
+      delView(unref(currentRoute))
+      push({ name: 'IotInspectOrder', params:{}})
     }
   } catch (error) {
     ElMessage.error('提交失败,请检查数据')

+ 15 - 8
src/views/pms/inspect/order/index.vue

@@ -62,14 +62,14 @@
         <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="['rq:iot-inspect-order:create']"
-          >
-            <Icon icon="ep:plus" class="mr-5px" /> 新增
-          </el-button>
+<!--          <el-button-->
+<!--            type="primary"-->
+<!--            plain-->
+<!--            @click="openForm('create')"-->
+<!--            v-hasPermi="['rq:iot-inspect-order:create']"-->
+<!--          >-->
+<!--            <Icon icon="ep:plus" class="mr-5px" /> 新增-->
+<!--          </el-button>-->
           <el-button
             type="success"
             plain
@@ -218,6 +218,13 @@ const openForm = (id?: number) => {
   push({ name: 'InspectOrderDetail', params:{id} })
 }
 const openWrite = (id?: number) => {
+  IotInspectOrderApi.getIotInspectOrder(id).then((res) => {
+    debugger
+    if (res.status==='finished') {
+      message.warning("该工单已执行");
+      return
+    }
+  })
   push({ name: 'InspectOrderWrite', params:{id} })
 }
 /** 删除按钮操作 */

+ 1 - 1
src/views/pms/inspect/plan/InspectRouteList.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="选择设备路线" style="width: 1300px; max-height: 800px">
+  <Dialog v-model="dialogVisible" title="选择设备路线" style="width: 1300px; min-height: 600px">
     <ContentWrap>
       <el-form
         class="-mb-15px"

+ 12 - 8
src/views/pms/inspect/plan/IotInspectPlan.vue

@@ -17,7 +17,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="单位" prop="planUnit">
-              <el-select v-model="formData.planUnit" placeholder="请选择单位" :disabled = "formType==='update'">
+              <el-select v-model="formData.planUnit" placeholder="请选择单位" :disabled = "formType==='update'" clearable>
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
                   :key="dict.label"
@@ -35,6 +35,8 @@
           <el-col :span="12">
             <el-form-item label="负责人" prop="charges">
               <el-select
+                clearable
+                filterable
                 v-model="formData.charges"
                 multiple
                 style="width: 100%"
@@ -147,7 +149,7 @@ import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
 import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
 import * as JobApi from '@/api/infra/job'
 /** 维修工单 表单 */
-defineOptions({ name: 'IotMaintainAe' })
+defineOptions({ name: 'InspectPlanAdd' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -208,12 +210,14 @@ const openForm = () => {
 
 const close = () => {
   delView(unref(currentRoute))
-  push({
-    name: 'IotInspectPlan',
-    query: {
-      date: new Date().getTime()
-    }
-  })
+  push({ name: 'IotInspectPlan', params:{}})
+  // delView(unref(currentRoute))
+  // push({
+  //   name: 'IotInspectPlan',
+  //   query: {
+  //     date: new Date().getTime()
+  //   }
+  // })
 }
 const { wsCache } = useCache()
 /** 提交表单 */

+ 3 - 3
src/views/pms/inspect/route/InspectItemList.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="选择巡检项" style="width: 1300px; max-height: 115%">
+  <Dialog v-model="dialogVisible" title="选择巡检项" style="width: 1300px; min-height: 115%">
     <ContentWrap>
       <el-form
         class="-mb-15px"
@@ -159,8 +159,8 @@ const { wsCache } = useCache()
 const getList = async () => {
   loading.value = true
   try {
-    const user = wsCache.get(CACHE_KEY.USER)
-    queryParams.deptId = user.user.deptId
+    // const user = wsCache.get(CACHE_KEY.USER)
+    // queryParams.deptId = user.user.deptId
     console.log(JSON.stringify(queryParams))
 
     const data = await IotInspectItemApi.getIotInspectItemPage(queryParams)

+ 3 - 1
src/views/pms/inspect/route/IotInspectRoute.vue

@@ -18,6 +18,8 @@
           <el-col :span="8">
             <el-form-item label="设备类别" prop="deviceClassify">
               <el-tree-select
+                filterable
+                clearable
                 v-model="formData.deviceClassify"
                 :data="productClassifyList"
                 :props="defaultProps"
@@ -122,7 +124,7 @@ import DeviceList from '@/views/pms/failure/DeviceList.vue'
 import {IotInspectRouteApi, IotInspectRouteVO} from "@/api/pms/inspect/route";
 
 /** 维修工单 表单 */
-defineOptions({ name: 'IotMaintainAe' })
+defineOptions({ name: 'RouteAdd' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

+ 85 - 47
src/views/pms/iotmainworkorder/WorkOrderMaterial.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="选择物料" style="width: 1100px; max-height: 800px">
+  <Dialog v-model="dialogVisible" title="选择物料" style="width: 1100px; min-height: 800px">
     <ContentWrap>
       <el-form
         class="-mb-15px"
@@ -27,7 +27,12 @@
         <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-button @click="handleConfirm" class="custom-green-button"
+            ><Icon icon="ep:check" class="mr-5px" /> 确认选择</el-button
+          >
+          <el-button @click="handleView" type="warning"
+            ><Icon icon="ep:plus" class="mr-5px" /> 新增物料</el-button
+          >
         </el-form-item>
       </el-form>
     </ContentWrap>
@@ -44,7 +49,7 @@
         <el-table-column width="60" label="选择">
           <template #default="{ row }">
             <el-checkbox
-              :model-value="selectedRows.some(item => item.materialCode === row.materialCode)"
+              :model-value="selectedRows.some((item) => item.materialCode === row.materialCode)"
               @click.stop="toggleRow(row)"
               class="no-label-radio"
             />
@@ -63,14 +68,16 @@
           prop="materialName"
           :show-overflow-tooltip="true"
         />
-        <el-table-column label="单位" align="center" prop="unit"  />
-        <el-table-column label="总库存数量" align="center" prop="totalInventoryQuantity"/>
-        <el-table-column label="库存类型" align="center" prop="materialSource"/>
-        <el-table-column label="消耗数量" align="center" prop="quantity" >
+        <el-table-column label="单位" align="center" prop="unit" />
+        <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"
-                      @click.stop=""
-                      @focus="scope.$el.querySelector('input').focus()"/>
+            <el-input
+              v-model="scope.row.quantity"
+              @click.stop=""
+              @focus="scope.$el.querySelector('input').focus()"
+            />
           </template>
         </el-table-column>
       </el-table>
@@ -83,16 +90,21 @@
       />
     </ContentWrap>
   </Dialog>
+  <MaintainMaterialDrawer
+    ref="showDrawer"
+    :model-value="drawerVisible"
+    @update:model-value="(val) => (drawerVisible = val)"
+    @add="handleChildSubmit"
+  />
 </template>
 
 <script setup lang="ts">
-import * as MaintainMaterialApi from '@/api/pms/maintain/material'
-import * as WorkOrderBomMaterialApi from '@/api/pms/iotmainworkorderbommaterial'
 import { IotMaintainMaterialVO } from '@/api/pms/maintain/material'
-import { propTypes } from '@/utils/propTypes'
-import { defineExpose } from 'vue';
-import {IotDeviceVO} from "@/api/pms/device";
+import * as WorkOrderBomMaterialApi from '@/api/pms/iotmainworkorderbommaterial'
+import { defineExpose, ref } from 'vue'
+import MaintainMaterialDrawer from '@/views/pms/maintain/material/MaintainMaterialDrawer.vue'
 
+const message = useMessage() // 消息弹窗
 // const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
 // 调整 emit 类型
 const emit = defineEmits<{
@@ -104,8 +116,8 @@ const loading = ref(true) // 列表的加载中
 const queryFormRef = ref() // 搜索的表单
 const list = ref<WorkOrderBomMaterialApi.IotMainWorkOrderBomMaterialVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const tableRef = ref();
-const selectedRows = ref<WorkOrderBomMaterialApi.IotMainWorkOrderBomMaterialVO[]>([]); // 多选数据(存储所有选中行的数组)
+const tableRef = ref()
+const selectedRows = ref<WorkOrderBomMaterialApi.IotMainWorkOrderBomMaterialVO[]>([]) // 多选数据(存储所有选中行的数组)
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -114,25 +126,43 @@ const queryParams = reactive({
   name: '',
   code: ''
 })
-
-const selectedRow = ref(null)
-
-// 处理单选逻辑
-const selectRow = (row) => {
-  selectedRow.value = selectedRow.value?.id === row.id ? null : row
-  emit('choose', row)
-  dialogVisible.value = false
+const drawerVisible = ref<boolean>(false)
+const showDrawer = ref()
+const handleView = () => {
+  drawerVisible.value = true
+  showDrawer.value.openDrawer()
+}
+const addMateriall = ref(null)
+const handleChildSubmit = (formData) => {
+  const modified = removeOnesFromKeys(formData)
+  modified.materialName = modified.name;
+  modified.materialCode = modified.code;
+  modified.materialSource = '手动添加';
+  addMateriall.value = modified;
+  list.value.unshift(modified)
+  total.value = total.value+1
+  debugger
+}
+const removeOnesFromKeys = (obj: Record<string, any>) => {
+  return Object.keys(obj).reduce(
+    (acc, key) => {
+      const newKey = key.replace(/1/g, '') // 替换所有 1
+      acc[newKey] = obj[key]
+      return acc
+    },
+    {} as Record<string, any>
+  )
 }
-
 // 点击整行选中
 const handleRowClick = (row) => {
   toggleRow(row)
 }
 const open = async (deptId: number, bomNodeId: number) => {
   console.log('传递过来的数据:', deptId)
+  selectedRows.value = []
   dialogVisible.value = true
-  queryParams.deptId = deptId;
-  queryParams.bomNodeId = bomNodeId;
+  queryParams.deptId = deptId
+  queryParams.bomNodeId = bomNodeId
   await getList()
 }
 
@@ -157,26 +187,35 @@ const handleConfirm = () => {
     ElMessage.warning('请至少选择一个物料')
     return
   }
-  emit('choose', selectedRows.value.map(row => ({
-    ...row,
-    // 确保返回必要字段
-    id: row.id,
-    materialCode: row.materialCode,
-    materialName: row.materialName,
-    unitPrice: row.unitPrice,
-    unit: row.unit,
-    quantity: row.quantity,
-  })))
-  dialogVisible.value = false;
+  const filters = selectedRows.value.filter((item) => item.quantity === null||item.quantity === undefined)
+  debugger
+  if (filters.length > 0) {
+    message.error('消耗数量必填')
+    return
+  }
+  emit(
+    'choose',
+    selectedRows.value.map((row) => ({
+      ...row,
+      // 确保返回必要字段
+      id: row.id,
+      materialCode: row.materialCode,
+      materialName: row.materialName,
+      unitPrice: row.unitPrice,
+      unit: row.unit,
+      quantity: row.quantity
+    }))
+  )
+  dialogVisible.value = false
   handleClose()
-};
+}
 
 // 关闭时清空选择
 const handleClose = () => {
-  tableRef.value?.clearSelection();
+  tableRef.value?.clearSelection()
   selectedRows.value = []
   emit('close')
-};
+}
 
 const rowClassName = ({ row }: { row: any }) => {
   let className = '';
@@ -194,13 +233,13 @@ const rowClassName = ({ row }: { row: any }) => {
 
 // 多选 切换行选中状态
 const toggleRow = (row) => {
-  const index = selectedRows.value.findIndex(item => item.materialCode === row.materialCode);
+  const index = selectedRows.value.findIndex((item) => item.materialCode === row.materialCode)
   if (index > -1) {
-    selectedRows.value.splice(index, 1); // 取消选中
+    selectedRows.value.splice(index, 1) // 取消选中
   } else {
-    selectedRows.value.push(row); // 选中
+    selectedRows.value.push(row) // 选中
   }
-};
+}
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
@@ -225,7 +264,6 @@ const resetQuery = () => {
   margin-right: 0;
 }
 
-
 /* 自定义淡绿色按钮 */
 :deep(.custom-green-button) {
   background-color: #e1f3d8;

+ 178 - 0
src/views/pms/iotopeationfill/IotOpeationFillForm.vue

@@ -0,0 +1,178 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="资产编号" prop="deviceCode">
+            <el-input v-model="formData.deviceCode" placeholder="请输入资产编号" disabled/>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备名称" prop="deviceName">
+            <el-input v-model="formData.deviceName" placeholder="请输入设备名称" disabled/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="设备类别" prop="deviceType">
+            <el-select v-model="formData.deviceType" placeholder="请选择设备类别" disabled>
+              <el-option label="请选择字典生成" value="" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="所属组织" prop="orgName">
+            <el-input v-model="formData.orgName" placeholder="请输入所属组织" disabled/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="设备部件" prop="deviceComponent">
+        <el-input v-model="formData.deviceComponent" placeholder="请输入设备部件" disabled/>
+      </el-form-item>
+      <div v-for="(item,index) in arry1" :key="index">
+        <p>填写信息:{{item.label}}</p>
+        <el-input placeholder="请输入" v-model="item.value" @input="arry1[index].text = $event.target.value"/>
+      </div>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
+import {forEach} from "min-dash";
+
+/** 运行记录填报 表单 */
+defineOptions({ name: 'IotOpeationFillForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  deviceCode: undefined,
+  deviceName: undefined,
+  fillContent: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  deptId: undefined,
+  orgName: undefined,
+  proId: undefined,
+  proName: undefined,
+  teamId: undefined,
+  teamName: undefined,
+  dutyName: undefined,
+  creDate: undefined
+})
+const formRules = reactive({
+  deviceCode: [{ required: true, message: '资产编号不能为空', trigger: 'blur' }],
+  deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+  fillContent: [{ required: true, message: '填写内容不能为空', trigger: 'blur' }],
+  deviceType: [{ required: true, message: '设备类别不能为空', trigger: 'change' }],
+  deviceComponent: [{ required: true, message: '设备部件不能为空', trigger: 'blur' }],
+  orgName: [{ required: true, message: '所属组织不能为空', trigger: 'blur' }],
+  proName: [{ required: true, message: '所属项目部不能为空', trigger: 'blur' }],
+  teamName: [{ required: true, message: '所属小队不能为空', trigger: 'blur' }],
+  dutyName: [{ required: true, message: '设备负责人不能为空', trigger: 'blur' }],
+  creDate: [{ required: true, message: '填写日期不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+let itemArry = [];
+let arry = [];
+let arry1 =[];
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotOpeationFillApi.getIotOpeationFill(id)
+      if(formData.value.fillContent.includes(":")){
+        arry = formData.value.fillContent.split(",");
+        arry.forEach(function (item, index) {
+          arry1.push({label:item.split(":")[0],value:item.split(":")[1]})
+        });
+      }else{
+        arry = formData.value.fillContent.split(",");
+        arry.forEach(function (item, index) {
+          arry1.push({label:item,value:''})
+        });
+      }
+
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  formData.value.fillContent = '';
+  arry1.forEach(function (item, index) {
+    formData.value.fillContent += item.label+":"+item.value+",";
+  })
+  formData.value.fillContent = formData.value.fillContent.slice(0,-1);
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IotOpeationFillVO
+    if (formType.value === 'create') {
+      await IotOpeationFillApi.createIotOpeationFill(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotOpeationFillApi.updateIotOpeationFill(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceCode: undefined,
+    deviceName: undefined,
+    fillContent: undefined,
+    deviceType: undefined,
+    deviceComponent: undefined,
+    deptId: undefined,
+    orgName: undefined,
+    proId: undefined,
+    proName: undefined,
+    teamId: undefined,
+    teamName: undefined,
+    dutyName: undefined,
+    creDate: undefined,
+  }
+  formRef.value?.resetFields()
+  arry1.length=0
+}
+</script>

+ 247 - 0
src/views/pms/iotopeationfill/index.vue

@@ -0,0 +1,247 @@
+<template>
+  <el-row :gutter="20">
+    <el-col :span="4" :xs="24">
+      <el-card class="box-card" v-for="(item,index) in arry1" :key="index">
+        <template #header>
+          <div class="card-header">
+            <span>Card name</span>
+            <el-button class="button" text>Operation button</el-button>
+          </div>
+        </template>
+        <div v-for="o in 4" :key="o" class="text item">{{ 'List item ' + o }}</div>
+      </el-card>
+    </el-col>
+    <el-col :span="16" :xs="24">
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="资产编号" prop="deviceCode">
+            <el-input
+              v-model="queryParams.deviceCode"
+              placeholder="请输入资产编号"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="填写日期" prop="creDate">
+            <el-date-picker
+              v-model="queryParams.creDate"
+              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="['rq:iot-opeation-fill:create']"
+            >
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>-->
+            <el-button
+              type="success"
+              plain
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['rq:iot-opeation-fill: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="deviceCode" />
+          <el-table-column label="设备名称" align="center" prop="deviceName" />
+          <el-table-column label="填写内容" align="center" prop="fillContent" />
+          <el-table-column label="所属组织" align="center" prop="orgName" />
+<!--          <el-table-column label="项目部" align="center" prop="proName" />
+          <el-table-column label="小队" align="center" prop="teamName" />-->
+          <el-table-column label="负责人" align="center" prop="dutyName" />
+          <el-table-column
+            label="创建日期"
+            align="center"
+            prop="creDate"
+            :formatter="dateFormatter2"
+            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="['rq:iot-opeation-fill:update']"
+              >
+                编辑
+              </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="handleDelete(scope.row.id)"
+                v-hasPermi="['rq:iot-opeation-fill: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>
+
+    </el-col>
+
+    <!-- 表单弹窗:添加/修改 -->
+    <IotOpeationFillForm ref="formRef" @success="getList" :dept="deptInfo"/>
+  </el-row>
+
+</template>
+
+<script setup lang="ts">
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
+import IotOpeationFillForm from './IotOpeationFillForm.vue'
+import Vue from "@vitejs/plugin-vue";
+/** 运行记录填报 列表 */
+defineOptions({ name: 'IotOpeationFill' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotOpeationFillVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+let arry1 =[];
+const  cards= [
+    { title: '卡片 1', content: '这是卡片 1 的内容' },
+    { title: '卡片 2', content: '这是卡片 2 的内容' },
+    { title: '卡片 3', content: '这是卡片 3 的内容' }
+  ]
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceCode: undefined,
+  deviceName: undefined,
+  fillContent: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  deptId: undefined,
+  orgName: undefined,
+  proId: undefined,
+  proName: undefined,
+  teamId: undefined,
+  teamName: undefined,
+  dutyName: undefined,
+  creDate: [],
+  createTime: [],
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+let deptInfo = {deptId:'',orgName:''};
+/** 处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  queryParams.orgName = row.name;
+  deptInfo.orgName = queryParams.orgName;
+  deptInfo.deptId = queryParams.deptId;
+  await getList()
+}
+
+const formatDescription = async(row, column, cellValue) =>{
+  return cellValue.split(',').map(part => `<div>${part}</div>`).join('');
+}
+
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotOpeationFillApi.getIotOpeationFillPage(queryParams)
+    list.value = data;
+    list.value.forEach(function (item, index) {
+      arry1.push({deviceName:item.deviceName,deviceCode:item.deviceCode})
+    });
+    alert(JSON.stringify(arry1))
+    alert(Array.isArray(arry1));
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+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)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotOpeationFillApi.deleteIotOpeationFill(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotOpeationFillApi.exportIotOpeationFill(queryParams)
+    download.excel(data, '运行记录填报.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+
+})
+</script>

+ 263 - 0
src/views/pms/iotopeationmodel/IotOpeationModelForm.vue

@@ -0,0 +1,263 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+<!--      <el-form-item label="部门id" prop="deptId">
+        <el-input v-model="formData.deptId" placeholder="请输入部门id" />
+      </el-form-item>
+      <el-form-item label="所属组织" prop="orgName">
+        <el-input v-model="formData.orgName" placeholder="请输入所属组织" />
+      </el-form-item>-->
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="设备类别" prop="deviceType">
+            <el-tree-select
+              v-model="formData.deviceType"
+              :data="productClassifyList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              placeholder="请选择设备类别"
+              @change="assetclasschange"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备部件" prop="deviceComponent">
+            <el-input v-model="formData.deviceComponent" placeholder="请输入设备部件" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row>
+        <el-col :span="12">
+          <div v-for="(dynamicItem, index) in dynamicItems" :key="index">
+            <el-form-item label="填写信息" prop="fillInfo" :for="`dynamic-${index}`" >
+              <el-input v-model="dynamicItems[index]" :id="`dynamic-${index}`" placeholder="请输入填写信息"/>
+            </el-form-item>
+
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div style="margin-left:30px">
+            <el-button type="success" @click="addDynamicItem">添加</el-button>
+            <el-button type="danger" @click="removeDynamicItem(index)">移除</el-button>
+          </div>
+        </el-col>
+      </el-row>
+
+
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotOpeationModelApi, IotOpeationModelVO } from '@/api/pms/iotopeationmodel'
+import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
+import {defaultProps,handleTree} from "@/utils/tree";
+import * as DeptApi from "@/api/system/dept";
+import * as ProductClassifyApi from "@/api/pms/productclassify";
+import {IotDeviceApi} from "@/api/pms/device";
+import {object} from "vue-types";
+import {userInfo} from "os";
+import {UserInfo} from "@/layout/components/UserInfo";
+
+
+
+/** 运行记录模板主 表单 */
+defineOptions({ name: 'IotOpeationModelForm' })
+const deptInfo = defineProps(['dept'])
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const list = ref([])
+
+const deptList = ref<Tree[]>([]) // 树形结构
+const productClassifyList = ref<Tree[]>([]) // 树形结构
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const brandLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
+
+
+import { ref } from 'vue';
+
+
+
+
+
+
+
+
+const fixedItems = ref([
+  { label: '设备类别', value: '' },
+  { label: '设备部件', value: '' }
+]);
+
+const comp = ref([''])//初始化部件数组
+
+function addComp() {
+  comp.value.push(''); // 添加一个空字符串作为新的动态项输入框的初始值
+}
+
+function removeComp(index) {
+  comp.value.splice(index, 1); // 移除指定索引的动态项
+}
+
+const dynamicItems = ref(['']); // 初始动态项数组
+
+function addDynamicItem() {
+  dynamicItems.value.push(''); // 添加一个空字符串作为新的动态项输入框的初始值
+}
+
+function removeDynamicItem(index) {
+    dynamicItems.value.splice(index, 1); // 移除指定索引的动态项
+}
+
+
+const formData = ref({
+  id: undefined,
+  deptId: undefined,
+  orgName: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  fillInfo: undefined,
+  creName: undefined,
+  creDate: undefined,
+})
+const formRules = reactive({
+  orgName: [{ required: true, message: '所属组织不能为空', trigger: 'blur' }],
+  deviceType: [{ required: true, message: '设备类别不能为空', trigger: 'change' }],
+  deviceComponent: [{ required: true, message: '设备部件不能为空', trigger: 'blur' }],
+  fillInfo: [{ required: true, message: '填写信息不能为空', trigger: 'blur' }],
+  creName: [{ required: true, message: '创建人不能为空', trigger: 'blur' }],
+  creDate: [{ required: true, message: '创建日期不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  productClassifyList.value = handleTree(
+    await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
+  )
+  formData.value.assetProperty = 'zy'
+  // 修改时,设置数据
+  if (id) {
+    formType.value = 'update';
+    formLoading.value = true
+    try {
+      const iotDevice = await IotDeviceApi.getIotDevice(id);
+      formData.value = iotDevice
+      brandLabel.value = iotDevice.brandName;
+      zzLabel.value = iotDevice.zzName;
+      supplierLabel.value = iotDevice.supplierName;
+      list.value = JSON.parse(iotDevice.templateJson);
+      list.value.forEach((item) => {
+        formData.value[item.identifier] = item.value;
+      })
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    formType.value = 'create';
+  }
+})
+
+const assetclasschange = () => {
+  const deviceType = formData.value.deviceType
+  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(deviceType).then(res => {
+    if (res){
+      res.forEach((item) => {
+        if (item.requiredFlag) {
+          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
+          item.rules = []
+          item.rules.push(rule)
+        }
+      })
+      list.value = res
+      debugger
+    } else {
+      list.value = []
+    }
+  })
+}
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotOpeationModelApi.getIotOpeationModel(id)
+      dynamicItems.value = formData.value.fillInfo.split(",")
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
+const arry = []
+
+const submitForm = async () => {
+  // 方法1:使用JavaScript的Date对象
+  formData.value.creName = "test";
+  if(formData.value.deptId==undefined){
+    formData.value.deptId = deptInfo.dept.deptId;
+    formData.value.orgName = deptInfo.dept.orgName;
+  }
+  formData.value.fillInfo = dynamicItems.value.join(",");
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IotOpeationModelVO
+    if (formType.value === 'create') {
+      alert(JSON.stringify(data));
+      await IotOpeationModelApi.createIotOpeationModel(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotOpeationModelApi.updateIotOpeationModel(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deptId: undefined,
+    orgName: undefined,
+    deviceType: undefined,
+    deviceComponent: undefined,
+    fillInfo: undefined,
+    creName: undefined,
+    creDate: undefined,
+  }
+  formRef.value?.resetFields()
+  dynamicItems.value = [''];
+}
+</script>

+ 306 - 0
src/views/pms/iotopeationmodel/index.vue

@@ -0,0 +1,306 @@
+<template>
+
+  <el-row :gutter="20">
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="20" :xs="24">
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="设备类别" prop="deviceType">
+            <el-tree-select
+              v-model="formData.deviceType"
+              :data="productClassifyList"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              placeholder="请选择设备类别"
+              @change="assetclasschange"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="设备部件" prop="deviceComponent">
+            <el-input
+              v-model="queryParams.deviceComponent"
+              placeholder="请输入设备部件"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="创建人" prop="creName">
+            <el-input
+              v-model="queryParams.creName"
+              placeholder="请输入创建人"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="创建日期" prop="creDate">
+            <el-date-picker
+              v-model="queryParams.creDate"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="date"
+              start-placeholder="请选择"
+              :default-time="[new Date('1 00:00:00')]"
+              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
+              type="primary"
+              plain
+              @click="openForm('create')"
+              v-hasPermi="['rq:iot-operation-model:create']"
+            >
+              <Icon icon="ep:plus" class="mr-5px" /> 新增
+            </el-button>
+            <el-button
+              type="success"
+              plain
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['rq:iot-operation-model: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="orgName" />
+          <el-table-column label="设备类别" align="center" prop="deviceType" />
+          <el-table-column label="设备部件" align="center" prop="deviceComponent" />
+          <el-table-column label="填写信息" align="center" prop="fillInfo" />
+          <el-table-column label="创建人" align="center" prop="creName" />
+          <el-table-column
+            label="创建日期"
+            align="center"
+            prop="creDate"
+            :formatter="dateFormatter2"
+            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="['rq:iot-operation-model:update']"
+              >
+                编辑
+              </el-button>
+              <el-button
+                link
+                type="danger"
+                @click="handleDelete(scope.row.id)"
+                v-hasPermi="['rq:iot-operation-model: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>
+    </el-col>
+
+
+    <!-- 表单弹窗:添加/修改 -->
+    <IotOpeationModelForm ref="formRef" @success="getList" :dept="deptInfo"/>
+  </el-row>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter2 } from '@/utils/formatTime'
+import DeptTree from "@/views/system/user/DeptTree.vue";
+import download from '@/utils/download'
+import {defaultProps,handleTree} from "@/utils/tree";
+import { IotOpeationModelApi, IotOpeationModelVO } from '@/api/pms/iotopeationmodel'
+import IotOpeationModelForm from './IotOpeationModelForm.vue'
+import * as DeptApi from "@/api/system/dept";
+import * as ProductClassifyApi from "@/api/pms/productclassify";
+import {IotDeviceApi} from "@/api/pms/device";
+import {DeviceAttrModelApi} from "@/api/pms/deviceattrmodel";
+
+/** 运行记录模板主 列表 */
+defineOptions({ name: 'IotOpeationModel' })
+
+const message = useMessage() // 消息弹窗
+
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotOpeationModelVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const { push } = useRouter() // 路由跳转
+const deptList = ref<Tree[]>([]) // 树形结构
+const productClassifyList = ref<Tree[]>([]) // 树形结构
+const { params, name } = useRoute() // 查询参数
+const id = params.id
+const brandLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: undefined,
+  orgName: undefined,
+  deviceType: undefined,
+  deviceComponent: undefined,
+  fillInfo: undefined,
+  creName: undefined,
+  creDate: [],
+})
+let deptInfo = {deptId:'',orgName:''};
+const formData = ref({
+  deviceType: undefined
+})
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotOpeationModelApi.getIotOpeationModelPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+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)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotOpeationModelApi.deleteIotOpeationModel(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotOpeationModelApi.exportIotOpeationModel(queryParams)
+    download.excel(data, '运行记录模板主.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+const assetclasschange = () => {
+  const assetClass = formData.value.deviceType
+  DeviceAttrModelApi.getDeviceAttrModelListByDeviceCategoryId(assetClass).then(res => {
+    if (res){
+      res.forEach((item) => {
+        if (item.requiredFlag) {
+          const rule = {required: true, message: item.name+'不能为空', trigger: 'blur'}
+          item.rules = []
+          item.rules.push(rule)
+        }
+      })
+      list.value = res
+      debugger
+    } else {
+      list.value = []
+    }
+  })
+}
+
+onMounted(async () => {
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  productClassifyList.value = handleTree(
+    await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
+  )
+  formData.value.assetProperty = 'zy'
+  // 修改时,设置数据
+  if (id) {
+    formType.value = 'update';
+    formLoading.value = true
+    try {
+      const iotDevice = await IotDeviceApi.getIotDevice(id);
+      formData.value = iotDevice
+      brandLabel.value = iotDevice.brandName;
+      zzLabel.value = iotDevice.zzName;
+      supplierLabel.value = iotDevice.supplierName;
+      list.value = JSON.parse(iotDevice.templateJson);
+      list.value.forEach((item) => {
+        formData.value[item.identifier] = item.value;
+      })
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    formType.value = 'create';
+  }
+})
+/** 处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  queryParams.orgName = row.name;
+  deptInfo.orgName = queryParams.orgName;
+  deptInfo.deptId = queryParams.deptId;
+  await getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 149 - 64
src/views/pms/maintain/IotMaintain.vue

@@ -13,7 +13,7 @@
           <el-col :span="8">
             <el-form-item label="设备" prop="deviceName">
               <el-select
-                :disabled="formType==='update'"
+                :disabled="formType === 'update'"
                 v-model="formData.deviceName"
                 :model-value="deviceLabel"
                 placeholder="请选择设备"
@@ -128,7 +128,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'" >
+          <el-col :span="8" v-if="formData.type === 'out'">
             <el-form-item label="委外相关附件" prop="outFile">
               <UploadFile v-model="formData.outFile" class="min-w-80px" />
             </el-form-item>
@@ -162,12 +162,16 @@
       <!-- 搜索工作栏 -->
       <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
         <el-form-item>
-          <el-button @click="openMaterialForm" type="primary"
-            ><Icon icon="ep:plus" class="mr-5px" /> 选择物料</el-button
+          <!--          <el-button @click="openMaterialForm" type="primary"-->
+          <!--            ><Icon icon="ep:plus" class="mr-5px" /> 选择物料</el-button>-->
+          <el-button @click="openMaintainForm" type="primary"
+            ><Icon icon="ep:plus" class="mr-5px" /> 选择维修项</el-button
           >
-          <el-button @click="handleView" type="warning"
-            ><Icon icon="ep:plus" class="mr-5px" /> 新增物料</el-button
+          <el-button @click="openMaintainItem" type="warning"
+            ><Icon icon="ep:plus" class="mr-5px" /> 新增维修项</el-button
           >
+          <!--          <el-button @click="handleViewNew" type="warning"-->
+          <!--            ><Icon icon="ep:plus" class="mr-5px" /> 新增物料</el-button>-->
         </el-form-item>
       </el-form>
     </ContentWrap>
@@ -175,26 +179,10 @@
     <!-- 列表 -->
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column label="物料编码" align="center" prop="code" />
-        <el-table-column label="物料名称" align="center" prop="name" />
-        <el-table-column label="单位" align="center" prop="unit" />
-        <el-table-column label="单价" align="center" prop="price">
-          <template #default="{ row }">
-            {{ row.price }}
-          </template>
-        </el-table-column>
-        <el-table-column label="消耗数量" align="center" prop="depleteCount">
-          <template #default="scope">
-            <el-input-number :precision="2" :step="0.1" v-model="scope.row.depleteCount" />
-          </template>
-        </el-table-column>
-        <el-table-column label="总库存数量" align="center" prop="totalCount" />
-        <el-table-column label="备注" align="center" prop="remark">
-          <template #default="scope">
-            <el-input v-model="scope.row.remark" />
-          </template>
-        </el-table-column>
-        <el-table-column label="物料来源" align="center" prop="sourceType" />
+        <el-table-column label="资产编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="维修项" align="center" prop="name" />
+
         <el-table-column label="操作" align="center" min-width="120px">
           <template #default="scope">
             <div style="display: flex; justify-content: center; align-items: center; width: 100%">
@@ -209,6 +197,16 @@
                   移除
                 </el-button>
               </div>
+              <div style="margin-left: 12px">
+                <el-button link type="primary" @click="openMaterialForm(scope.row)">
+                  选择物料
+                </el-button>
+              </div>
+              <div style="margin-left: 12px">
+                <el-button link type="primary" @click="handleViewNew(scope.row.bomNodeId)">
+                  物料详情
+                </el-button>
+              </div>
             </div>
           </template>
         </el-table-column>
@@ -216,13 +214,8 @@
     </ContentWrap>
 
     <!-- 表单弹窗:添加/修改 -->
-    <ChooseMaterial ref="materialFormRef" :deptId="formData.deptId" @choose="selectChoose" />
-    <MaintainMaterialDrawer
-      ref="showDrawer"
-      :model-value="drawerVisible"
-      @update:model-value="(val) => (drawerVisible = val)"
-      @add="handleChildSubmit"
-    />
+    <WorkOrderMaterial ref="materialFormRef" @choose="selectChoose" />
+    <ChooseMaintain ref="maintainFormRef" @choose="maintainChoose" />
   </ContentWrap>
   <ContentWrap>
     <el-form>
@@ -234,6 +227,40 @@
   </ContentWrap>
   <DeviceList ref="deviceFormRef" @choose="deviceChoose" />
   <CustomerList ref="customerFormRef" @choose="customerChoose" />
+  <MaterialListDrawer
+    :model-value="drawerVisible"
+    @update:model-value="(val) => (drawerVisible = val)"
+    :node-id="currentBomNodeId"
+    :materials="materialList.filter((item) => item.bomNodeId === currentBomNodeId)"
+  />
+  <Dialog title="维修项" v-model="addItemVisible" style="min-height: 300px">
+    <el-form
+      ref="addFormRef"
+      :model="addFormData"
+      :rules="addFormRules"
+      label-position="right"
+      label-width="68px"
+    >
+      <el-form-item label="BOM节点id" v-if="false" prop="deviceCode">
+        <el-input v-model="addFormData.bomNodeId" disabled />
+      </el-form-item>
+      <el-form-item label="资产编码" prop="deviceCode">
+        <el-input v-model="addFormData.deviceCode" disabled />
+      </el-form-item>
+      <el-form-item label="设备名称" prop="deviceName">
+        <el-input v-model="addFormData.deviceName" disabled />
+      </el-form-item>
+      <el-form-item label="维修项" prop="name">
+        <el-input v-model="addFormData.name" placeholder="请输入维修项" />
+      </el-form-item>
+    </el-form>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button @click="handleConfirm" type="primary">确 定</el-button>
+        <el-button @click="closeDialog">取 消</el-button>
+      </el-form-item>
+    </el-form>
+  </Dialog>
 </template>
 <script setup lang="ts">
 import { IotMaintainApi } from '@/api/pms/maintain'
@@ -241,17 +268,18 @@ import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
 import DeviceList from '@/views/pms/failure/DeviceList.vue'
 import * as UserApi from '@/api/system/user'
 import { useUserStore } from '@/store/modules/user'
-import ChooseMaterial from '@/views/pms/maintain/material/ChooseMaterial.vue'
-import MaintainMaterialDrawer from '@/views/pms/maintain/material/MaintainMaterialDrawer.vue'
 import { ref } from 'vue'
 import { IotMaintainMaterialVO } from '@/api/pms/maintain/material'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import CustomerList from '@/views/pms/device/CustomerList.vue'
-
+import WorkOrderMaterial from '@/views/pms/iotmainworkorder/WorkOrderMaterial.vue'
+import { IotMainWorkOrderBomMaterialVO } from '@/api/pms/iotmainworkorderbommaterial'
+import MaterialListDrawer from '@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue'
+import ChooseMaintain from "@/views/pms/maintain/material/ChooseMaintain.vue";
 /** 维修工单 表单 */
-defineOptions({ name: 'IotMaintainAe' })
-
+defineOptions({ name: 'MaintainAdd' })
+const addItemVisible = ref(false) // 弹窗的是否展示
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const { delView } = useTagsViewStore() // 视图操作
@@ -267,6 +295,12 @@ const list = ref<IotMaintainMaterialVO[]>([]) // 列表的数据
 const { params, name } = useRoute() // 查询参数
 const id = params.id
 const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const addFormData = ref({
+  name: '',
+  deviceName: '',
+  deviceCode: '',
+  bomNodeId: ''
+})
 const formData = ref({
   id: undefined,
   failureCode: undefined,
@@ -297,11 +331,11 @@ const formData = ref({
 const strictRules = {
   required: true,
   message: '请输入维修费用'
-};
+}
 const supplierRules = {
   required: true,
   message: '请选择供应商'
-};
+}
 const formRules = reactive({
   deviceName: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
   failureCode: [{ required: true, message: '故障编码不能为空', trigger: 'blur' }],
@@ -317,6 +351,9 @@ const formRules = reactive({
   // status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
 })
 
+const addFormRules = reactive({
+  name: [{ required: true, message: '维修项不能为空', trigger: 'blur' }]
+})
 const outFormRules = reactive({
   deviceName: [{ required: true, message: '设备不能为空', trigger: 'blur' }],
   failureCode: [{ required: true, message: '故障编码不能为空', trigger: 'blur' }],
@@ -332,6 +369,11 @@ const outFormRules = reactive({
   supplier: [{ required: true, message: '供应商不能为空', trigger: 'blur' }]
 })
 
+const handleConfirm = () => {
+  addFormData.value.bomNodeId = Math.floor(100000 + Math.random() * 900000);
+  list.value.push(addFormData.value)
+  addItemVisible.value = false
+}
 const customerChoose = (row) => {
   formData.value.supplier = row.id
   supplierLabel.value = row.name
@@ -342,11 +384,14 @@ const openCustomer = (type) => {
   customerFormRef.value.open(type)
 }
 const formRef = ref() // 表单 Ref
+const addFormRef = ref()
 const deviceChoose = (row) => {
   formData.value.deviceId = row.id
   formData.value.deviceName = row.deviceName
+  formData.value.deviceCode = row.deviceCode
   formData.value.deptId = row.deptId
   deviceLabel.value = row.deviceName
+  list.value = []
 }
 const deviceFormRef = ref()
 const openForm = () => {
@@ -354,12 +399,26 @@ const openForm = () => {
 }
 
 const materialFormRef = ref()
-const openMaterialForm = (type: string, id?: number) => {
-  if (!formData.value.deptId) {
+const currentBomNodeId = ref() // 当前选中的bom节点
+const openMaterialForm = (row: any) => {
+  bomNodeId.value = row.bomNodeId
+  console.log('这是一个对象:', row.bomNodeId)
+  materialFormRef.value.open(formData.value.deptId, bomNodeId.value)
+}
+
+const maintainFormRef = ref()
+const openMaintainForm = (type: string, id?: number) => {
+  if (!formData.value.deviceId) {
     message.error('请选择设备')
     return
   }
-  materialFormRef.value.open(type, id)
+  maintainFormRef.value.open(type, formData.value.deviceId)
+}
+const openMaintainItem = () => {
+  addItemVisible.value = true
+  addFormData.value.deviceName = formData.value.deviceName
+  addFormData.value.deviceCode = formData.value.deviceCode
+  addFormData.value.name = ''
 }
 const endTimeBlur = () => {
   if (formData.value.maintainEndTime <= formData.value.maintainStartTime) {
@@ -369,24 +428,58 @@ const endTimeBlur = () => {
 }
 const close = () => {
   delView(unref(currentRoute))
-  push({
-    name: 'IotMaintain',
-    query: {
-      date: new Date().getTime()
-    }
-  })
+  push({ name: 'IotMaintain', params: {} })
 }
-const handleView = () => {
+const closeDialog = () => {
+  addItemVisible.value = false
+}
+
+const handleViewNew = (nodeId) => {
+  currentBomNodeId.value = nodeId
   drawerVisible.value = true
-  showDrawer.value.openDrawer()
+  // showDrawer.value.openDrawer()
+  console.log('当前bom节点:', currentBomNodeId.value)
+}
+const materialList = ref<IotMainWorkOrderBomMaterialVO[]>([]) // 保养工单bom关联物料列表
+const bomNodeId = ref() // 最新的bomNodeId
+const selectChoose = (selectedMaterial) => {
+  selectedMaterial.bomNodeId = bomNodeId.value
+  // 关联 bomNodeId
+  const processedMaterials = selectedMaterial.map((material) => ({
+    ...material,
+    bomNodeId: bomNodeId.value // 统一关联当前行的 bomNodeId
+  }))
+
+  // 避免重复添加
+  processedMaterials.forEach((newMaterial) => {
+    // 检查是否已存在相同 bomNodeId + materialCode 的条目
+    const isExist = materialList.value.some(
+      (item) => item.bomNodeId === bomNodeId.value && item.materialCode === newMaterial.materialCode
+    )
+
+    if (!isExist) {
+      materialList.value.push(newMaterial)
+    }
+  })
+  list.value.forEach((item) => {
+    if (item.bomNodeId === bomNodeId.value) {
+      item.materials = processedMaterials;
+    }
+  })
+  console.log('选择完成的数据:', JSON.stringify(selectedMaterial))
+  console.log('添加到本地列表的数据:', materialList.value)
 }
-const selectChoose = (formData) => {
-  console.log('接收到的数据:', JSON.stringify(formData))
-  list.value.push(formData)
+const maintainChoose = (formData) => {
+  console.log('接收到的数据eee:', JSON.stringify(formData))
+  formData.forEach((item) => {
+    list.value.push(item)
+  })
 }
 const handleChildSubmit = (formData) => {
   const modified = removeOnesFromKeys(formData)
-  list.value.push(modified)
+  modified.forEach((item) => {
+    list.value.push(item)
+  })
 }
 
 const removeOnesFromKeys = (obj: Record<string, any>) => {
@@ -404,14 +497,6 @@ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成
 const submitForm = async () => {
   // 校验表单
   await formRef.value.validate()
-  if (list.value.length > 0) {
-    const nullList = list.value.filter((item) => item.depleteCount === null)
-    debugger
-    if (nullList.length > 0) {
-      message.error('请填写消耗数量')
-      return
-    }
-  }
   // 提交请求
   formLoading.value = true
   try {
@@ -425,6 +510,7 @@ const submitForm = async () => {
         })
         .join(',')
     }
+    debugger
     const data = {
       maintain: formData.value,
       maintainMaterials: list.value
@@ -491,7 +577,6 @@ onMounted(async () => {
 const handleDelete = async (id: number) => {
   try {
     const index = list.value.findIndex((item) => item.code === id)
-    debugger
     if (index !== -1) {
       // 通过 splice 删除元素
       list.value.splice(index, 1)

+ 76 - 46
src/views/pms/maintain/IotMaintainDetail.vue

@@ -3,7 +3,7 @@
     <el-form
       ref="formRef"
       :model="formData"
-      :rules="formData.type==='in'?formRules:outFormRules"
+      :rules="formData.type === 'in' ? formRules : outFormRules"
       v-loading="formLoading"
       style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
       label-width="130px"
@@ -12,10 +12,7 @@
         <el-row>
           <el-col :span="8">
             <el-form-item label="设备" prop="deviceName">
-              <el-select
-                disabled
-                v-model="formData.deviceName"
-              />
+              <el-select disabled v-model="formData.deviceName" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
@@ -78,7 +75,13 @@
           </el-col>
           <el-col :span="8">
             <el-form-item label="维修负责人" prop="maintainPerson">
-              <el-select v-model="formData.maintainPerson" filterable clearable style="width: 100%" disabled>
+              <el-select
+                v-model="formData.maintainPerson"
+                filterable
+                clearable
+                style="width: 100%"
+                disabled
+              >
                 <el-option
                   v-for="item in deptUsers"
                   :key="item.id"
@@ -101,20 +104,20 @@
           </el-col>
           <el-col :span="8">
             <el-form-item label="故障影响" prop="failureInfluence">
-              <el-input v-model="formData.failureInfluence"  disabled/>
+              <el-input v-model="formData.failureInfluence" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="故障系统" prop="failureSystem">
-              <el-input v-model="formData.failureSystem"  disabled/>
+              <el-input v-model="formData.failureSystem" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="图片" prop="pic" >
+            <el-form-item label="图片" prop="pic">
               <UploadImg v-model="formData.pic" :disabled="true" height="55px" />
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'" >
+          <el-col :span="8" v-if="formData.type === 'out'">
             <el-form-item label="供应商" prop="supplier">
               <el-select
                 disabled
@@ -124,7 +127,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'" >
+          <el-col :span="8" v-if="formData.type === 'out'">
             <el-form-item label="维修费用" prop="maintainFee">
               <el-input-number
                 disabled
@@ -137,20 +140,19 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="8" v-if="formData.type === 'out'" >
+          <el-col :span="8" v-if="formData.type === 'out'">
             <el-form-item label="委外相关附件" prop="outFile">
               <UploadFile disabled="" v-model="formData.outFile" class="min-w-80px" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-
             <el-form-item label="故障描述" prop="description">
               <el-input type="textarea" v-model="formData.description" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
             <el-form-item label="维修描述" prop="maintainDescription">
-              <el-input type="textarea" v-model="formData.maintainDescription"  disabled/>
+              <el-input type="textarea" v-model="formData.maintainDescription" disabled />
             </el-form-item>
           </el-col>
           <el-col :span="8">
@@ -160,7 +162,7 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="备注" prop="remark">
-              <el-input v-model="formData.remark" type="textarea"  disabled/>
+              <el-input v-model="formData.remark" type="textarea" disabled />
             </el-form-item>
           </el-col>
         </el-row>
@@ -171,22 +173,25 @@
     <!-- 列表 -->
     <ContentWrap>
       <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column label="物料编码" align="center" prop="code" />
-        <el-table-column label="物料名称" align="center" prop="name" />
-        <el-table-column label="单位" align="center" prop="unit" />
-        <el-table-column label="单价" align="center" prop="price" />
-        <el-table-column label="消耗数量" align="center" prop="depleteCount">
-<!--          <template #default="scope">-->
-<!--            <el-input v-model="scope.row.depleteCount" disabled/>-->
-<!--          </template>-->
-        </el-table-column>
-        <el-table-column label="总库存数量" align="center" prop="totalCount" />
-        <el-table-column label="备注" align="center" prop="remark">
-<!--          <template #default="scope">-->
-<!--            <el-input v-model="scope.row.remark" />-->
-<!--          </template>-->
+        <el-table-column label="资产编码" align="center" prop="deviceCode" />
+        <el-table-column label="设备名称" align="center" prop="deviceName" />
+        <el-table-column label="维修项" align="center" prop="name" />
+
+        <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 style="margin-left: 12px">
+                <el-button
+                  link
+                  type="primary"
+                  @click="handleViewNew(scope.row.id, scope.row.bomNodeId)"
+                >
+                  物料详情
+                </el-button>
+              </div>
+            </div>
+          </template>
         </el-table-column>
-        <el-table-column label="物料来源" align="center" prop="sourceType" />
       </el-table>
     </ContentWrap>
   </ContentWrap>
@@ -197,17 +202,25 @@
       </el-form-item>
     </el-form>
   </ContentWrap>
+  <MaterialListDrawer
+    :model-value="drawerVisible"
+    @update:model-value="(val) => (drawerVisible = val)"
+    :node-id="currentBomNodeId"
+    :materials="materialList"
+  />
 </template>
 <script setup lang="ts">
-import {DICT_TYPE, getBoolDictOptions, getStrDictOptions} from '@/utils/dict'
+import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
 import * as UserApi from '@/api/system/user'
 import { ref } from 'vue'
-import {IotMaintainMaterialApi, IotMaintainMaterialVO} from '@/api/pms/maintain/material'
+import { IotMaintainMaterialApi, IotMaintainMaterialVO } from '@/api/pms/maintain/material'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import {IotMaintainApi} from "@/api/pms/maintain";
+import { IotMaintainApi } from '@/api/pms/maintain'
+import MaterialListDrawer from '@/views/pms/iotmainworkorder/SelectedMaterialDrawer.vue'
+import { IotMaintainMaterialsApi } from '@/api/pms/maintain/materials'
 
 /** 维修工单 表单 */
-defineOptions({ name: 'IotMaintain' })
+defineOptions({ name: 'IotMaintainDetail' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -221,7 +234,9 @@ const drawerVisible = ref<boolean>(false)
 const showDrawer = ref()
 const list = ref<IotMaintainMaterialVO[]>([]) // 列表的数据
 const { params, name } = useRoute() // 查询参数
-const id = params.id||props.id
+const id = params.id || props.id
+const materialList = ref<any[]>([]) // 保养工单bom关联物料列表
+const currentBomNodeId = ref<string>()
 const formData = ref({
   id: undefined,
   failureCode: undefined,
@@ -255,7 +270,7 @@ const formRules = reactive({
   type: [{ required: true, message: '维修类型不能为空', trigger: 'blur' }],
   ifStop: [{ required: true, message: '是否停机不能为空', trigger: 'blur' }],
   failureName: [{ required: true, message: '故障名称不能为空', trigger: 'blur' }],
-  failureTime: [{ required: true, message: '故障时间不能为空', trigger: 'blur' }],
+  // failureTime: [{ required: true, message: '故障时间不能为空', trigger: 'blur' }],
   maintainStartTime: [{ required: true, message: '维修开始时间不能为空', trigger: 'blur' }],
   maintainDescription: [{ required: true, message: '维修描述不能为空', trigger: 'blur' }],
   maintainPerson: [{ required: true, message: '维修负责人不能为空', trigger: 'blur' }],
@@ -274,30 +289,45 @@ const outFormRules = reactive({
   maintainPerson: [{ required: true, message: '维修负责人不能为空', trigger: 'blur' }],
   deviceId: [{ required: true, message: '设备id不能为空', trigger: 'blur' }],
   maintainFee: [{ required: true, message: '维修费用不能为空', trigger: 'blur' }],
-  supplier: [{ required: true, message: '供应商不能为空', trigger: 'blur' }],
+  supplier: [{ required: true, message: '供应商不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
 const close = () => {
   delView(unref(currentRoute))
-  push({
-    name: 'IotMaintain',
-    query: {
-      date: new Date().getTime()
-    }
+  push({ name: 'IotMaintain', params: {} })
+}
+
+const handleViewNew = (nodeId, bomId) => {
+  drawerVisible.value = true
+  // showDrawer.value.openDrawer()
+  const queryParams = {
+    pageNo: 1,
+    pageSize: 100,
+    bomId: nodeId
+  }
+  IotMaintainMaterialsApi.getIotMaintainMaterialsPage(queryParams).then((res) => {
+    debugger
+    currentBomNodeId.value = bomId
+    materialList.value = res.list
   })
 }
+
 /** 提交表单 */
 onMounted(async () => {
   await IotMaintainApi.getIotMaintain(id).then((res) => {
     formData.value = res
-    if (res.outFile){
-      formData.value.outFile = res.outFile.split(",")
+    if (res.outFile) {
+      formData.value.outFile = res.outFile.split(',')
     }
 
-    list.value = res.maintainMaterialDOS
+    list.value = res.maintainBomDOS
+    list.value.forEach((item) => {
+      item.deviceName = res.deviceName
+      item.deviceCode = res.deviceCode
+    })
     UserApi.getUser(formData.value.maintainPerson).then((res) => {
-      formData.value.maintainPerson = res.nickname;
+      formData.value.maintainPerson = res.nickname
     })
     IotMaintainMaterialApi.getMaterialsByDeviceId()
   })

+ 7 - 7
src/views/pms/maintain/index.vue

@@ -183,13 +183,13 @@
 <!--            width="180px"-->
 <!--          />-->
 <!--          <el-table-column label="备注" align="center" prop="remark" />-->
-          <el-table-column
-            label="创建时间"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-            width="180px"
-          />
+<!--          <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

+ 241 - 0
src/views/pms/maintain/material/ChooseMaintain.vue

@@ -0,0 +1,241 @@
+<template>
+  <Dialog v-model="dialogVisible" title="选择维修项" style="width: 1100px; max-height: 800px">
+    <ContentWrap>
+      <el-form
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="68px"
+      >
+        <el-form-item label="物料编码" prop="code">
+          <el-input
+            v-model="queryParams.code"
+            placeholder="请输入物料编码"
+            clearable
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="物料名称" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            placeholder="请输入物料名称"
+            clearable
+            @keyup.enter="handleQuery"
+          />
+        </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-button @click="toggleAll" type="primary"
+            ><Icon icon="ep:refresh" class="mr-5px" />全选</el-button
+          >
+          <el-button @click="invertSelection" type="info"
+            ><Icon icon="ep:refresh" class="mr-5px" />取消全选</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+    <ContentWrap>
+      <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        @row-click="handleRowClick"
+        row-key="id"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          type="selection"
+          :reserve-selection="true"
+          :selectable="(row) => !row.disabled"
+        />
+        <el-table-column
+          label="资产编码"
+          align="center"
+          prop="deviceCode"
+          :show-overflow-tooltip="true"
+          class="!w-100px"
+        />
+        <el-table-column
+          label="设备名称"
+          align="center"
+          prop="deviceName"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column
+          label="设备名称"
+          align="center"
+          prop="deviceName"
+          :show-overflow-tooltip="true"
+        />
+        <el-table-column label="维修项" align="center" prop="name" :show-overflow-tooltip="true" />
+        <el-table-column
+          label="创建时间"
+          align="center"
+          prop="createTime"
+          :formatter="dateFormatter"
+          width="180px"
+        />
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { IotDeviceApi } from '@/api/pms/device'
+import { IotMaintainMaterialVO } from '@/api/pms/maintain/material'
+import { propTypes } from '@/utils/propTypes'
+import { dateFormatter } from '@/utils/formatTime'
+const message = useMessage() // 消息弹窗
+const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const queryFormRef = ref() // 搜索的表单
+const selectedRows = ref<any[]>([]) // 多选数据(存储所有选中行的数组)
+const list = ref<any[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const tableRef = ref()
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deviceIds: []
+})
+const props = defineProps({
+  deptId: propTypes.number.def(undefined)
+})
+const invertSelection = () => {
+  selectedRows.value.forEach((row) => {
+    tableRef.value.toggleRowSelection(row, false)
+    selectedRows.value.splice(row)
+  })
+}
+const toggleAll = () => {
+  list.value.forEach((row) => {
+    tableRef.value.toggleRowSelection(row, true)
+  })
+}
+const selectedRow = ref(null)
+const handleConfirm = () => {
+  if (selectedRows.value.length === 0) {
+    ElMessage.warning('请至少选择一个维修项')
+    return
+  }
+
+  // emit('choose', selectedRows.value);
+  emit(
+    'choose',
+    selectedRows.value.map((row) => ({
+      ...row
+      // 确保返回必要字段
+      // code: row.item,
+      // name: row.standard
+    }))
+  )
+  dialogVisible.value = false
+  handleClose()
+}
+// 关闭时清空选择
+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 handleRowClick = (row) => {
+  toggleRow(row)
+}
+const toggleRow = (row) => {
+  const includes = selectedRows.value.includes(row)
+  if (includes) {
+    selectedRows.value.splice(row)
+  } else {
+    selectedRows.value.push(row) // 选中
+  }
+  tableRef.value.toggleRowSelection(row, !includes)
+}
+const handleSelectionChange = (val) => {
+  selectedRows.value = val
+}
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  queryParams.deviceIds.push(id)
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+const getList = async () => {
+  loading.value = true
+  try {
+    const res = await IotDeviceApi.deviceAssociateBomListPage(queryParams)
+    const rawData = res || []
+    if (rawData.length === 0) {
+      message.error('选择的设备不存在待保养BOM项')
+    }
+    list.value = rawData.list
+    total.value = rawData.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const choose = (row: IotMaintainMaterialVO) => {
+  emit('choose', row)
+  dialogVisible.value = false
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+</script>
+<style lang="scss">
+.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>

+ 3 - 0
src/views/pms/maintain/material/MaterialSelect.vue

@@ -95,6 +95,8 @@ import * as DictTypeApi from '@/api/system/dict/dict.type'
 import { checkPermi } from '@/utils/permission'
 import * as MaterialApi from '@/api/pms/material'
 import {DICT_TYPE} from "@/utils/dict";
+import {CrmStatisticCustomerAreaRespVO} from "@/api/crm/statistics/portrait";
+import {areaReplace} from "@/utils";
 
 const emit = defineEmits(['choose']) // 定义 success 事件,用于操作成功后的回调
 const dialogVisible = ref(false) // 弹窗的是否展示
@@ -125,6 +127,7 @@ const handleRowClick = (row) => {
 }
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
+  selectedRow.value = ''
   await getList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗

+ 2 - 2
src/views/pms/maintenance/MainPlanDeviceList.vue

@@ -118,8 +118,8 @@ const { wsCache } = useCache()
 const getList = async () => {
   loading.value = true
   try {
-    const user = wsCache.get(CACHE_KEY.USER)
-    queryParams.deptId = user.user.deptId
+    // const user = wsCache.get(CACHE_KEY.USER)
+    // queryParams.deptId = user.user.deptId
     const data = await IotDeviceApi.getIotDevicePage(queryParams)
     list.value = data.list
     total.value = data.total

+ 125 - 0
src/views/pms/modeltemplate/ModelCategoryTree.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="head-container">
+    <el-input v-model="deviceCategoryName" class="mb-20px" clearable placeholder="请输入设备分类名称">
+      <template #prefix>
+        <Icon icon="ep:search" />
+      </template>
+    </el-input>
+  </div>
+  <div class="head-container">
+    <el-tree
+      ref="treeRef"
+      :data="deviceCategoryList"
+      :expand-on-click-node="false"
+      :filter-node-method="filterNode"
+      :props="defaultProps"
+      default-expand-all
+      highlight-current
+      node-key="id"
+      @node-click="handleNodeClick"
+      @node-contextmenu="handleRightClick"
+      style="height: 35em"
+    />
+  </div>
+  <div
+    v-show="menuVisible"
+    class="custom-menu"
+    :style="{ left: menuX + 'px', top: menuY + 'px' }"
+  >
+    <ul>
+      <li @click="handleMenuClick('add')">新增子节点</li>
+      <li @click="handleMenuClick('edit')">重命名</li>
+      <li @click="handleMenuClick('delete')">删除</li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ElTree } from 'element-plus'
+import * as DeviceCategoryApi from '@/api/pms/productclassify'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
+
+defineOptions({ name: 'DeviceCategoryTree' })
+
+const treeStore = useTreeStore();
+const deviceCategoryName = ref('')
+const deviceCategoryList = ref<Tree[]>([]) // 树形结构
+const treeRef = ref<InstanceType<typeof ElTree>>()
+const menuVisible = ref(false);
+const menuX = ref(0);
+const menuY = ref(0);
+let selectedNode = null;
+const handleRightClick = (event, { node, data }) => {
+  event.preventDefault();
+  menuX.value = event.clientX;
+  menuY.value = event.clientY;
+  selectedNode = data; // 存储当前操作的节点数据 ‌:ml-citation{ref="7" data="citationList"}
+  menuVisible.value = true;
+};
+
+const handleMenuClick = (action) => {
+  switch(action) {
+    case 'add':
+      // 调用新增节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
+      break;
+    case 'edit':
+      // 调用编辑节点逻辑 ‌:ml-citation{ref="7" data="citationList"}
+      break;
+    case 'delete':
+      // 调用删除节点逻辑 ‌:ml-citation{ref="4" data="citationList"}
+      break;
+  }
+  menuVisible.value = false;
+};
+/** 获得 设备分类 树 */
+const getTree = async () => {
+  const res = await DeviceCategoryApi.IotProductClassifyApi.getSimpleProductClassifyList()
+  deviceCategoryList.value = []
+  deviceCategoryList.value.push(...handleTree(res))
+}
+
+/** 基于名字过滤 */
+const filterNode = (name: string, data: Tree) => {
+  if (!name) return true
+  return data.name.includes(name)
+}
+
+/** 处理 设备分类树 被点击 */
+const handleNodeClick = async (row: { [key: string]: any }) => {
+  emits('node-click', row)
+  treeStore.setSelectedId(row.id);
+}
+const emits = defineEmits(['node-click'])
+
+/** 监听 deviceCategoryName */
+watch(deviceCategoryName, (val) => {
+  treeRef.value!.filter(val)
+})
+
+/** 初始化 */
+onMounted(async () => {
+  await getTree()
+})
+</script>
+<style lang="scss" scoped>
+.custom-menu {
+  position: fixed;
+  background: white;
+  border: 1px solid #ccc;
+  box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
+  z-index: 1000;
+}
+.custom-menu ul {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+.custom-menu li {
+  padding: 8px 20px;
+  cursor: pointer;
+}
+.custom-menu li:hover {
+  background: #f5f5f5;
+}
+</style>

+ 168 - 0
src/views/pms/modeltemplate/TemplateForm.vue

@@ -0,0 +1,168 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="设备分类" prop="deviceCategoryId">
+            <el-tree-select
+              v-model="formData.deviceCategoryId"
+              :data="deviceCategoryTree"
+              :props="defaultProps"
+              check-strictly
+              default-expand-all
+              value-key="deviceCategoryId"
+              placeholder="请选择设备分类"
+              @node-click="handleDeviceCategoryTreeNodeClick"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="模板名称" prop="name">
+            <el-input v-model="formData.name" maxlength="32" placeholder="请输入模板名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="模板编码" prop="code">
+            <el-input v-model="formData.code" maxlength="50" placeholder="请输入模板编码" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="备注">
+            <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
+import * as DeviceTemplateApi from '@/api/pms/modeltemplate'
+import { FormRules } from 'element-plus'
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore';
+import {ModelAttrTemplateVO} from "@/api/pms/modeltemplate";
+
+defineOptions({ name: 'DeviceTemplateForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const treeStore = useTreeStore();
+const localDeviceCategoryId = ref(null);  // 通过store存储的设备分类id 由父组件传递过来
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  name: '',
+  deviceCategoryId: localDeviceCategoryId.value,
+  code: '',
+  id: undefined,
+  remark: '',
+  status: CommonStatusEnum.ENABLE,
+})
+const formRules = reactive<FormRules>({
+  name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '模板编码不能为空', trigger: 'blur' }],
+  deviceCategoryId: [{ required: true, message: '所属设备分类不能为空', trigger: 'blur' }],
+
+})
+const formRef = ref() // 表单 Ref
+const deviceCategoryTree = ref()  // 设备分类树
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  // 获取store中的设备分类id
+  localDeviceCategoryId.value = treeStore.selectedId;
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DeviceTemplateApi.getModelTemplate(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得 设备分类树
+  await getDeviceCategoryTree()
+  // 加载 设备分类 树
+  // deviceCategoryList.value = handleTree(await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList())
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success', 'node-click']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as DeviceTemplateApi.ModelAttrTemplateVO
+    if (formType.value === 'create') {
+      await DeviceTemplateApi.createModelTemplate(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeviceTemplateApi.updateModelTemplate(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 获得 设备分类 树 **/
+const getDeviceCategoryTree = async () => {
+  deviceCategoryTree.value = []
+  const res = await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
+  let categoryTree: Tree = { id: 0, name: '顶级设备分类', children: [] }
+  categoryTree.children = handleTree(res)
+  deviceCategoryTree.value.push(categoryTree)
+}
+
+/** 处理 设备分类 树 被点击 */
+const handleDeviceCategoryTreeNodeClick = async (row: { [key: string]: any }) => {
+  emit('node-click', row)
+  // treeStore.setSelectedId(row.id);
+  // 更新当前设备分类id
+  localDeviceCategoryId.value = row.id
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    name: '',
+    deviceCategoryId: localDeviceCategoryId.value,
+    code: '',
+    id: undefined,
+    remark: '',
+    status: CommonStatusEnum.ENABLE,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 34 - 0
src/views/pms/modeltemplate/detail/TemplateDetailsHeader.vue

@@ -0,0 +1,34 @@
+<template>
+  <div>
+    <div class="flex items-start justify-between">
+      <div>
+        <el-col>
+          <el-row>
+            <span class="text-xl font-bold">{{ template.name }}</span>
+          </el-row>
+        </el-col>
+      </div>
+      <div>
+        <!-- 右上:按钮 -->
+      </div>
+    </div>
+  </div>
+  <ContentWrap class="mt-10px">
+    <el-descriptions :column="5" direction="horizontal">
+      <el-descriptions-item label="设备编码:">
+        {{ template.code }}
+      </el-descriptions-item>
+      <el-descriptions-item label="备注:">
+        {{ template.remark }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as DeviceTemplateApi from '@/api/pms/devicetemplate'
+
+const message = useMessage()
+
+const { template } = defineProps<{ template: DeviceTemplateApi.DeviceAttrTemplateVO }>() // 定义 Props
+
+</script>

+ 218 - 0
src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelForm.vue

@@ -0,0 +1,218 @@
+<!-- 设备分类属性表单 -->
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="DeviceAttrModelFormRules"
+      label-width="100px"
+    >
+      <el-form-item label="属性名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入属性名称" />
+      </el-form-item>
+      <el-form-item label="标识符" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入标识符" />
+      </el-form-item>
+      <!-- 属性配置 -->
+      <DeviceAttrModelProperty
+        v-model="formData.selectOptions"
+      />
+      <el-form-item label="是否数采" prop="isCollection">
+        <el-radio-group v-model="formData.isCollection">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.RQ_IOT_ISCOLLECTION)"
+            :key="dict.value"
+            :value="dict.value"
+            @click="radioChange(dict.value)"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="物属性" prop="modelAttr">
+        <el-select v-model="formData.modelAttr" placeholder="请选择">
+          <el-option
+            v-for="dict in thingsModelData"
+            :key="dict.modelName"
+            :label="dict.modelName"
+            :value="dict.identifier"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="默认值" prop="defaultValue">
+        <el-input v-model="formData.defaultValue" placeholder="请输入默认值" />
+      </el-form-item>
+      <el-form-item label="描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          :maxlength="200"
+          :rows="3"
+          placeholder="请输入属性描述"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import DeviceAttrModelProperty from './AttrTemplateModelProperty.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DeviceAttrModelApi, DeviceAttrModelData } from '@/api/pms/modelattrtemplate'
+import { DataSpecsDataType, DeviceAttrModelFormRules } from './config'
+import { cloneDeep } from 'lodash-es'
+import { isEmpty } from '@/utils/is'
+import { defineProps } from 'vue'
+import * as ModelTemplateApi from "@/api/pms/modeltemplate";
+
+/** 设备属性 模型数据表单 */
+defineOptions({ name: 'AttrTemplateModelForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref<DeviceAttrModelData>({
+  deviceCategoryId: -1,
+  dataType: DataSpecsDataType.DOUBLE,
+  type: DataSpecsDataType.DOUBLE,
+  requiredFlag: 0,
+  description: '',
+  defaultValue: '',
+  selectOptions: {
+    type: DataSpecsDataType.DOUBLE,
+    defaultValue: '',
+    requiredFlag: '',
+    dataSpecs: {
+      dataType: DataSpecsDataType.DOUBLE
+    }
+  },
+  isCollection:1,
+  modelAttr:'',
+})
+const thingsModelData = ref([{
+    modelName:'',
+    identifier:''
+  }]
+)
+
+
+let deviceCategoryName: string;
+const radioChange = (event) => {
+  if(event==1){
+    getAttrList();
+  }
+  // 在这里添加你的逻辑处理代码
+};
+
+const getAttrList = async () => {
+  try {
+    const data = await DeviceAttrModelApi.getThingsModelAttr(deviceCategoryName);
+    thingsModelData.value = data
+  } finally {
+
+  }
+}
+
+const props = defineProps({
+  deviceCategoryId: [String, Number]
+})
+
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number,name?:string) => {
+  deviceCategoryName = name;
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await DeviceAttrModelApi.getDeviceAttrModel(id)
+      // 情况一:属性初始化
+      if (isEmpty(formData.value.selectOptions)) {
+        formData.value.type = DataSpecsDataType.DOUBLE
+        formData.value.selectOptions = {
+          type: DataSpecsDataType.DOUBLE,
+          dataSpecs: {
+            dataType: DataSpecsDataType.DOUBLE
+          }
+        }
+      }
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+
+/** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  await formRef.value.validate()
+  formLoading.value = true
+  try {
+    const data = cloneDeep(formData.value) as DeviceAttrModelData
+    data.deviceCategoryId = props.deviceCategoryId
+    data.requiredFlag = formData.value.selectOptions.requiredFlag
+    fillExtraAttributes(data)
+    if (formType.value === 'create') {
+      await DeviceAttrModelApi.createDeviceAttrModel(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeviceAttrModelApi.updateDeviceAttrModel(data)
+      message.success(t('common.updateSuccess'))
+    }
+  } finally {
+    dialogVisible.value = false // 确保关闭弹框
+    emit('success')
+    formLoading.value = false
+  }
+}
+
+/** 填写额外的属性 */
+const fillExtraAttributes = (data: any) => {
+  // 处理不同类型的情况
+  // 属性
+  removeDataSpecs(data.selectOptions)
+  data.type = data.selectOptions.type
+  data.selectOptions.identifier = data.code
+  data.selectOptions.code = data.code
+  data.selectOptions.name = data.name
+}
+/** 处理 dataSpecs 为空的情况 */
+const removeDataSpecs = (val: any) => {
+  if (isEmpty(val.dataSpecs)) {
+    delete val.dataSpecs
+  }
+  if (isEmpty(val.dataSpecsList)) {
+    delete val.dataSpecsList
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    dataType: DataSpecsDataType.DOUBLE,
+    type: DataSpecsDataType.DOUBLE,
+    selectOptions: {
+      type: DataSpecsDataType.DOUBLE,
+      dataSpecs: {
+        dataType: DataSpecsDataType.DOUBLE
+      }
+    },
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 171 - 0
src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelProperty.vue

@@ -0,0 +1,171 @@
+<!-- 设备属性模型表单(property 项) -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
+    label="数据类型"
+    prop="selectOptions.type"
+  >
+    <el-select v-model="selectOptions.type" placeholder="请选择数据类型" @change="handleChange">
+      <!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) ${option.value}(${option.label}) -->
+      <el-option
+        v-for="option in getDataTypeOptions"
+        :key="option.value"
+        :label="`${option.label}`"
+        :value="option.value"
+      />
+    </el-select>
+  </el-form-item>
+  <!-- 数值型配置 -->
+  <ModelAttrModelNumberDataSpecs
+    v-if="
+      [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
+        selectOptions.type || ''
+      )
+    "
+    v-model="selectOptions.dataSpecs"
+  />
+  <!-- 枚举型配置 -->
+  <ModelAttrModelNumberDataSpecs
+    v-if="selectOptions.type === DataSpecsDataType.ENUM"
+    v-model="selectOptions.dataSpecsList"
+  />
+  <!-- 布尔型配置 -->
+  <el-form-item v-if="selectOptions.type === DataSpecsDataType.BOOL" label="布尔值">
+    <template v-for="(item, index) in selectOptions.dataSpecsList" :key="item.value">
+      <div class="flex items-center justify-start w-1/1 mb-5px">
+        <span>{{ item.value }}</span>
+        <span class="mx-2">-</span>
+        <el-form-item
+          :prop="`selectOptions.dataSpecsList[${index}].name`"
+          :rules="[
+            { required: true, message: '枚举描述不能为空' },
+            { validator: validateBoolName, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input
+            v-model="item.name"
+            :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
+            class="w-255px!"
+          />
+        </el-form-item>
+      </div>
+    </template>
+  </el-form-item>
+  <!-- 文本型配置
+  <el-form-item
+    v-if="selectOptions.type === DataSpecsDataType.TEXT"
+    label="数据长度"
+    prop="selectOptions.dataSpecs.length"
+  >
+    <el-input v-model="selectOptions.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
+      <template #append>字节</template>
+    </el-input>
+  </el-form-item>
+  -->
+  <!-- 时间型配置 -->
+  <el-form-item v-if="selectOptions.type === DataSpecsDataType.DATE" label="时间格式" prop="date">
+    <el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" />
+  </el-form-item>
+  <!-- 数组型配置-->
+  <ModelAttrModelArrayDataSpecs
+    v-if="selectOptions.type === DataSpecsDataType.ARRAY"
+    v-model="selectOptions.dataSpecs"
+  />
+  <!-- Struct 型配置-->
+  <ModelAttrModelStructDataSpecs
+    v-if="selectOptions.type === DataSpecsDataType.STRUCT"
+    v-model="selectOptions.dataSpecsList"
+  />
+  <el-form-item v-if="!isStructDataSpecs && !isParams" label="是否必填" prop="selectOptions.requiredFlag">
+    <el-radio-group v-model="selectOptions.requiredFlag">
+      <el-radio :label="DeviceAttrModelRequired.REQUIRED.value">
+        {{ DeviceAttrModelRequired.REQUIRED.label }}
+      </el-radio>
+      <el-radio :label="DeviceAttrModelRequired.NOT_REQUIRED.value">
+        {{ DeviceAttrModelRequired.NOT_REQUIRED.label }}
+      </el-radio>
+    </el-radio-group>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import {
+  DataSpecsDataType,
+  dataTypeOptions,
+  DeviceAttrModelRequired,
+  validateBoolName
+} from './config'
+import {
+  ModelAttrModelArrayDataSpecs,
+  ModelAttrModelEnumDataSpecs,
+  ModelAttrModelNumberDataSpecs,
+  ModelAttrModelStructDataSpecs
+} from '../dataSpecs'
+import { ModelTemplateAttrs } from '@/api/pms/modelattrtemplate'
+import { isEmpty } from '@/utils/is'
+
+/** 设备属性模板 属性 */
+defineOptions({ name: 'AttrTemplateModelProperty' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const selectOptions = useVModel(props, 'modelValue', emits) as Ref<ModelTemplateAttrs>
+const getDataTypeOptions = computed(() => {
+  return !props.isStructDataSpecs
+    ? dataTypeOptions
+    : dataTypeOptions.filter(
+        (item) =>
+          !([DataSpecsDataType.STRUCT, DataSpecsDataType.ARRAY] as any[]).includes(item.value)
+      )
+}) // 获得数据类型列表
+
+/** 属性值的数据类型切换时初始化相关数据 */
+const handleChange = (dataType: any) => {
+  selectOptions.value.dataSpecs = {}
+  selectOptions.value.dataSpecsList = []
+  // 不是列表型数据才设置 dataSpecs.dataType
+  ![DataSpecsDataType.ENUM, DataSpecsDataType.BOOL, DataSpecsDataType.STRUCT].includes(dataType) &&
+    (selectOptions.value.dataSpecs.dataType = dataType)
+  switch (dataType) {
+    case DataSpecsDataType.ENUM:
+      selectOptions.value.dataSpecsList.push({
+        dataType: DataSpecsDataType.ENUM,
+        name: '', // 枚举项的名称
+        value: undefined // 枚举值
+      })
+      break
+    case DataSpecsDataType.BOOL:
+      for (let i = 0; i < 2; i++) {
+        selectOptions.value.dataSpecsList.push({
+          dataType: DataSpecsDataType.BOOL,
+          name: '', // 布尔值的名称
+          value: i // 布尔值
+        })
+      }
+      break
+  }
+}
+
+// 默认选中 是否必填
+watch(
+  () => selectOptions.value.requiredFlag,
+  (val: string) => {
+    if (props.isStructDataSpecs || props.isParams) {
+      return
+    }
+    isEmpty(val) && (selectOptions.value.requiredFlag = DeviceAttrModelRequired.REQUIRED.value)
+  },
+  { immediate: true }
+)
+
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 182 - 0
src/views/pms/modeltemplate/detail/attrsModel/config.ts

@@ -0,0 +1,182 @@
+import { isEmpty } from '@/utils/is'
+
+/** dataSpecs 数值型数据结构 */
+export interface DataSpecsNumberDataVO {
+  dataType: 'int' | 'float' | 'double' // 数据类型,取值为 INT、FLOAT 或 DOUBLE
+  max: string // 最大值,必须与 dataType 设置一致,且为 STRING 类型
+  min: string // 最小值,必须与 dataType 设置一致,且为 STRING 类型
+  step: string // 步长,必须与 dataType 设置一致,且为 STRING 类型
+  precise?: string // 精度,当 dataType 为 FLOAT 或 DOUBLE 时可选
+  defaultValue?: string // 默认值,可选
+  unit: string // 单位的符号
+  unitName: string // 单位的名称
+}
+
+/** dataSpecs 枚举型数据结构 */
+export interface DataSpecsEnumOrBoolDataVO {
+  dataType: 'enum' | 'bool'
+  defaultValue?: string // 默认值,可选
+  name: string // 枚举项的名称
+  value: number | undefined // 枚举值
+}
+
+/** 属性值的数据类型 */
+export const DataSpecsDataType = {
+  INT: 'int',
+  FLOAT: 'float',
+  DOUBLE: 'double',   //
+  ENUM: 'enum',       //
+  BOOL: 'bool',
+  TEXT: 'text',       //
+  TEXTAREA: 'textarea',
+  DATE: 'date',       //
+  STRUCT: 'struct',
+  ARRAY: 'array',
+} as const
+
+/** 物体模型数据类型配置项 */
+export const dataTypeOptions = [
+  // { value: DataSpecsDataType.INT, label: '整数型' },
+  // { value: DataSpecsDataType.FLOAT, label: '单精度浮点型' },
+  { value: DataSpecsDataType.DOUBLE, label: '数值' },
+  { value: DataSpecsDataType.ENUM, label: '下拉框' },
+  // { value: DataSpecsDataType.BOOL, label: '布尔型' },
+  { value: DataSpecsDataType.TEXT, label: '单行文本' },
+  { value: DataSpecsDataType.TEXTAREA, label: '多行文本' },
+  { value: DataSpecsDataType.DATE, label: '日期' },
+  // { value: DataSpecsDataType.STRUCT, label: '结构体' },
+  // { value: DataSpecsDataType.ARRAY, label: '数组' }
+]
+
+/** 获得物体模型数据类型配置项名称 dataType && `${dataType.value}(${dataType.label})` */
+export const getDataTypeOptionsLabel = (value: string) => {
+  if (isEmpty(value)) {
+    return value
+  }
+  const dataType = dataTypeOptions.find((option) => option.value === value)
+  return dataType && `${dataType.label}`
+}
+
+// 设备属性模型访问模式枚举类
+export const DeviceAttrModelAccessMode = {
+  READ_WRITE: {
+    label: '读写',
+    value: 'rw'
+  },
+  READ_ONLY: {
+    label: '只读',
+    value: 'r'
+  }
+} as const
+
+// 设备属性模型 是否必填 枚举类
+export const DeviceAttrModelRequired = {
+  REQUIRED: {
+    label: '必填',
+    value: 1
+  },
+  NOT_REQUIRED: {
+    label: '非必填',
+    value: 0
+  }
+} as const
+
+/** 公共校验规则 */
+export const DeviceAttrModelFormRules = {
+  name: [
+    { required: true, message: '属性名称不能为空', trigger: 'blur' },
+    {
+      pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
+      message:
+        '支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
+      trigger: 'blur'
+    }
+  ],
+  type: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
+  code: [
+    { required: true, message: '标识符不能为空', trigger: 'blur' },
+    {
+      pattern: /^[a-zA-Z0-9_]{1,50}$/,
+      message: '支持大小写字母、数字和下划线,不超过 50 个字符',
+      trigger: 'blur'
+    },
+    {
+      validator: (_: any, value: string, callback: any) => {
+        const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
+        if (reservedKeywords.includes(value)) {
+          callback(
+            new Error(
+              'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
+            )
+          )
+        } else if (/^\d+$/.test(value)) {
+          callback(new Error('标识符不能是纯数字'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  'selectOptions.dataSpecs.childDataType': [{ required: true, message: '元素类型不能为空' }],
+  'selectOptions.dataSpecs.size': [
+    { required: true, message: '元素个数不能为空' },
+    {
+      validator: (_: any, value: any, callback: any) => {
+        if (isEmpty(value)) {
+          callback(new Error('元素个数不能为空'))
+          return
+        }
+        if (isNaN(Number(value))) {
+          callback(new Error('元素个数必须是数字'))
+          return
+        }
+        callback()
+      },
+      trigger: 'blur'
+    }
+  ],
+  'selectOptions.dataSpecs.length': [
+    { required: true, message: '请输入文本字节长度', trigger: 'blur' },
+    {
+      validator: (_: any, value: any, callback: any) => {
+        if (isEmpty(value)) {
+          callback(new Error('文本长度不能为空'))
+          return
+        }
+        if (isNaN(Number(value))) {
+          callback(new Error('文本长度必须是数字'))
+          return
+        }
+        callback()
+      },
+      trigger: 'blur'
+    }
+  ],
+  'selectOptions.accessMode': [{ required: true, message: '请选择读写类型', trigger: 'change' }]
+}
+
+/** 校验布尔值名称 */
+export const validateBoolName = (_: any, value: string, callback: any) => {
+  if (isEmpty(value)) {
+    callback(new Error('布尔值名称不能为空'))
+    return
+  }
+  // 检查开头字符
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
+    callback(new Error('布尔值名称必须以中文、英文字母或数字开头'))
+    return
+  }
+  // 检查整体格式
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
+    callback(new Error('布尔值名称只能包含中文、英文字母、数字、下划线和短划线'))
+    return
+  }
+  // 检查长度(一个中文算一个字符)
+  if (value.length > 20) {
+    callback(new Error('布尔值名称长度不能超过 20 个字符'))
+    return
+  }
+
+  callback()
+}

+ 173 - 0
src/views/pms/modeltemplate/detail/attrsModel/index.vue

@@ -0,0 +1,173 @@
+<!-- 设备分类属性列表 -->
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+        <el-button
+          v-hasPermi="[`rq:iot-model-template-attrs:create`]"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus" />
+          添加属性
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-tabs>
+      <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+        <el-table-column align="center" label="属性名称" prop="name" />
+        <el-table-column align="center" label="标识符" prop="code" />
+        <el-table-column align="center" label="数据类型" prop="type">
+          <template #default="{ row }">
+            {{ dataTypeOptionsLabel(row.selectOptions?.type) ?? '-' }}
+          </template>
+        </el-table-column>
+        <!--
+        <el-table-column align="left" label="数据定义" prop="code">
+          <template #default="{ row }">
+            <DataDefinition :data="row" />
+          </template>
+        </el-table-column>
+        -->
+        <el-table-column align="center" label="操作">
+          <template #default="scope">
+            <el-button
+              v-hasPermi="[`rq:iot-model-template-attrs:update`]"
+              link
+              type="primary"
+              @click="openForm('update', scope.row.id)"
+            >
+              编辑
+            </el-button>
+            <el-button
+              v-hasPermi="['rq:iot-model-template-attrs:delete']"
+              link
+              type="danger"
+              @click="handleDelete(scope.row.id)"
+            >
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
+        @pagination="getList"
+      />
+    </el-tabs>
+  </ContentWrap>
+  <!-- 表单弹窗:添加/修改 -->
+  <AttrTemplateModelForm ref="formRef" @success="getList" :deviceCategoryId="deviceCategoryId" />
+</template>
+<script lang="ts" setup>
+import { DeviceAttrModelApi, DeviceAttrModelData } from '@/api/pms/modelattrtemplate'
+import AttrTemplateModelForm from './AttrTemplateModelForm.vue'
+import { getDataTypeOptionsLabel } from './config'
+import { DataDefinition } from '../components'
+import {useTagsViewStore} from "@/store/modules/tagsView";
+
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const { currentRoute } = useRouter()
+
+defineOptions({ name: 'ModelAttrTemplate' })
+
+const deviceCategoryId = route.params.id.split(",")[0] // 设备分类id
+const categoryName = route.params.id.split(",")[1];//设备分类名称
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const list = ref<DeviceAttrModelData[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  deviceCategoryId: -1
+})
+
+const queryFormRef = ref() // 搜索的表单
+const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.deviceCategoryId = deviceCategoryId
+    const data = await DeviceAttrModelApi.getDeviceAttrModelPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  queryParams.type = undefined
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number,name?:string) => {
+  name = categoryName;
+
+    formRef.value.open(type,id,name)
+  s
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DeviceAttrModelApi.deleteDeviceAttrModel(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  if (!deviceCategoryId) {
+    message.warning('参数错误,设备属性模板不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  getList()
+})
+</script>

+ 48 - 0
src/views/pms/modeltemplate/detail/components/DataDefinition.vue

@@ -0,0 +1,48 @@
+<template>
+  <!-- 属性 -->
+    <!-- 非列表型:数值 -->
+    <div
+      v-if="
+        [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
+          data.selectOptions.type
+        )
+      "
+    >
+      取值范围:{{ `${data.selectOptions.dataSpecs.min}~${data.selectOptions.dataSpecs.max}` }}
+    </div>
+    <!-- 非列表型:文本 -->
+    <div v-if="DataSpecsDataType.TEXT === data.selectOptions.type">
+      数据长度:{{ data.selectOptions.dataSpecs.length }}
+    </div>
+    <!-- 列表型: 数组、结构、时间(特殊) -->
+    <div
+      v-if="
+        [DataSpecsDataType.ARRAY, DataSpecsDataType.STRUCT, DataSpecsDataType.DATE].includes(
+          data.selectOptions.type
+        )
+      "
+    >
+      -
+    </div>
+    <!-- 列表型: 布尔值、枚举 -->
+    <div v-if="[DataSpecsDataType.BOOL, DataSpecsDataType.ENUM].includes(data.selectOptions.type)">
+      <div> {{ DataSpecsDataType.BOOL === data.selectOptions.dataType ? '布尔值' : '枚举值' }}:</div>
+      <div v-for="item in data.selectOptions.dataSpecsList" :key="item.value">
+        {{ `${item.name}-${item.value}` }}
+      </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  DataSpecsDataType,
+} from '@/views/pms/devicetemplate/detail/attrsModel/config'
+import { DeviceAttrModelData } from '@/api/pms/modelattrtemplate'
+
+/** 数据定义展示组件 */
+defineOptions({ name: 'DataDefinition' })
+
+defineProps<{ data: DeviceAttrModelData }>()
+</script>
+
+<style lang="scss" scoped></style>

+ 3 - 0
src/views/pms/modeltemplate/detail/components/index.ts

@@ -0,0 +1,3 @@
+import DataDefinition from './DataDefinition.vue'
+
+export { DataDefinition }

+ 52 - 0
src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelArrayDataSpecs.vue

@@ -0,0 +1,52 @@
+<!-- dataType:array 数组类型 -->
+<template>
+  <el-form-item label="元素类型" prop="selectOptions.dataSpecs.childDataType">
+    <el-radio-group v-model="dataSpecs.childDataType" @change="handleChange">
+      <template v-for="item in dataTypeOptions" :key="item.value">
+        <el-radio
+          v-if="
+            !(
+              [DataSpecsDataType.ENUM, DataSpecsDataType.ARRAY, DataSpecsDataType.DATE] as any[]
+            ).includes(item.value)
+          "
+          :value="item.value"
+          class="w-1/3"
+        >
+          {{ `${item.value}(${item.label})` }}
+        </el-radio>
+      </template>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="元素个数" prop="selectOptions.dataSpecs.size">
+    <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
+  </el-form-item>
+  <!-- Struct 型配置-->
+  <DeviceAttrModelStructDataSpecs
+    v-if="dataSpecs.childDataType === DataSpecsDataType.STRUCT"
+    v-model="dataSpecs.dataSpecsList"
+  />
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsDataType, dataTypeOptions } from '../attrsModel/config'
+import DeviceAttrModelStructDataSpecs from './ModelAttrModelStructDataSpecs.vue'
+
+/** 数组型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ModelAttrModelArrayDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>
+
+/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
+const handleChange = (val: string) => {
+  if (val !== DataSpecsDataType.STRUCT) {
+    return
+  }
+
+  dataSpecs.value.dataSpecsList = []
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 163 - 0
src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelEnumDataSpecs.vue

@@ -0,0 +1,163 @@
+<!-- dataType:enum 数组类型 -->
+<template>
+  <el-form-item
+    :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
+    label="下拉选项"
+  >
+    <div class="flex flex-col">
+      <div class="flex items-center">
+        <!--
+        <span class="flex-1"> 参数值 </span>
+        <span class="flex-1"> 参数描述 </span>
+        -->
+      </div>
+      <div
+        v-for="(item, index) in dataSpecsList"
+        :key="index"
+        class="flex items-center justify-between mb-5px"
+      >
+        <!--
+        <el-form-item
+          :prop="`selectOptions.dataSpecsList[${index}].value`"
+          :rules="[
+            { required: true, message: '枚举值不能为空' },
+            { validator: validateEnumValue, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input v-model="item.value" placeholder="请输入枚举值,如'0'" />
+        </el-form-item>
+        <span class="mx-2">~</span>
+        -->
+        <el-form-item
+          :prop="`selectOptions.dataSpecsList[${index}].name`"
+          :rules="[
+            { required: true, message: '下拉选项不能为空' },
+            { validator: validateEnumName, trigger: 'blur' }
+          ]"
+          class="flex-1 mb-0"
+        >
+          <el-input v-model="item.name" placeholder="对该下拉选项的描述" />
+        </el-form-item>
+        <el-button class="ml-10px" link type="primary" @click="deleteEnum(index)">删除</el-button>
+      </div>
+      <el-button link type="primary" @click="addEnum">+添加下拉选项值</el-button>
+    </div>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsDataType, DataSpecsEnumOrBoolDataVO } from '../attrsModel/config'
+import { isEmpty } from '@/utils/is'
+
+/** 枚举型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ModelAttrModelEnumDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolDataVO[]>
+const message = useMessage()
+
+/** 添加枚举项 */
+const addEnum = () => {
+  dataSpecsList.value.push({
+    dataType: DataSpecsDataType.ENUM,
+    name: '', // 枚举项的名称
+    value: undefined // 枚举值
+  })
+}
+
+/** 删除枚举项 */
+const deleteEnum = (index: number) => {
+  if (dataSpecsList.value.length === 1) {
+    message.warning('至少需要一个枚举项')
+    return
+  }
+  dataSpecsList.value.splice(index, 1)
+}
+
+/** 校验枚举值 */
+const validateEnumValue = (_: any, value: any, callback: any) => {
+  if (isEmpty(value)) {
+    callback(new Error('枚举值不能为空'))
+    return
+  }
+  if (isNaN(Number(value))) {
+    callback(new Error('枚举值必须是数字'))
+    return
+  }
+  // 检查枚举值是否重复
+  const values = dataSpecsList.value.map((item) => item.value)
+  if (values.filter((v) => v === value).length > 1) {
+    callback(new Error('枚举值不能重复'))
+    return
+  }
+  callback()
+}
+
+/** 校验枚举描述 */
+const validateEnumName = (_: any, value: string, callback: any) => {
+  if (isEmpty(value)) {
+    callback(new Error('枚举描述不能为空'))
+    return
+  }
+  // 检查开头字符
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
+    callback(new Error('枚举描述必须以中文、英文字母或数字开头'))
+    return
+  }
+  // 检查整体格式
+  if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
+    callback(new Error('枚举描述只能包含中文、英文字母、数字、下划线和短划线'))
+    return
+  }
+  // 检查长度(一个中文算一个字符)
+  if (value.length > 20) {
+    callback(new Error('枚举描述长度不能超过20个字符'))
+    return
+  }
+  callback()
+}
+
+/** 校验整个枚举列表 */
+const validateEnumList = (_: any, __: any, callback: any) => {
+  if (isEmpty(dataSpecsList.value)) {
+    callback(new Error('请至少添加一个枚举项'))
+    return
+  }
+
+  // 检查是否存在空值
+  const hasEmptyValue = dataSpecsList.value.some(
+    (item) => isEmpty(item.value) || isEmpty(item.name)
+  )
+  if (hasEmptyValue) {
+    callback(new Error('存在未填写的枚举值或描述'))
+    return
+  }
+
+  // 检查枚举值是否都是数字
+  const hasInvalidNumber = dataSpecsList.value.some((item) => isNaN(Number(item.value)))
+  if (hasInvalidNumber) {
+    callback(new Error('存在非数字的枚举值'))
+    return
+  }
+
+  // 检查是否有重复的枚举值
+  const values = dataSpecsList.value.map((item) => item.value)
+  const uniqueValues = new Set(values)
+  if (values.length !== uniqueValues.size) {
+    callback(new Error('存在重复的枚举值'))
+    return
+  }
+  callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 142 - 0
src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelNumberDataSpecs.vue

@@ -0,0 +1,142 @@
+<!-- dataType:number 数组类型 -->
+<template>
+  <!--
+  <el-form-item label="取值范围">
+    <div class="flex items-center justify-between">
+      <el-form-item
+        :rules="[
+          { required: true, message: '最小值不能为空' },
+          { validator: validateMin, trigger: 'blur' }
+        ]"
+        class="mb-0"
+        prop="selectOptions.dataSpecs.min"
+      >
+        <el-input v-model="dataSpecs.min" placeholder="请输入最小值" />
+      </el-form-item>
+      <span class="mx-2">~</span>
+      <el-form-item
+        :rules="[
+          { required: true, message: '最大值不能为空' },
+          { validator: validateMax, trigger: 'blur' }
+        ]"
+        class="mb-0"
+        prop="selectOptions.dataSpecs.max"
+      >
+        <el-input v-model="dataSpecs.max" placeholder="请输入最大值" />
+      </el-form-item>
+    </div>
+  </el-form-item>
+  <el-form-item
+    :rules="[
+      { required: true, message: '步长不能为空' },
+      { validator: validateStep, trigger: 'blur' }
+    ]"
+    label="步长"
+    prop="selectOptions.dataSpecs.step"
+  >
+    <el-input v-model="dataSpecs.step" placeholder="请输入步长" />
+  </el-form-item>
+  -->
+
+  <el-form-item
+    :rules="[{ required: true, message: '请选择单位' }]"
+    label="单位"
+    prop="selectOptions.dataSpecs.unit"
+  >
+    <el-select
+      :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
+      filterable
+      placeholder="请选择单位"
+      class="w-1/1"
+      @change="unitChange"
+    >
+      <el-option
+        v-for="(item, index) in getStrDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)"
+        :key="index"
+        :label="item.label + '-' + item.value"
+        :value="item.label + '-' + item.value"
+      />
+    </el-select>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsNumberDataVO } from '../attrsModel/config'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+
+/** 数值型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ModelAttrModelNumberDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberDataVO>
+
+/** 单位发生变化时触发 */
+const unitChange = (UnitSpecs: string) => {
+  const [unitName, unit] = UnitSpecs.split('-')
+  dataSpecs.value.unitName = unitName
+  dataSpecs.value.unit = unit
+}
+
+/** 校验最小值 */
+const validateMin = (_: any, __: any, callback: any) => {
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (isNaN(min)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (max !== undefined && !isNaN(max) && min >= max) {
+    callback(new Error('最小值必须小于最大值'))
+    return
+  }
+
+  callback()
+}
+
+/** 校验最大值 */
+const validateMax = (_: any, __: any, callback: any) => {
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (isNaN(max)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (min !== undefined && !isNaN(min) && max <= min) {
+    callback(new Error('最大值必须大于最小值'))
+    return
+  }
+
+  callback()
+}
+
+/** 校验步长 */
+const validateStep = (_: any, __: any, callback: any) => {
+  const step = Number(dataSpecs.value.step)
+  if (isNaN(step)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+  if (step <= 0) {
+    callback(new Error('步长必须大于0'))
+    return
+  }
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+  if (!isNaN(min) && !isNaN(max) && step > max - min) {
+    callback(new Error('步长不能大于最大值和最小值的差值'))
+    return
+  }
+
+  callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 170 - 0
src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelStructDataSpecs.vue

@@ -0,0 +1,170 @@
+<!-- dataType:struct 数组类型 -->
+<template>
+  <!-- struct 数据展示 -->
+  <el-form-item
+    :rules="[{ required: true, validator: validateList, trigger: 'change' }]"
+    label="JSON 对象"
+  >
+    <div
+      v-for="(item, index) in dataSpecsList"
+      :key="index"
+      class="w-1/1 struct-item flex justify-between px-10px mb-10px"
+    >
+      <span>参数名称:{{ item.name }}</span>
+      <div class="btn">
+        <el-button link type="primary" @click="openStructForm(item)">编辑</el-button>
+        <el-divider direction="vertical" />
+        <el-button link type="danger" @click="deleteStructItem(index)">删除</el-button>
+      </div>
+    </div>
+    <el-button link type="primary" @click="openStructForm(null)">+新增参数</el-button>
+  </el-form-item>
+
+  <!-- struct 表单 -->
+  <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body>
+    <el-form
+      ref="structFormRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="DeviceAttrModelFormRules"
+      label-width="100px"
+    >
+      <el-form-item label="属性名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入属性名称" />
+      </el-form-item>
+      <el-form-item label="标识符" prop="identifier">
+        <el-input v-model="formData.identifier" placeholder="请输入标识符" />
+      </el-form-item>
+      <!-- 属性配置 -->
+      <AttrTemplateModelProperty v-model="formData.selectOptions" is-struct-data-specs />
+    </el-form>
+
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import AttrTemplateModelProperty from '../attrsModel/AttrTemplateModelProperty.vue'
+import { DataSpecsDataType, DeviceAttrModelFormRules } from '../attrsModel/config'
+import { isEmpty } from '@/utils/is'
+
+/** Struct 型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ModelAttrModelStructDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('新增参数') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const structFormRef = ref() // 表单 ref
+const formData = ref<any>({
+  property: {
+    dataType: DataSpecsDataType.INT,
+    dataSpecs: {
+      dataType: DataSpecsDataType.INT
+    }
+  }
+})
+
+/** 打开 struct 表单 */
+const openStructForm = (val: any) => {
+  dialogVisible.value = true
+  resetForm()
+  if (isEmpty(val)) {
+    return
+  }
+  // 编辑时回显数据
+  formData.value = {
+    identifier: val.identifier,
+    name: val.name,
+    description: val.description,
+    property: {
+      dataType: val.childDataType,
+      dataSpecs: val.dataSpecs,
+      dataSpecsList: val.dataSpecsList
+    }
+  }
+}
+
+/** 删除 struct 项 */
+const deleteStructItem = (index: number) => {
+  dataSpecsList.value.splice(index, 1)
+}
+
+/** 添加参数 */
+const submitForm = async () => {
+  await structFormRef.value.validate()
+
+  try {
+    const data = unref(formData)
+    // 构建数据对象
+    const item = {
+      identifier: data.identifier,
+      name: data.name,
+      description: data.description,
+      dataType: DataSpecsDataType.STRUCT,
+      childDataType: data.property.dataType,
+      dataSpecs:
+        !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+          ? data.property.dataSpecs
+          : undefined,
+      dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
+    }
+
+    // 查找是否已有相同 identifier 的项
+    const existingIndex = dataSpecsList.value.findIndex(
+      (spec) => spec.identifier === data.identifier
+    )
+    if (existingIndex > -1) {
+      // 更新已有项
+      dataSpecsList.value[existingIndex] = item
+    } else {
+      // 添加新项
+      dataSpecsList.value.push(item)
+    }
+  } finally {
+    // 隐藏对话框
+    dialogVisible.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    property: {
+      dataType: DataSpecsDataType.INT,
+      dataSpecs: {
+        dataType: DataSpecsDataType.INT
+      }
+    }
+  }
+  structFormRef.value?.resetFields()
+}
+
+/** 校验 struct 不能为空 */
+const validateList = (_: any, __: any, callback: any) => {
+  if (isEmpty(dataSpecsList.value)) {
+    callback(new Error('struct 不能为空'))
+    return
+  }
+  callback()
+}
+
+/** 组件初始化 */
+onMounted(async () => {
+  await nextTick()
+  // 预防 dataSpecsList 空指针
+  isEmpty(dataSpecsList.value) && (dataSpecsList.value = [])
+})
+</script>
+
+<style lang="scss" scoped>
+.struct-item {
+  background-color: #e4f2fd;
+}
+</style>

+ 11 - 0
src/views/pms/modeltemplate/detail/dataSpecs/index.ts

@@ -0,0 +1,11 @@
+import ModelAttrModelEnumDataSpecs from './ModelAttrModelEnumDataSpecs.vue'
+import ModelAttrModelNumberDataSpecs from './ModelAttrModelNumberDataSpecs.vue'
+import ModelAttrModelArrayDataSpecs from './ModelAttrModelArrayDataSpecs.vue'
+import ModelAttrModelStructDataSpecs from './ModelAttrModelStructDataSpecs.vue'
+
+export {
+  ModelAttrModelEnumDataSpecs,
+  ModelAttrModelNumberDataSpecs,
+  ModelAttrModelArrayDataSpecs,
+  ModelAttrModelStructDataSpecs
+}

+ 52 - 0
src/views/pms/modeltemplate/detail/index.vue

@@ -0,0 +1,52 @@
+<template>
+  <TemplateDetailsHeader :loading="loading" :template="template" @refresh="() => getTemplateData(id)" />
+  <el-col>
+    <el-tabs v-model="activeTab">
+      <el-tab-pane label="属性模板" lazy name="attrModel">
+        <ModelAttrTemplate ref="attrModelRef" />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+</template>
+<script lang="ts" setup>
+import * as DeviceTemplateApi from '@/api/pms/devicetemplate'
+import TemplateDetailsHeader from './TemplateDetailsHeader.vue'
+import ModelAttrTemplate from '@/views/pms/modeltemplate/detail/attrsModel/index.vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useRouter } from 'vue-router'
+import {IOT_PROVIDE_KEY} from "@/views/iot/utils/constants";
+
+defineOptions({ name: 'DeviceAttrTemplateDetail' })
+
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter()
+
+const route = useRoute()
+const message = useMessage()
+const id = route.params.id // 编号
+const loading = ref(true) // 加载中
+const template = ref<DeviceTemplateApi.DeviceAttrTemplateVO>({} as DeviceTemplateApi.DeviceAttrTemplateVO) // 模板详情
+const activeTab = ref('attrModel') // 默认为 属性列表 标签页
+
+provide(IOT_PROVIDE_KEY.DEVICE_ATTR_TEMPLATE, template) // 提供设备属性模板信息给详情页的子组件
+
+/** 获取 设备属性模板 详情 */
+const getTemplateData = async (id: number) => {
+  loading.value = true
+  try {
+    template.value = await DeviceTemplateApi.getDeviceTemplate(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 */
+onMounted(async () => {
+  if (!id) {
+    message.warning('参数错误,设备属性模板不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  await getTemplateData(id)
+})
+</script>

+ 294 - 0
src/views/pms/modeltemplate/index.vue

@@ -0,0 +1,294 @@
+<template>
+  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
+  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
+  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
+
+  <el-row :gutter="20">
+    <!-- 左侧 设备分类 树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1">
+        <ModelCategoryTree @node-click="handleModelCategoryTreeNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="20" :xs="24">
+      <!-- 搜索 -->
+      <ContentWrap>
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item label="模板名称" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="请输入模板名称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="模板编码" prop="code">
+            <el-input
+              v-model="queryParams.code"
+              placeholder="请输入模板编码"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="属性模板状态"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </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="datetimerange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+            <el-button
+              type="primary"
+              plain
+              @click="openForm('create')"
+              v-hasPermi="['rq:iot-model-template:create']"
+            >
+              <Icon icon="ep:plus" /> 新增
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+      <ContentWrap>
+        <el-table v-loading="loading" :data="list">
+          <el-table-column
+            label="模板编码"
+            align="center"
+            prop="code"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column
+            label="模板名称"
+            align="center"
+            prop="name"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column
+            label="设备分类名称"
+            align="center"
+            key="deviceCategoryName"
+            prop="deviceCategoryName"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column label="状态" key="status">
+            <template #default="scope">
+              <el-switch
+                v-model="scope.row.status"
+                :active-value="0"
+                :inactive-value="1"
+                @change="handleStatusChange(scope.row)"
+                :disabled="!checkPermi(['rq:iot-model-template:update'])"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="创建时间"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            width="180"
+          />
+          <el-table-column label="操作" align="center" width="160">
+            <template #default="scope">
+              <div class="flex items-center justify-center">
+                <el-button
+                  type="primary"
+                  link
+                  @click="openDetail(scope.row.deviceCategoryId+','+scope.row.deviceCategoryName)"
+                >
+                  <Icon icon="ep:edit" />查看
+                </el-button>
+                <el-button
+                  type="primary"
+                  link
+                  @click="openForm('update', scope.row.id)"
+                  v-hasPermi="['rq:iot-model-template:update']"
+                >
+                  <Icon icon="ep:edit" />修改
+                </el-button>
+                <el-dropdown
+                  @command="(command) => handleCommand(command, scope.row)"
+                  v-hasPermi="[
+                    'rq:iot-model-template:delete'
+                  ]"
+                >
+                  <el-button type="primary" link><Icon icon="ep:d-arrow-right" /> 更多</el-button>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item
+                        command="handleDelete"
+                        v-if="checkPermi(['rq:iot-model-template:delete'])"
+                      >
+                        <Icon icon="ep:delete" />删除
+                      </el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 添加或修改 属性模板 对话框 -->
+  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { checkPermi } from '@/utils/permission'
+import { dateFormatter } from '@/utils/formatTime'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as ModelTemplateApi from '@/api/pms/modeltemplate'
+import TemplateForm from './TemplateForm.vue'
+import ModelCategoryTree from './ModelCategoryTree.vue'
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore';
+
+defineOptions({ name: 'DeviceAttrsTemplate' })
+
+const treeStore = useTreeStore();
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const {push} = useRouter()
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  deviceCategoryId: undefined,
+  deviceCategoryName:undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+// 从 Store 中获取左侧 设备分类树 选中的 节点ID
+const selectedId = computed(() => treeStore.selectedId);
+
+/** 查询 设备属性模板 列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ModelTemplateApi.getModelTemplatePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 处理 设备分类树 被点击 */
+const handleModelCategoryTreeNodeClick = async (row) => {
+  queryParams.modelCategoryId = row.id
+  await getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 打开详情 */
+const openDetail = (id: string) => {
+  push({ name: 'ModelAttrTemplate', params: {id} })
+}
+
+
+/** 修改 设备属性模板 状态 */
+const handleStatusChange = async (row: ModelTemplateApi.ModelAttrTemplateVO) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
+    await message.confirm('确认要"' + text + '""' + row.name + '"属性模板吗?')
+    // 发起修改状态
+    await ModelTemplateApi.updateModelTemplateStatus(row.id, row.status)
+    // 刷新列表
+    await getList()
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 操作分发 */
+const handleCommand = (command: string, row: ModelTemplateApi.ModelAttrTemplateVO) => {
+  switch (command) {
+    case 'handleDelete':
+      handleDelete(row.id)
+      break
+    default:
+      break
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ModelTemplateApi.deleteModelTemplate(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 1 - 1
src/views/system/tree/PmsTree.vue

@@ -33,7 +33,7 @@
             <Icon
               style="vertical-align: middle"
               v-if="node.data.type === 'device'"
-              icon="fa:wrench"
+              icon="ep:collection"
             />
             <el-icon v-if="node.data.type === 'file'" style="vertical-align: middle"
               ><Folder