Ver código fonte

feat(IotDeviceForm): 重构设备台账编辑新建页面,重构几个选择列表弹窗

Zimo 5 dias atrás
pai
commit
d579ff888f

+ 2 - 4
src/components/Pagination/index.vue

@@ -8,17 +8,15 @@
     :page-sizes="[10, 20, 30, 50, 100]"
     :pager-count="pagerCount"
     :total="total"
-    :small="isSmall"
+    :size="isSmall ? 'small' : 'default'"
     class="float-right mb-15px mt-15px"
     layout="total, sizes, prev, pager, next, jumper"
     @size-change="handleSizeChange"
-    @current-change="handleCurrentChange"
-  />
+    @current-change="handleCurrentChange" />
 </template>
 <script lang="ts" setup>
 import { computed, watchEffect } from 'vue'
 import { useAppStore } from '@/store/modules/app'
-
 defineOptions({ name: 'Pagination' })
 
 // 此处解决了当全局size为small的时候分页组件样式太大的问题

+ 4 - 30
src/router/modules/remaining.ts

@@ -512,41 +512,15 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: 'device/detail/add/:type/:deptId',
+        path: 'device/detail/form',
         component: () => import('@/views/pms/device/IotDeviceForm.vue'),
-        name: 'DeviceDetailAdd',
-        meta: {
-          noCache: false,
-          hidden: true,
-          canTo: true,
-          icon: 'ep:add',
-          title: t('rem.AddEquipment'),
-          activeMenu: '/device/base'
-        }
-      },
-      {
-        path: 'device/detail/add',
-        component: () => import('@/views/pms/device/IotDeviceFormAdd.vue'),
-        name: 'DeviceDetailAddd',
-        meta: {
-          noCache: false,
-          hidden: true,
-          canTo: true,
-          icon: 'ep:add',
-          title: t('rem.AddEquipment'),
-          activeMenu: '/device/base'
-        }
-      },
-      {
-        path: 'device/detail/edit/:type/:id(\\d+)',
-        component: () => import('@/views/pms/device/IotDeviceForm.vue'),
-        name: 'DeviceDetailEdit',
+        name: 'DeviceDetailForm',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          icon: 'ep:edit',
-          title: t('rem.EquipmentEditing'),
+          icon: 'ep:document',
+          title: t('rem.AddEquipment'),
           activeMenu: '/device/base'
         }
       },

Diferenças do arquivo suprimidas por serem muito extensas
+ 315 - 690
src/views/pms/device/IotDeviceForm.vue


+ 885 - 0
src/views/pms/device/IotDeviceForm1.vue

