Răsfoiți Sursa

Merge remote-tracking branch 'origin/master'

lipenghui 3 luni în urmă
părinte
comite
11a5fe9379
26 a modificat fișierele cu 3216 adăugiri și 2 ștergeri
  1. 2 1
      src/api/pms/deviceattrmodel/index.ts
  2. 52 0
      src/api/pms/iotopeationfill/index.ts
  3. 80 0
      src/api/pms/modelattrtemplate/index.ts
  4. 59 0
      src/api/pms/modeltemplate/index.ts
  5. 22 1
      src/router/modules/remaining.ts
  6. 1 0
      src/utils/dict.ts
  7. 178 0
      src/views/pms/iotopeationfill/IotOpeationFillForm.vue
  8. 247 0
      src/views/pms/iotopeationfill/index.vue
  9. 263 0
      src/views/pms/iotopeationmodel/IotOpeationModelForm.vue
  10. 306 0
      src/views/pms/iotopeationmodel/index.vue
  11. 125 0
      src/views/pms/modeltemplate/ModelCategoryTree.vue
  12. 168 0
      src/views/pms/modeltemplate/TemplateForm.vue
  13. 34 0
      src/views/pms/modeltemplate/detail/TemplateDetailsHeader.vue
  14. 218 0
      src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelForm.vue
  15. 171 0
      src/views/pms/modeltemplate/detail/attrsModel/AttrTemplateModelProperty.vue
  16. 182 0
      src/views/pms/modeltemplate/detail/attrsModel/config.ts
  17. 173 0
      src/views/pms/modeltemplate/detail/attrsModel/index.vue
  18. 48 0
      src/views/pms/modeltemplate/detail/components/DataDefinition.vue
  19. 3 0
      src/views/pms/modeltemplate/detail/components/index.ts
  20. 52 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelArrayDataSpecs.vue
  21. 163 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelEnumDataSpecs.vue
  22. 142 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelNumberDataSpecs.vue
  23. 170 0
      src/views/pms/modeltemplate/detail/dataSpecs/ModelAttrModelStructDataSpecs.vue
  24. 11 0
      src/views/pms/modeltemplate/detail/dataSpecs/index.ts
  25. 52 0
      src/views/pms/modeltemplate/detail/index.vue
  26. 294 0
      src/views/pms/modeltemplate/index.vue

+ 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 })
+  },
+}

+ 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,
@@ -325,7 +347,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',//是否数采
 }

+ 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>

+ 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>