@@ -0,0 +1,885 @@
+<template>
+  <ContentWrap v-loading="formLoading">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      style="margin-right: 4em; margin-left: 0.5em"
+      label-width="130px">
+      <div class="title-div">
+        <el-button @click="baseInfoClick" class="title-button">
+          <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
+          <span class="cursor-pointer">{{ t('deviceForm.basic') }}</span>
+          <Icon
+            color="black"
+            :icon="baseIsExpanded ? 'fa-solid:angle-double-down' : 'fa-solid:angle-double-right'"
+            :size="18"
+            class="cursor-pointer" />
+        </el-button>
+      </div>
+      <div class="base-expandable-content" :class="{ 'is-expanded': baseIsExpanded }">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.yfClass')" prop="yfClass">
+              <el-cascader
+                :disabled="formType === 'update' && formData.yfDeviceCode"
+                style="width: 100%"
+                v-model="formData.yfClass"
+                :options="yfclasses"
+                :props="{ expandTrigger: 'hover' }"
+                clearable
+                filterable
+                @change="handleYfClassChange" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.yfCode')" prop="yfDeviceCode">
+              <el-input
+                v-model="formData.yfDeviceCode"
+                :disabled="formData.yfDeviceCode"
+                placeholder="请输入油服设备编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.code')" prop="deviceCode">
+              <el-input
+                v-model="formData.deviceCode"
+                :disabled="formType === 'update'"
+                placeholder="请输入设备编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.name')" prop="deviceName">
+              <lang-input v-model="formData.deviceName" placeholder="请输入设备名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.dept')" prop="deptId">
+              <el-tree-select
+                :disabled="formType === 'update'"
+                v-model="formData.deptId"
+                :data="deptList"
+                :props="defaultProps"
+                check-strictly
+                node-key="id"
+                filterable
+                placeholder="请选择所在部门" />
+              <!--              <el-tree-select-->
+              <!--                v-model="formData.deptId"-->
+              <!--                :data="deptList"-->
+              <!--                :props="defaultProps"-->
+              <!--                check-strictly-->
+              <!--                node-key="id"-->
+              <!--                placeholder="请选择归属部门"-->
+              <!--              />-->
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.category')" prop="assetClass">
+              <el-tree-select
+                :disabled="formType === 'update' && username !== '超级管理员'"
+                v-model="formData.assetClass"
+                :data="productClassifyList"
+                :props="defaultProps"
+                check-strictly
+                node-key="id"
+                :placeholder="t('deviceForm.categoryHolder')"
+                @change="assetclasschange"
+                filterable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.status')" prop="deviceStatus">
+              <el-select
+                v-model="formData.deviceStatus"
+                :placeholder="t('deviceForm.choose')"
+                :disabled="formType === 'update'"
+                clearable>
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_DEVICE_STATUS)"
+                  :key="dict.label"
+                  :label="dict.label"
+                  :value="dict.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.assets')" prop="assetProperty">
+              <el-select v-model="formData.assetProperty" placeholder="请选择" clearable>
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_ASSET_PROPERTY)"
+                  :key="dict.id"
+                  :label="dict.label"
+                  :value="dict.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('iotDevice.brand')" prop="brandName">
+              <el-select
+                clearable
+                v-model="formData.brandName"
+                @clear="brandClear"
+                :placeholder="t('iotDevice.brandHolder')"
+                @click="openForm" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="车牌号" prop="carNo">
+              <el-input clearable v-model="formData.carNo" placeholder="请输入车牌号" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="设备号" prop="deviceNo">
+              <el-input clearable v-model="formData.deviceNo" placeholder="请输入设备号" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <div style="display: flex; flex-direction: row">
+              <el-form-item :label="t('deviceForm.model')" prop="model" style="width: 86%">
+                <el-input
+                  clearable
+                  v-model="formData.model"
+                  :placeholder="t('deviceForm.modelHolder')" />
+              </el-form-item>
+              <el-button type="info" @click="openModelForm">请选择</el-button>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.useProject')" prop="useProject">
+              <el-input v-model="formData.useProject" :disabled="isDetail" height="60px" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.assetOwner')" prop="assetOwnership">
+              <el-input v-model="formData.assetOwnership" :disabled="isDetail" height="60px" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="所在地点" prop="address">
+              <el-input v-model="formData.address" height="60px" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.picture')" prop="picUrl">
+              <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="60px" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.remark')" prop="remark">
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                :placeholder="t('deviceForm.remarkHolder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <div class="title-div">
+        <el-button @click="zzInfoClick" class="title-button">
+          <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
+          <span class="cursor-pointer">{{ t('deviceForm.make') }}</span>
+          <Icon
+            color="black"
+            :icon="zzIsExpanded ? 'fa-solid:angle-double-down' : 'fa-solid:angle-double-right'"
+            :size="18"
+            class="cursor-pointer" />
+        </el-button>
+      </div>
+      <div class="zz-expandable-content" :class="{ 'is-expanded': zzIsExpanded }">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.mfg')" prop="manufacturerId">
+              <el-select
+                clearable
+                @clear="zzClear"
+                v-model="formData.manufacturerName"
+                :placeholder="t('deviceForm.mfgHolder')"
+                @click="openCustomerZz" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.pd')" prop="manDate">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.manDate"
+                type="date"
+                value-format="x"
+                :placeholder="t('deviceForm.pdHolder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.supplier')" prop="supplierId">
+              <el-select
+                clearable
+                @clear="supplierClear"
+                v-model="formData.supplierName"
+                :placeholder="t('deviceForm.suppHolder')"
+                @click="openCustomerSupplier" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.warranty')" prop="expires">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.expires"
+                type="date"
+                value-format="x"
+                :placeholder="t('deviceForm.warrHolder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.enable')" prop="enableDate">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.enableDate"
+                type="date"
+                value-format="x"
+                :placeholder="t('deviceForm.enableHolder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item :label="t('deviceForm.ni')" prop="nameplate">
+              <el-input
+                v-model="formData.nameplate"
+                type="textarea"
+                :placeholder="t('deviceForm.niHolder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <div class="title-div">
+        <el-button @click="cwInfoClick" class="title-button">
+          <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
+          <span class="cursor-pointer">{{ t('deviceForm.finance') }}</span>
+          <Icon
+            color="black"
+            :icon="cwIsExpanded ? 'fa-solid:angle-double-down' : 'fa-solid:angle-double-right'"
+            :size="18"
+            class="cursor-pointer" />
+        </el-button>
+      </div>
+      <div class="cw-expandable-content" :class="{ 'is-expanded': cwIsExpanded }">
+        <el-row>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购价格' : '租赁价格'"
+              prop="plPrice">
+              <el-input
+                v-model="formData.plPrice"
+                @input="handleInput(formData.plPrice, 'plPrice')"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入采购价格' : '请输入租赁价格'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '采购日期' : '租赁日期'"
+              prop="plDate">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.plDate"
+                type="date"
+                value-format="x"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入采购日期' : '请输入租赁日期'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧年限' : '租赁年限'"
+              prop="plYear">
+              <el-input
+                v-model="formData.plYear"
+                type="number"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入折旧年限' : '请输入租赁年限'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '折旧开始日期' : '租赁开始日期'"
+              prop="plStartDate">
+              <el-date-picker
+                style="width: 150%"
+                v-model="formData.plStartDate"
+                type="date"
+                value-format="x"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请选择折旧开始日期' : '请选择租赁开始日期'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧月数' : '已租赁月数'"
+              prop="plMonthed">
+              <el-input
+                v-model="formData.plMonthed"
+                type="number"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入已提折旧月数' : '请输入已租赁月数'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item
+              :label="formData.assetProperty === 'zy' ? '已提折旧金额' : '已租赁金额'"
+              prop="plAmounted">
+              <el-input
+                v-model="formData.plAmounted"
+                @input="handleInput(formData.plAmounted, 'plAmounted')"
+                :placeholder="
+                  formData.assetProperty === 'zy' ? '请输入已提折旧金额' : '已租赁金额'
+                " />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="剩余金额" prop="remainAmount">
+              <el-input
+                v-model="formData.remainAmount"
+                @input="handleInput(formData.remainAmount, 'remainAmount')"
+                placeholder="请输入剩余金额" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="每月折旧金额" prop="monthAmount">
+              <el-input
+                v-model="formData.monthAmount"
+                @input="handleInput(formData.monthAmount, 'monthAmount')"
+                placeholder="请输入每月折旧金额" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="总折旧月份" prop="totalMonth">
+              <el-input
+                v-model="formData.totalMonth"
+                @input="handleInput(formData.totalMonth, 'totalMonth')"
+                placeholder="请输入总折旧月份" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="8">
+            <el-form-item label="币种" prop="currency">
+              <el-input v-model="formData.currency" placeholder="请输入币种" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+      <div class="title-div">
+        <el-button @click="qtInfoClick" class="title-button">
+          <Icon color="black" icon="ep:set-up" :size="18" class="cursor-pointer first-icon" />
+          <span class="cursor-pointer">{{ t('deviceForm.other') }}</span>
+          <Icon
+            color="black"
+            :icon="qtIsExpanded ? 'fa-solid:angle-double-down' : 'fa-solid:angle-double-right'"
+            :size="18"
+            class="cursor-pointer" />
+        </el-button>
+      </div>
+      <div class="qt-expandable-content" :class="{ 'is-expanded': qtIsExpanded }">
+        <el-row>
+          <el-col v-for="field in list" :key="field.sort" :span="8">
+            <el-form-item
+              label-width="180px"
+              :label="field.name"
+              :prop="field.code"
+              :rules="field.rules">
+              <!-- 文本输入 -->
+              <el-input
+                v-if="field.type === 'text'"
+                v-model="formData[field.code]"
+                :placeholder="'请输入' + field.name"
+                :type="field.type || 'text'" />
+
+              <el-select
+                v-else-if="field.type === 'enum'"
+                v-model="formData[field.code]"
+                :placeholder="'请输入' + field.name"
+                clearable
+                filterable>
+                <el-option
+                  v-for="item in field.selectOptions.dataSpecsList"
+                  :key="item.name"
+                  :label="item.name"
+                  :value="item.name" />
+              </el-select>
+
+              <!-- 数字输入 -->
+              <el-input
+                v-else-if="field.type === 'int'"
+                type="number"
+                v-model="formData[field.code]"
+                style="width: 150%" />
+              <el-input
+                v-else-if="field.type === 'double'"
+                v-model="formData[field.code]"
+                @input="handleInput(formData[field.code], field.code)"
+                style="width: 150%" />
+              <!-- 日期选择 -->
+              <el-date-picker
+                v-else-if="field.type === 'date'"
+                v-model="formData[field.code]"
+                :type="field.type || 'date'"
+                :placeholder="'请输入' + field.name"
+                value-format="YYYY-MM-DD"
+                style="width: 150%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </div>
+    </el-form>
+    <el-form>
+      <el-form-item style="float: right">
+        <el-button type="success" @click="allzhankai">{{ t('deviceForm.expand') }}</el-button>
+        <el-button type="info" @click="allshouqi">{{ t('deviceForm.close') }}</el-button>
+        <el-button v-if="!isDetail" :loading="formLoading" type="warning" @click="submitForm">
+          {{ t('deviceForm.save') }}
+        </el-button>
+        <el-button @click="close" type="primary">{{ t('deviceForm.return') }}</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+  <BrandList ref="brandFormRef" @choose="brandChoose" />
+  <ModelList ref="modelFormRef" @choose="modelChoose" :brand="formData.brand" />
+  <CustomerList ref="customerZzFormRef" @choose="customerZzChoose" />
+  <CustomerList ref="customerSupplierFormRef" @choose="customerSupplierChoose" />
+</template>
+<script setup lang="ts">
+import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import BrandList from '@/views/pms/device/BrandList.vue'
+import ModelList from '@/views/pms/device/ModelList.vue'
+import CustomerList from '@/views/pms/device/CustomerList.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DeviceAttrModelApi } from '@/api/pms/deviceattrmodel'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { IotYfClassifyApi } from '@/api/pms/yfclass'
+import { useRefreshStore } from '@/store/modules/pms/refreshStore'
+import { watch } from 'vue'
+
+/** 设备台账 表单 */
+defineOptions({ name: 'DeviceDetailAdd' })
+const baseIsExpanded = ref(true) // 控制表单是否展开的变量
+const zzIsExpanded = ref(true) // 控制表单是否展开的变量
+const cwIsExpanded = ref(true) // 控制表单是否展开的变量
+const qtIsExpanded = ref(true) // 控制表单是否展开的变量
+
+const username = ref('')
+const deptList = ref<Tree[]>([]) // 树形结构
+const productClassifyList = ref<Tree[]>([]) // 树形结构
+const { delView } = useTagsViewStore() // 视图操作
+const { params, name, query } = useRoute() // 查询参数
+const { currentRoute, push } = useRouter()
+const { wsCache } = useCache()
+const id = params.id
+const type = params.type
+const deptId = params.deptId
+const isDetail = params.isDetail
+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 brandLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const zzLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const supplierLabel = ref('') // 表单的类型:create - 新增;update - 修改
+const yfclasses = ref([])
+const refreshStore = useRefreshStore()
+
+const formData = ref({
+  id: undefined,
+  yfClass: undefined,
+  yfDeviceCode: undefined,
+  enableDate: undefined,
+  deviceCode: undefined,
+  deviceName: undefined,
+  brand: undefined,
+  brandName: undefined,
+  model: undefined,
+  deptId: undefined,
+  deviceStatus: undefined,
+  assetProperty: undefined,
+  picUrl: undefined,
+  assetOwnership: undefined,
+  remark: undefined,
+  useProject: undefined,
+  manufacturerId: undefined,
+  manufacturerName: undefined,
+  supplierId: undefined,
+  supplierName: undefined,
+  manDate: undefined,
+  nameplate: undefined,
+  expires: undefined,
+  plPrice: undefined,
+  plDate: undefined,
+  plYear: undefined,
+  plStartDate: undefined,
+  plMonthed: undefined,
+  plAmounted: undefined,
+  remainAmount: undefined,
+  infoId: undefined,
+  infoType: undefined,
+  infoName: undefined,
+  infoRemark: undefined,
+  infoUrl: undefined,
+  templateJson: undefined,
+  assetClass: undefined,
+  carNo: undefined,
+  deviceNo: undefined,
+  address: undefined,
+  monthAmount: undefined,
+  totalMonth: undefined,
+  currency: undefined
+})
+const formRules = reactive({
+  yfClass: [
+    {
+      validator: (rule, value, callback) => {
+        // 当资产性质为租赁('zl')时,yfClass非必填;否则必填
+        if (formData.value.assetProperty === 'zl' || value) {
+          callback() // 租赁资产或有值时通过验证
+        } else {
+          callback(new Error('编码类别不能为空')) // 非租赁资产且无值时失败
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  yfDeviceCode: [{ required: true, message: '油服编码不能为空', trigger: 'blur' }],
+  assetClass: [{ required: true, message: '资产类别不能为空', trigger: 'blur' }],
+  deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
+  deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+  brandName: [{ required: true, message: '品牌不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }],
+  deviceStatus: [{ required: true, message: '设备状态不能为空', trigger: 'blur' }],
+  assetProperty: [{ required: true, message: '资产性质不能为空', trigger: 'blur' }],
+  manufacturerId: [{ required: true, message: '制造商id不能为空', trigger: 'blur' }],
+  manDate: [{ required: true, message: '生产日期不能为空', trigger: 'blur' }]
+})
+
+const list = ref([])
+const handleYfClassChange = async (value) => {
+  console.log(value)
+  const prefix = value.join('')
+  const last = await IotDeviceApi.getMaxCode(prefix)
+  formData.value.yfDeviceCode = prefix + last
+}
+const assetclasschange = () => {
+  const assetClass = formData.value.assetClass
+  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
+    } else {
+      list.value = []
+    }
+  })
+}
+
+watch(
+  () => formData.value.assetProperty,
+  (newVal) => {
+    nextTick(() => {
+      if (formRef.value) {
+        // 重新验证 yfClass 和 yfCode 字段
+        formRef.value.validateField('yfClass')
+      }
+    })
+  },
+  { immediate: true }
+)
+
+const brandChoose = (row) => {
+  formData.value.brand = row.id
+  // brandLabel.value = row.value
+  formData.value.brandName = row.label
+}
+const brandClear = () => {
+  formData.value.brand = undefined
+  formData.value.brandName = undefined
+}
+const modelChoose = (row) => {
+  formData.value.model = row.name
+}
+const customerSupplierChoose = (row) => {
+  formData.value.supplierId = row.id
+  formData.value.supplierName = row.name
+  // supplierLabel.value = row.name
+}
+const supplierClear = (row) => {
+  formData.value.supplierId = undefined
+  formData.value.supplierName = undefined
+}
+const customerZzChoose = (row) => {
+  formData.value.manufacturerId = row.id
+  // zzLabel.value = row.name
+  formData.value.manufacturerName = row.name
+}
+const zzClear = () => {
+  formData.value.manufacturerId = undefined
+  formData.value.manufacturerName = undefined
+}
+/** 添加/修改操作 */
+const brandFormRef = ref()
+const openForm = () => {
+  brandFormRef.value.open()
+}
+const modelFormRef = ref()
+const openModelForm = () => {
+  modelFormRef.value.open()
+}
+const customerSupplierFormRef = ref()
+const openCustomerSupplier = () => {
+  customerSupplierFormRef.value.open()
+}
+const customerZzFormRef = ref()
+const openCustomerZz = () => {
+  customerZzFormRef.value.open()
+}
+const allshouqi = () => {
+  baseIsExpanded.value = false
+  zzIsExpanded.value = false
+  cwIsExpanded.value = false
+  qtIsExpanded.value = false
+}
+const allzhankai = () => {
+  baseIsExpanded.value = true
+  zzIsExpanded.value = true
+  cwIsExpanded.value = true
+  qtIsExpanded.value = true
+}
+
+const handleInput = (value, obj) => {
+  // 1. 过滤非法字符(只允许数字和小数点)
+  let filtered = value.replace(/[^\d.]/g, '')
+
+  // 2. 处理多个小数点的情况
+  filtered = filtered.replace(/\.{2,}/g, '.')
+
+  // 3. 限制小数点后最多两位
+  let decimalParts = filtered.split('.')
+  if (decimalParts.length > 1) {
+    decimalParts = decimalParts.slice(0, 2)
+    filtered = decimalParts.join('.')
+  }
+
+  // 4. 处理以小数点开头的情况(自动补0)
+  if (filtered.startsWith('.')) {
+    filtered = '0' + filtered
+  }
+
+  // 5. 更新绑定值(同时处理连续输入多个0的情况)
+  formData.value[obj] = filtered.replace(/^0+(?=\d)/, '')
+}
+
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'IotDevicePms', params: {} })
+  // delView(unref(currentRoute))
+  // push({
+  //   name: 'IotDevicePms',
+  //   query: {
+  //     date: new Date().getTime()
+  //   }
+  // })
+}
+const baseInfoClick = () => {
+  baseIsExpanded.value = !baseIsExpanded.value // 切换展开状态
+}
+const zzInfoClick = () => {
+  zzIsExpanded.value = !zzIsExpanded.value // 切换展开状态
+}
+const cwInfoClick = () => {
+  cwIsExpanded.value = !cwIsExpanded.value // 切换展开状态
+}
+const qtInfoClick = () => {
+  qtIsExpanded.value = !qtIsExpanded.value // 切换展开状态
+}
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const formRef = ref()
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    if (list.value) {
+      list.value = list.value.map((item) => ({
+        ...item,
+        value: formData.value[item.code] // 自定义属性生成逻辑
+      }))
+      formData.value.templateJson = JSON.stringify(list.value)
+    }
+    formData.value.yfClass = formData.value.yfClass.join(',')
+    const data = formData.value as unknown as IotDeviceVO
+    if (formType.value === 'create') {
+      await IotDeviceApi.createIotDevice(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotDeviceApi.updateIotDevice(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    //emit('success')
+    const sourcePage = query.source as string
+
+    // 如果有来源页面标识,触发原页面的刷新
+    if (sourcePage) {
+      refreshStore.triggerRefresh(sourcePage)
+    }
+    close()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+onMounted(async () => {
+  const userInfo = wsCache.get(CACHE_KEY.USER)
+  // NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
+  username.value = userInfo.user.nickname
+  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
+      formData.value.brandName = iotDevice.brandName
+      formData.value.manufacturerName = iotDevice.zzName
+      formData.value.supplierName = iotDevice.supplierName
+      formData.value.carNo = iotDevice.carNo
+      formData.value.deviceNo = iotDevice.deviceNo
+      formData.value.address = iotDevice.address
+      if (iotDevice.yfClass) {
+        formData.value.yfClass = iotDevice.yfClass.split(',')
+      }
+      list.value = JSON.parse(iotDevice.templateJson)
+      list.value.forEach((item) => {
+        formData.value[item.code] = item.value
+      })
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    if (deptId) {
+      formData.value.deptId = Number(deptId)
+    }
+    formType.value = 'create'
+  }
+  await IotYfClassifyApi.getChildrenList().then((res) => {
+    yfclasses.value = res
+  })
+})
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceCode: undefined,
+    deviceName: undefined,
+    brand: undefined,
+    model: undefined,
+    deptId: undefined,
+    deviceStatus: undefined,
+    assetProperty: undefined,
+    picUrl: undefined,
+    remark: undefined,
+    manufacturerId: undefined,
+    supplierId: undefined,
+    manDate: undefined,
+    nameplate: undefined,
+    expires: undefined,
+    plPrice: undefined,
+    plDate: undefined,
+    plYear: undefined,
+    plStartDate: undefined,
+    plMonthed: undefined,
+    plAmounted: undefined,
+    remainAmount: undefined,
+    infoId: undefined,
+    infoType: undefined,
+    infoName: undefined,
+    infoRemark: undefined,
+    infoUrl: undefined,
+    templateJson: undefined,
+    assetClass: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>
+
+<style scoped lang="scss">
+.base-expandable-content {
+  max-height: 0; /* 初始高度为0 */
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+
+.base-expandable-content.is-expanded {
+  min-height: 350px; /* 或者根据内容设定一个合适的最大高度 */
+}
+.zz-expandable-content {
+  max-height: 0; /* 初始高度为0 */
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+
+.zz-expandable-content.is-expanded {
+  min-height: 130px; /* 或者根据内容设定一个合适的最大高度 */
+}
+.cw-expandable-content {
+  max-height: 0; /* 初始高度为0 */
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+
+.cw-expandable-content.is-expanded {
+  max-height: 200px; /* 或者根据内容设定一个合适的最大高度 */
+}
+.qt-expandable-content {
+  max-height: 0; /* 初始高度为0 */
+  overflow: hidden; /* 隐藏溢出的内容 */
+  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+}
+
+.qt-expandable-content.is-expanded {
+  max-height: 1200px; /* 或者根据内容设定一个合适的最大高度 */
+}
+.title-button {
+  font-size: 18px;
+  border: none;
+}
+.title-div {
+  margin-bottom: 20px;
+  margin-top: 10px;
+}
+.cursor-pointer {
+  vertical-align: middle;
+}
+.first-icon {
+  margin-bottom: 2px;
+}
+</style>

+ 254 - 0
src/views/pms/device/components/select-list/brand-list.vue

@@ -0,0 +1,254 @@
+<script lang="ts" setup>
+import * as DictDataApi from '@/api/system/dict/dict.data'
+import type { DictDataVO } from '@/api/system/dict/dict.data'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+const emit = defineEmits<{
+  choose: [row: DictDataVO]
+}>()
+
+const { ZmTable, ZmTableColumn } = useTableComponents<DictDataVO>()
+
+const visible = ref(false)
+const loading = ref(false)
+const queryFormRef = ref()
+const list = ref<DictDataVO[]>([])
+const total = ref(0)
+const selectedRow = ref<DictDataVO | null>(null)
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  label: '',
+  status: undefined as number | undefined,
+  dictType: 'pms_device_brand'
+})
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DictDataApi.getDictDataPage(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 chooseRow = (row: DictDataVO) => {
+  selectedRow.value = row
+}
+
+const confirmChoose = () => {
+  if (!selectedRow.value) return
+  emit('choose', selectedRow.value)
+  visible.value = false
+}
+
+const cancelChoose = () => {
+  visible.value = false
+}
+
+const open = async () => {
+  visible.value = true
+  selectedRow.value = null
+  queryParams.pageNo = 1
+  queryParams.label = ''
+  queryParams.status = undefined
+  await getList()
+}
+
+defineExpose({ open })
+</script>
+
+<template>
+  <Dialog v-model="visible" title="选择品牌" style="width: 1100px" class="brand-select-dialog">
+    <div class="brand-select">
+      <el-form
+        ref="queryFormRef"
+        :model="queryParams"
+        class="brand-select__toolbar"
+        label-width="68px"
+        size="default">
+        <div class="brand-select__filters">
+          <el-form-item label="字典标签" prop="label">
+            <el-input
+              v-model="queryParams.label"
+              placeholder="请输入字典标签"
+              clearable
+              class="brand-select__control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="数据状态"
+              clearable
+              class="brand-select__control">
+              <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>
+        </div>
+        <div class="brand-select__actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+        </div>
+      </el-form>
+
+      <div class="brand-select__table-panel">
+        <ZmTable
+          :data="list"
+          :loading="loading"
+          :max-height="430"
+          :show-border="true"
+          align="center"
+          @row-click="chooseRow">
+          <ZmTableColumn label="选择" width="70" hide-in-column-settings>
+            <template #default="{ row }">
+              <el-radio
+                :model-value="selectedRow?.id"
+                :value="row.id"
+                class="brand-select__radio"
+                @click.stop="chooseRow(row)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="id" label="字典编码" width="120" />
+          <ZmTableColumn prop="label" label="字典标签" min-width="180" />
+          <ZmTableColumn prop="value" label="字典键值" min-width="180" />
+          <ZmTableColumn prop="status" label="状态" width="120">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status ?? 0" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="remark" label="备注" min-width="160" />
+          <ZmTableColumn
+            prop="createTime"
+            label="创建时间"
+            width="180"
+            :formatter="dateFormatter" />
+        </ZmTable>
+
+        <div class="brand-select__pagination">
+          <Pagination
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            :total="total"
+            @pagination="getList" />
+        </div>
+      </div>
+
+      <div class="brand-select__footer">
+        <el-button size="default" @click="cancelChoose">取消</el-button>
+        <el-button size="default" type="primary" :disabled="!selectedRow" @click="confirmChoose">
+          确定
+        </el-button>
+      </div>
+    </div>
+  </Dialog>
+</template>
+
+<style scoped lang="scss">
+.brand-select {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.brand-select__toolbar {
+  display: flex;
+  padding: 14px 16px;
+  background: linear-gradient(135deg, rgb(64 158 255 / 6%) 0%, rgb(255 255 255 / 0%) 100%);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.brand-select__filters {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px 24px;
+}
+
+.brand-select__toolbar :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.brand-select__control {
+  width: 240px;
+}
+
+.brand-select__actions {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  gap: 8px;
+}
+
+.brand-select__table-panel {
+  padding: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.brand-select__radio {
+  height: 20px;
+}
+
+.brand-select__radio :deep(.el-radio__label) {
+  display: none;
+}
+
+.brand-select__pagination {
+  display: flex;
+  padding-top: 12px;
+  justify-content: flex-end;
+}
+
+.brand-select__footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+@media (width <= 900px) {
+  .brand-select__toolbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .brand-select__actions {
+    justify-content: flex-end;
+  }
+
+  .brand-select__control {
+    width: 100%;
+  }
+}
+</style>

+ 256 - 0
src/views/pms/device/components/select-list/customer-list.vue

@@ -0,0 +1,256 @@
+<script lang="ts" setup>
+import { Api, type SupplierVO } from '@/api/supplier/base'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+interface SupplierRow extends SupplierVO {
+  createTime?: string | number | Date
+}
+
+const emit = defineEmits<{
+  choose: [row: SupplierRow]
+}>()
+
+const { ZmTable, ZmTableColumn } = useTableComponents<SupplierRow>()
+
+const visible = ref(false)
+const loading = ref(false)
+const title = ref('选择客商')
+const queryFormRef = ref()
+const list = ref<SupplierRow[]>([])
+const total = ref(0)
+const selectedRow = ref<SupplierRow | null>(null)
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined as string | undefined,
+  code: undefined as string | undefined
+})
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await Api.getPage(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 chooseRow = (row: SupplierRow) => {
+  selectedRow.value = row
+}
+
+const confirmChoose = () => {
+  if (!selectedRow.value) return
+  emit('choose', selectedRow.value)
+  visible.value = false
+}
+
+const cancelChoose = () => {
+  visible.value = false
+}
+
+const open = async (dialogTitle = '选择客商') => {
+  title.value = dialogTitle
+  visible.value = true
+  selectedRow.value = null
+  queryParams.pageNo = 1
+  queryParams.name = undefined
+  queryParams.code = undefined
+  await getList()
+}
+
+defineExpose({ open })
+</script>
+
+<template>
+  <Dialog v-model="visible" :title="title" style="width: 1100px" class="customer-select-dialog">
+    <div class="customer-select">
+      <el-form
+        ref="queryFormRef"
+        :model="queryParams"
+        class="customer-select__toolbar"
+        label-width="68px"
+        size="default">
+        <div class="customer-select__filters">
+          <el-form-item label="客商名称" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="请输入客商名称"
+              clearable
+              class="customer-select__control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="客商编号" prop="code">
+            <el-input
+              v-model="queryParams.code"
+              placeholder="请输入客商编号"
+              clearable
+              class="customer-select__control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+        </div>
+        <div class="customer-select__actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+        </div>
+      </el-form>
+
+      <div class="customer-select__table-panel">
+        <ZmTable
+          :data="list"
+          :loading="loading"
+          :max-height="430"
+          :show-border="true"
+          align="center"
+          @row-click="chooseRow">
+          <ZmTableColumn label="选择" width="70" hide-in-column-settings>
+            <template #default="{ row }">
+              <el-radio
+                :model-value="selectedRow?.id"
+                :value="row.id"
+                class="customer-select__radio"
+                @click.stop="chooseRow(row)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="name" label="客商名称" min-width="180" />
+          <ZmTableColumn prop="code" label="客商编号" min-width="150" />
+          <ZmTableColumn prop="classification" label="客商分类" width="140">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.SUPPLIER_TYPE" :value="row.classification ?? 0" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="status" label="状态" width="120">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.SUPPLIER_STATUS" :value="row.status ?? 0" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn
+            prop="createTime"
+            label="创建时间"
+            width="180"
+            :formatter="dateFormatter" />
+        </ZmTable>
+
+        <div class="customer-select__pagination">
+          <Pagination
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            :total="total"
+            @pagination="getList" />
+        </div>
+      </div>
+
+      <div class="customer-select__footer">
+        <el-button size="default" @click="cancelChoose">取消</el-button>
+        <el-button size="default" type="primary" :disabled="!selectedRow" @click="confirmChoose">
+          确定
+        </el-button>
+      </div>
+    </div>
+  </Dialog>
+</template>
+
+<style scoped lang="scss">
+.customer-select {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.customer-select__toolbar {
+  display: flex;
+  padding: 14px 16px;
+  background: linear-gradient(135deg, rgb(64 158 255 / 6%) 0%, rgb(255 255 255 / 0%) 100%);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.customer-select__filters {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px 24px;
+}
+
+.customer-select__toolbar :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.customer-select__control {
+  width: 220px;
+}
+
+.customer-select__actions {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  gap: 8px;
+}
+
+.customer-select__table-panel {
+  padding: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.customer-select__radio {
+  height: 20px;
+}
+
+.customer-select__radio :deep(.el-radio__label) {
+  display: none;
+}
+
+.customer-select__pagination {
+  display: flex;
+  padding-top: 12px;
+  justify-content: flex-end;
+}
+
+.customer-select__footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+@media (width <= 900px) {
+  .customer-select__toolbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .customer-select__actions {
+    justify-content: flex-end;
+  }
+
+  .customer-select__control {
+    width: 100%;
+  }
+}
+</style>

+ 262 - 0
src/views/pms/device/components/select-list/model-list.vue

@@ -0,0 +1,262 @@
+<script lang="ts" setup>
+import { IotModelApi, type IotModelVO } from '@/api/pms/model'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+type BrandValue = number | string | Array<number | string>
+
+interface ModelRow extends IotModelVO {
+  createTime?: string | number | Date
+}
+
+const props = defineProps<{
+  brand?: BrandValue
+}>()
+
+const emit = defineEmits<{
+  choose: [row: ModelRow]
+}>()
+
+const { ZmTable, ZmTableColumn } = useTableComponents<ModelRow>()
+
+const visible = ref(false)
+const loading = ref(false)
+const queryFormRef = ref()
+const list = ref<ModelRow[]>([])
+const total = ref(0)
+const selectedRow = ref<ModelRow | null>(null)
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  brand: undefined as BrandValue | undefined,
+  name: '',
+  status: undefined as number | undefined
+})
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotModelApi.getIotModelPage(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 chooseRow = (row: ModelRow) => {
+  selectedRow.value = row
+}
+
+const confirmChoose = () => {
+  if (!selectedRow.value) return
+  emit('choose', selectedRow.value)
+  visible.value = false
+}
+
+const cancelChoose = () => {
+  visible.value = false
+}
+
+const open = async () => {
+  visible.value = true
+  selectedRow.value = null
+  queryParams.pageNo = 1
+  queryParams.name = ''
+  queryParams.status = undefined
+  queryParams.brand = props.brand
+  await getList()
+}
+
+defineExpose({ open })
+</script>
+
+<template>
+  <Dialog v-model="visible" title="选择规格型号" style="width: 1100px" class="model-select-dialog">
+    <div class="model-select">
+      <el-form
+        ref="queryFormRef"
+        :model="queryParams"
+        class="model-select__toolbar"
+        label-width="68px"
+        size="default">
+        <div class="model-select__filters">
+          <el-form-item label="型号名称" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="请输入型号名称"
+              clearable
+              class="model-select__control"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="数据状态"
+              clearable
+              class="model-select__control">
+              <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>
+        </div>
+        <div class="model-select__actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />
+            搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />
+            重置
+          </el-button>
+        </div>
+      </el-form>
+
+      <div class="model-select__table-panel">
+        <ZmTable
+          :data="list"
+          :loading="loading"
+          :max-height="430"
+          :show-border="true"
+          align="center"
+          @row-click="chooseRow">
+          <ZmTableColumn label="选择" width="70" hide-in-column-settings>
+            <template #default="{ row }">
+              <el-radio
+                :model-value="selectedRow?.id"
+                :value="row.id"
+                class="model-select__radio"
+                @click.stop="chooseRow(row)" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn prop="name" label="型号名称" min-width="180" />
+          <ZmTableColumn prop="standard" label="符合标准" min-width="180" />
+          <ZmTableColumn prop="status" label="状态" width="120">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status ?? 0" />
+            </template>
+          </ZmTableColumn>
+          <ZmTableColumn
+            prop="createTime"
+            label="创建时间"
+            width="180"
+            :formatter="dateFormatter" />
+        </ZmTable>
+
+        <div class="model-select__pagination">
+          <Pagination
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            :total="total"
+            @pagination="getList" />
+        </div>
+      </div>
+
+      <div class="model-select__footer">
+        <el-button size="default" @click="cancelChoose">取消</el-button>
+        <el-button size="default" type="primary" :disabled="!selectedRow" @click="confirmChoose">
+          确定
+        </el-button>
+      </div>
+    </div>
+  </Dialog>
+</template>
+
+<style scoped lang="scss">
+.model-select {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.model-select__toolbar {
+  display: flex;
+  padding: 14px 16px;
+  background: linear-gradient(135deg, rgb(64 158 255 / 6%) 0%, rgb(255 255 255 / 0%) 100%);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+}
+
+.model-select__filters {
+  display: flex;
+  min-width: 0;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px 24px;
+}
+
+.model-select__toolbar :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.model-select__control {
+  width: 240px;
+}
+
+.model-select__actions {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  gap: 8px;
+}
+
+.model-select__table-panel {
+  padding: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.model-select__radio {
+  height: 20px;
+}
+
+.model-select__radio :deep(.el-radio__label) {
+  display: none;
+}
+
+.model-select__pagination {
+  display: flex;
+  padding-top: 12px;
+  justify-content: flex-end;
+}
+
+.model-select__footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+
+@media (width <= 900px) {
+  .model-select__toolbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .model-select__actions {
+    justify-content: flex-end;
+  }
+
+  .model-select__control {
+    width: 100%;
+  }
+}
+</style>

+ 9 - 3
src/views/pms/device/index.vue

@@ -970,14 +970,20 @@ const resetQuery = () => {
 const openForm = (type: string, id?: number, deptId?: number) => {
   //修改
   if (typeof id === 'number') {
-    push({ name: 'DeviceDetailEdit', params: { type, id }, query: { source: 'devicerouter' } })
+    push({
+      name: 'DeviceDetailForm',
+      query: { source: 'devicerouter', type, id, deptId }
+    })
     return
   }
   // 新增
   if (deptId) {
-    push({ name: 'DeviceDetailAdd', params: { type, deptId }, query: { source: 'devicerouter' } })
+    push({
+      name: 'DeviceDetailForm',
+      query: { source: 'devicerouter', type, deptId }
+    })
   } else {
-    push({ name: 'DeviceDetailAddd', params: {}, query: { source: 'devicerouter' } })
+    push({ name: 'DeviceDetailForm', query: { source: 'devicerouter' } })
   }
 }
 

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff