浏览代码

Merge branch 'qhse_file' of shuzhihua/pms-iot-vue into master

yanghao 1 周之前
父节点
当前提交
5314b64c6c

+ 73 - 0
src/api/pms/qhse/index.ts

@@ -663,3 +663,76 @@ export const EmergencyDrillCertApi = {
     return await request.download({ url: `/rq/qhse-emergency-cert/export-excel`, params })
     return await request.download({ url: `/rq/qhse-emergency-cert/export-excel`, params })
   }
   }
 }
 }
+
+// 资料库
+export const DataLibraryApi = {
+  // // 获得资料库分页
+  // getDataLibraryList: async (params) => {
+  //   return await request.get({ url: `/rq/qhse-info/page`, params })
+  // },
+  // // 删除资料库
+  // deleteDataLibrary: async (id) => {
+  //   return await request.delete({ url: `/rq/qhse-info/delete?id=` + id })
+  // },
+
+  // // 添加资料库
+  // createDataLibrary: async (data) => {
+  //   return await request.post({ url: `/rq/qhse-info/create`, data })
+  // },
+  // // 获取详情
+  // getDataLibrary: async (id) => {
+  //   return await request.get({ url: `/rq/qhse-info/get?id=` + id })
+  // },
+  // // 修改资料库
+  // updateDataLibrary: async (data) => {
+  //   return await request.put({ url: `/rq/qhse-info/update`, data })
+  // },
+
+  // // 导出资料库 Excel
+  // exportDataLibrary: async (params) => {
+  //   return await request.download({ url: `/rq/qhse-info/export-excel`, params })
+  // },
+
+  // getFileTree: async () => {
+  //   return await request.get({ url: '/rq/qhse-file-tree/page' })
+  // }
+
+  // 查询资料分页
+  getIotInfoPage: async (params: any) => {
+    return await request.get({ url: `/rq/qhse-info/page`, params })
+  },
+  // 查询资料库的子目录及文件
+  getChildContentFile: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/child/content-file`, params })
+  },
+  getAllChildContentFile: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/all/content-file`, params })
+  },
+  getIotInfoFilePage: async (params: any) => {
+    return await request.post({ url: `/rq/qhse-info/file/page`, params })
+  },
+  // 查询资料详情
+  getIotInfo: async (id: number) => {
+    return await request.get({ url: `/rq/qhse-info/get?id=` + id })
+  },
+
+  // 新增资料
+  createIotInfo: async (data) => {
+    return await request.post({ url: `/rq/qhse-info/create`, data })
+  },
+
+  // 修改资料
+  updateIotInfo: async (data) => {
+    return await request.put({ url: `/rq/qhse-info/update`, data })
+  },
+
+  // 删除资料
+  deleteIotInfo: async (id: number) => {
+    return await request.delete({ url: `/rq/qhse-info/delete?id=` + id })
+  },
+
+  // 导出资料 Excel
+  exportIotInfo: async (params) => {
+    return await request.download({ url: `/rq/qhse-info/export-excel`, params })
+  }
+}

+ 3 - 1
src/utils/dict.ts

@@ -320,6 +320,7 @@ export enum DICT_TYPE {
 
 
   // QHSE
   // QHSE
   MEASURE_TYPE = 'measure_type',
   MEASURE_TYPE = 'measure_type',
+  MEASURE_STATUS = 'measure_status',
   PERSON_CERT = 'person_cert',
   PERSON_CERT = 'person_cert',
   ORG_CERT = 'org_cert',
   ORG_CERT = 'org_cert',
   DANGER_GRADE = 'danger_grade',
   DANGER_GRADE = 'danger_grade',
@@ -328,7 +329,8 @@ export enum DICT_TYPE {
   QHSE_PTW_TYPE = 'qhse_ptw_type',
   QHSE_PTW_TYPE = 'qhse_ptw_type',
   QHSE_PTW_GRADE = 'qhse_ptw_grade',
   QHSE_PTW_GRADE = 'qhse_ptw_grade',
   QHSE_HAZARD_SOURCE = 'hazard_soure',
   QHSE_HAZARD_SOURCE = 'hazard_soure',
-  QHSE_DEVICE_CERT_TYPE = 'qhse_device_cert_type'
+  QHSE_DEVICE_CERT_TYPE = 'qhse_device_cert_type',
+  QHSE_DETECT_CYCLE = 'detect_cycle'
 }
 }
 
 
 export function realValue(type: any, value: string) {
 export function realValue(type: any, value: string) {

+ 8 - 1
src/views/pms/qhse/MeasureCertDrawer.vue

@@ -85,6 +85,13 @@
                   }}</span>
                   }}</span>
                 </template>
                 </template>
               </zm-table-column>
               </zm-table-column>
+
+              <zm-table-column label="检测周期" align="center" width="140">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.QHSE_DETECT_CYCLE" :value="scope.row.detectCycle" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="精度" align="center" prop="precisions" />
               <zm-table-column label="校准金额" align="center" prop="detectAmount" />
               <zm-table-column label="校准金额" align="center" prop="detectAmount" />
               <zm-table-column label="附件" fixed="right" align="center" prop="file" width="90">
               <zm-table-column label="附件" fixed="right" align="center" prop="file" width="90">
                 <template #default="scope">
                 <template #default="scope">
@@ -144,7 +151,7 @@ import { IotMeasureDetectApi } from '@/api/pms/qhse/index'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { formatDate } from '@/utils/formatTime'
 import { formatDate } from '@/utils/formatTime'
 import IotMeasureDetectForm from './iotmeasuredetect/IotMeasureDetectForm.vue'
 import IotMeasureDetectForm from './iotmeasuredetect/IotMeasureDetectForm.vue'
-
+import { DICT_TYPE } from '@/utils/dict'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 const { ZmTable, ZmTableColumn } = useTableComponents()
 
 
 defineOptions({ name: 'MeasureCertDrawer' })
 defineOptions({ name: 'MeasureCertDrawer' })

+ 201 - 0
src/views/pms/qhse/data/IotInfoFormTree.vue

@@ -0,0 +1,201 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" style="width: 50vw">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading">
+      <el-form-item :label="t('fileInfo.fileDirectory')" prop="classId">
+        <el-tree-select
+          disabled="true"
+          v-model="formData.classId"
+          :data="fileList"
+          :props="defaultProps"
+          check-strictly
+          placeholder="请选择文件目录"
+          value-key="id" />
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.appendix')" prop="filePath" style="">
+        <file-upload
+          style="height: 300px"
+          @uploadSuccess="handleUploadSuccess"
+          @uploadError="handleUploadError"
+          @uploadComplete="handleUploadComplete"
+          :deviceId="props.classId" />
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.fileType')" prop="fileType">
+        <el-select
+          v-model="formData.fileType"
+          placeholder="请选择"
+          :disabled="formType === 'detail'">
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+            :key="dict.label"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('fileInfo.remark')" prop="remark">
+        <el-input
+          v-model="formData.remark"
+          :disabled="formType === 'detail'"
+          type="textarea"
+          :placeholder="t('deviceForm.remarkHolder')" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button
+        @click="submitForm"
+        type="primary"
+        v-if="formType !== 'detail'"
+        :disabled="formLoading"
+        >{{ t('common.ok') }}</el-button
+      >
+      <el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DataLibraryApi } from '@/api/pms/qhse/index'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as FileClassifyApi from '@/api/pms/info'
+import { IotTreeApi } from '@/api/system/tree'
+import FileUpload from '@/components/UploadFile/src/FileUpload.vue'
+
+/** 资料 表单 */
+defineOptions({ name: 'IotInfoFormTree' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const fileList = ref<Tree[]>([]) // 树形结构
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  deviceId: undefined,
+  classId: undefined,
+  deptId: undefined,
+  filename: undefined,
+  fileType: undefined,
+  filePath: undefined,
+  remark: undefined,
+  fileList: [],
+  folderTree: [],
+  folderJson: undefined
+})
+const formRules = reactive({
+  // classId: [{ required: true, message: '文件不能为空', trigger: 'blur' }],
+  // filePath: [{ required: true, message: '文件不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const props = defineProps<{
+  deviceId: string
+  classId: string
+  nodeId: string
+}>()
+const classIdd = ref('')
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  // if (props.type==='file') {
+  await getTreeInfo()
+  // } else {
+  //   await getTree();
+  // }
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value.classid = formData.value = await DataLibraryApi.getIotInfo(id)
+    } finally {
+      formLoading.value = false
+    }
+  } else {
+    formData.value.fileType = 'file'
+  }
+  classIdd.value = props.classId
+  formData.value.classId = props.nodeId
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+const getTreeInfo = async () => {
+  const res = await IotTreeApi.getSimpleTreeList()
+  fileList.value = []
+  fileList.value.push(...handleTree(res))
+  // firstLevelKeys.value = treeList.value.map(node => node.id);
+  // emits('success', treeList.value[0].id)
+}
+/** 获得分类树 */
+const getTree = async () => {
+  const res = await FileClassifyApi.IotInfoClassifyApi.getSimpleInfotClassifyList(props.deviceId)
+  fileList.value = []
+  fileList.value.push(...handleTree(res))
+}
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      data.deviceId = props.deviceId
+      // data.filePath = data.filePath.join(',')
+      formData.value.folderJson = JSON.stringify(formData.value.folderTree)
+      await DataLibraryApi.createIotInfo(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DataLibraryApi.updateIotInfo(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+    debugger
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    deviceId: undefined,
+    deptId: undefined,
+    filename: undefined,
+    fileType: undefined,
+    filePath: undefined,
+    remark: undefined,
+    fileList: [],
+    folderTree: []
+  }
+  formRef.value?.resetFields()
+}
+
+const handleUploadSuccess = (data) => {
+  if (!Array.isArray(formData.value.folderTree)) {
+    formData.value.folderTree = []
+  }
+  formData.value.folderTree.push(data.response.data)
+  message.success(`"${data.name}" 上传成功`)
+}
+
+const handleUploadError = (error) => {
+  console.error('上传失败', error)
+  message.error(`"${error.name}" 上传失败: ${error.error.message}`)
+}
+
+const handleUploadComplete = () => {
+  message.info('所有上传任务已处理完成')
+}
+</script>

+ 0 - 129
src/views/pms/qhse/data/IotTreeForm.vue

@@ -1,129 +0,0 @@
-<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="originId">
-        <el-input v-model="formData.originId" placeholder="请输入原id" />
-      </el-form-item>
-      <el-form-item label="父分类id" prop="parentId">
-        <el-input v-model="formData.parentId" placeholder="请输入父分类id" />
-      </el-form-item>
-      <el-form-item label="分类名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入分类名称" />
-      </el-form-item>
-      <el-form-item label="类型" prop="type">
-        <el-select v-model="formData.type" placeholder="请选择类型">
-          <el-option label="请选择字典生成" value="" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="分类排序" prop="sort">
-        <el-input v-model="formData.sort" placeholder="请输入分类排序" />
-      </el-form-item>
-      <el-form-item label="开启状态" prop="status">
-        <el-radio-group v-model="formData.status">
-          <el-radio value="1">请选择字典生成</el-radio>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
-    </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 { IotTreeApi, IotTreeVO } from '@/api/system/tree'
-
-/** pms树 表单 */
-defineOptions({ name: 'IotTreeForm' })
-
-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,
-  originId: undefined,
-  parentId: undefined,
-  name: undefined,
-  type: undefined,
-  sort: undefined,
-  status: undefined,
-  remark: undefined,
-})
-const formRules = reactive({
-  originId: [{ required: true, message: '原id不能为空', trigger: 'blur' }],
-  parentId: [{ required: true, message: '父分类id不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
-  type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
-})
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-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 IotTreeApi.getIotTree(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as IotTreeVO
-    if (formType.value === 'create') {
-      await IotTreeApi.createIotTree(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await IotTreeApi.updateIotTree(data)
-      message.success(t('common.updateSuccess'))
-    }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    originId: undefined,
-    parentId: undefined,
-    name: undefined,
-    type: undefined,
-    sort: undefined,
-    status: undefined,
-    remark: undefined,
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 218 - 338
src/views/pms/qhse/data/PmsTree.vue

@@ -1,404 +1,284 @@
-<template>
-  <div class="head-container">
-    <el-input v-model="deptName" class="mb-15px" clearable placeholder="请输入名称">
-      <template #prefix>
-        <Icon icon="ep:search" />
-      </template>
-    </el-input>
-  </div>
-  <div ref="treeContainer" class="tree-container">
-    <el-tree
-      ref="treeRef"
-      :data="treeList"
-      :expand-on-click-node="false"
-      :filter-node-method="filterNode"
-      :props="defaultProps"
-      :default-expanded-keys="expandedKeys"
-      highlight-current
-      node-key="id"
-      @node-click="handleNodeClick"
-      @node-contextmenu="handleRightClick"
-      style="height: 52em">
-      <template #default="{ node }">
-        <div
-          style="display: flex; justify-content: space-between; align-items: center; width: 100%">
-          <div>
-            <Icon
-              style="vertical-align: middle; fill: currentColor; color: orange"
-              v-if="node.data.type === 'dept'"
-              icon="fa:folder-open" />
-            <Icon
-              style="vertical-align: middle; fill: currentColor; color: dodgerblue"
-              v-if="node.data.type === 'device'"
-              icon="fa:tasks" />
-            <Icon
-              icon="fa:folder-open"
-              v-if="node.data.type === 'file'"
-              style="vertical-align: middle; color: orange; fill: currentColor" />
-            <span style="vertical-align: middle; margin-left: 3px">{{ node.data.name }}</span>
-          </div>
-        </div>
-      </template>
-    </el-tree>
-  </div>
-  <div
-    v-show="deviceVisible"
-    ref="contextMenuRef"
-    class="custom-menu"
-    :style="{ left: menuX + 'px', top: menuY + 'px' }">
-    <ul>
-      <li style="border-bottom: 1px solid #ccc" @click="handleDeviceClick('add')">设备详情</li>
-      <li style="border-bottom: 0px solid #ccc" @click="handleMenuClick('add')">新建目录</li>
-    </ul>
-  </div>
-  <div
-    v-show="menuVisible"
-    ref="contextMenuRef"
-    class="custom-menu"
-    :style="{ left: menuX + 'px', top: menuY + 'px' }">
-    <ul>
-      <li style="border-bottom: 1px solid #ccc" @click="handleMenuClick('add')">新建目录</li>
-      <li style="border-bottom: 1px solid #ccc" @click="handleMenuClick('edit')">编辑</li>
-      <li @click="handleMenuClick('delete')">删除目录</li>
-    </ul>
-  </div>
-
-  <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="80px">
-      <el-form-item label="目录名称" prop="name" label-width="110px">
-        <el-input v-model="formData.name" placeholder="请输入目录名称" />
-      </el-form-item>
-      <el-form-item label="显示排序" prop="sort" label-width="110px">
-        <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { ElTree } from 'element-plus'
 import { ElTree } from 'element-plus'
-import { defaultProps, handleTree } from '@/utils/tree'
-import { CommonStatusEnum } from '@/utils/constants'
+import { CaretLeft, CaretRight, Search } from '@element-plus/icons-vue'
+import type { IotTreeVO } from '@/api/system/tree'
 import { IotTreeApi } from '@/api/system/tree'
 import { IotTreeApi } from '@/api/system/tree'
-import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useI18n } from 'vue-i18n'
-import { useRouter } from 'vue-router'
-
-// 类型定义
-interface Tree {
-  id: number | string
-  name: string
-  type: string
-  children?: Tree[]
-  [key: string]: any
-}
+import { defaultProps, handleTree } from '@/utils/tree'
 
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formRef = ref() // 搜索的表单
-const firstLevelKeys = ref<(number | string)[]>([])
-const expandedKeys = ref<(number | string)[]>([]) // 用于展开节点的路径
-
-// 表单相关
-const formLoading = ref(false)
-const formType = ref<'create' | 'update'>('create')
-const formData = ref({
-  id: undefined,
-  title: '',
-  parentId: undefined,
-  name: undefined,
-  sort: undefined,
-  leaderUserId: undefined,
-  phone: undefined,
-  email: undefined,
-  status: CommonStatusEnum.ENABLE
-})
-const formRules = ref({
-  name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-  sort: [{ required: true, message: '请输入排序', trigger: 'blur' }]
-})
+defineOptions({ name: 'IotTree' })
 
 
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    title: '',
-    parentId: undefined,
-    name: undefined,
-    sort: undefined,
-    leaderUserId: undefined,
-    phone: undefined,
-    email: undefined,
-    status: CommonStatusEnum.ENABLE
-  }
-  formRef.value?.resetFields()
+interface TreeNode extends IotTreeVO {
+  children?: TreeNode[]
 }
 }
 
 
-defineOptions({ name: 'IotTree' })
+type TreeKey = number | string
 
 
-// 接收外部参数
-const props = defineProps({
-  deviceId: { type: Number, required: true },
-  currentId: { type: [Number, String], default: null } // 新增:接收需要定位的节点ID
-})
+const props = withDefaults(
+  defineProps<{
+    deviceId: number
+    currentId?: TreeKey | null
+  }>(),
+  {
+    currentId: null
+  }
+)
 
 
-const deptName = ref('')
-const nodeInfo = ref({})
-const treeList = ref<Tree[]>([]) // 树形结构
+const emits = defineEmits<{
+  (event: 'node-click', value: TreeNode): void
+  (event: 'success', value?: number): void
+}>()
+
+const isCollapsed = ref(false)
+const treeName = ref('')
+const treeList = ref<TreeNode[]>([])
 const treeRef = ref<InstanceType<typeof ElTree>>()
 const treeRef = ref<InstanceType<typeof ElTree>>()
-const menuVisible = ref(false)
-const deviceVisible = ref(false)
-const menuX = ref(0)
-const menuY = ref(0)
-const parentId = ref()
-
-const { push } = useRouter() // 路由跳转
-
-// 动态高度计算
-const treeContainer = ref(null)
-const setHeight = () => {
-  if (!treeContainer.value) return
-  const windowHeight = window.innerHeight
-  const containerTop = treeContainer.value.offsetTop
-  treeContainer.value.style.height = `${windowHeight * 0.78}px` // 60px 底部预留
+const expandedKeys = ref<Array<number | string>>([])
+
+const getNodeIcon = (type: string) => {
+  if (type === 'device') return 'fa:tasks'
+  return 'fa:folder-open'
+}
+
+const getNodeIconClass = (type: string) => {
+  return type === 'device' ? 'node-icon node-icon--device' : 'node-icon node-icon--folder'
 }
 }
 
 
-// 新增:查找节点路径的递归方法
 const findNodePath = (
 const findNodePath = (
-  nodes: Tree[],
+  nodes: TreeNode[],
   targetId: number | string,
   targetId: number | string,
-  path: (number | string)[] = []
-): (number | string)[] | null => {
+  path: Array<number | string> = []
+): Array<number | string> | null => {
   for (const node of nodes) {
   for (const node of nodes) {
-    path.push(node.id)
+    const nextPath = [...path, node.id]
+
     if (node.id === targetId) {
     if (node.id === targetId) {
-      return [...path]
+      return nextPath
     }
     }
-    if (node.children && node.children.length) {
-      const result = findNodePath(node.children, targetId, path)
-      if (result) {
-        return result
-      }
+
+    if (node.children?.length) {
+      const result = findNodePath(node.children, targetId, nextPath)
+      if (result) return result
     }
     }
-    path.pop()
   }
   }
+
   return null
   return null
 }
 }
 
 
-// 新增:定位节点并高亮
 const locateNode = (targetId: number | string) => {
 const locateNode = (targetId: number | string) => {
   if (!targetId || !treeList.value.length) return
   if (!targetId || !treeList.value.length) return
 
 
   const pathIds = findNodePath(treeList.value, targetId)
   const pathIds = findNodePath(treeList.value, targetId)
-  if (pathIds) {
-    // 展开所有父节点
-    expandedKeys.value = pathIds.slice(0, -1)
-
-    // 等待DOM更新后设置当前节点
-    nextTick(() => {
-      if (treeRef.value) {
-        treeRef.value.setCurrentKey(targetId)
-
-        // 滚动到节点位置
-        const node = treeRef.value.getNode(targetId)
-        if (node && node.$el) {
-          node.$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
-        }
-      }
-    })
-  }
-}
+  if (!pathIds) return
 
 
-onMounted(async () => {
-  await getTreeInfo()
-  setHeight()
-  window.addEventListener('resize', setHeight)
-})
-
-onUnmounted(() => {
-  window.removeEventListener('resize', setHeight)
-})
-
-const handleRightClick = (event, node, data) => {
-  nodeInfo.value = node
-  event.preventDefault()
-  menuX.value = event.clientX
-  menuY.value = event.clientY
-  selectedNode = data
-  debugger
-  parentId.value = data.data.id
-  if (nodeInfo.value.type === 'device') {
-    deviceVisible.value = true
-  } else if (nodeInfo.value.type === 'file') {
-    menuVisible.value = true
-  }
-}
-
-const handleDeviceClick = async () => {
-  const id = nodeInfo.value.originId
-  push({ name: 'DeviceDetailInfo', params: { id } })
-  deviceVisible.value = false
-  menuVisible.value = false
-}
+  expandedKeys.value = [...new Set([...expandedKeys.value, ...pathIds.slice(0, -1)])]
 
 
-const handleMenuClick = async (action) => {
-  switch (action) {
-    case 'add':
-      dialogVisible.value = true
-      dialogTitle.value = '新增目录'
-      formType.value = 'create'
-      resetForm()
-      break
-    case 'edit':
-      resetForm()
-      dialogVisible.value = true
-      dialogTitle.value = '编辑目录'
-      formType.value = 'update'
-      formData.value = { ...nodeInfo.value }
-      debugger
-      break
-    case 'delete':
-      // 删除的二次确认
-      await message.delConfirm()
-      // 假设存在删除接口
-      await IotTreeApi.deleteIotTree(nodeInfo.value.id)
-      message.success(t('common.delSuccess'))
-      // 刷新列表
-      await getTreeInfo()
-      break
-  }
-  deviceVisible.value = false
-  menuVisible.value = false
+  nextTick(() => {
+    treeRef.value?.setCurrentKey(targetId)
+    treeRef.value?.getNode(targetId)?.$el?.scrollIntoView({
+      behavior: 'smooth',
+      block: 'nearest'
+    })
+  })
 }
 }
 
 
-/** 获得部门树 */
 const getTreeInfo = async () => {
 const getTreeInfo = async () => {
   const res = await IotTreeApi.getSimpleTreeList()
   const res = await IotTreeApi.getSimpleTreeList()
-  treeList.value = []
-  treeList.value.push(...handleTree(res))
+  treeList.value = handleTree(res) as TreeNode[]
+  expandedKeys.value = treeList.value.map((node) => node.id)
 
 
-  // 处理展开逻辑:有currentId则展开对应路径,否则展开一级节点
   if (props.currentId) {
   if (props.currentId) {
     locateNode(props.currentId)
     locateNode(props.currentId)
-  } else {
-    firstLevelKeys.value = treeList.value.map((node) => node.id)
-    expandedKeys.value = [...firstLevelKeys.value]
   }
   }
+
   emits('success', treeList.value[0]?.id)
   emits('success', treeList.value[0]?.id)
 }
 }
 
 
-/** 基于名字过滤 */
-const filterNode = (name: string, data: Tree) => {
-  if (!name) return true
-  return data.name.includes(name)
+const filterNode = (name: string, data: TreeNode) => {
+  return !name || data.name.includes(name)
 }
 }
 
 
-/** 处理节点被点击 */
-const handleNodeClick = async (row: { [key: string]: any }) => {
-  deviceVisible.value = false
-  menuVisible.value = false
+const handleNodeClick = (row: TreeNode) => {
   emits('node-click', row)
   emits('node-click', row)
 }
 }
 
 
-/** 提交表单 */
-const submitForm = async () => {
-  // 表单验证逻辑
-  formLoading.value = true
-  try {
-    formData.value.deviceId = props.deviceId
-    debugger
-    // formData.value.parentId = parentId.value
-    if (formData.value.parentId === undefined || formData.value.parentId === null) {
-      formData.value.parentId = props.currentId
-    }
-    formData.value.type = 'file'
-    if (formType.value === 'create') {
-      debugger
-      await IotTreeApi.createIotTree(formData.value)
-    } else {
-      await IotTreeApi.updateIotTree(formData.value)
-    }
-    message.success(t(formType.value === 'create' ? 'common.addSuccess' : 'common.updateSuccess'))
-    dialogVisible.value = false
-    await getTreeInfo()
-  } catch (error) {
-    console.error(error)
-  } finally {
-    formLoading.value = false
-  }
-}
-
-const emits = defineEmits(['node-click', 'success'])
-
-/** 监听搜索框变化 */
-watch(deptName, (val) => {
-  treeRef.value!.filter(val)
+watch(treeName, (val) => {
+  treeRef.value?.filter(val)
 })
 })
 
 
-/** 监听currentId变化,重新定位 */
 watch(
 watch(
   () => props.currentId,
   () => props.currentId,
   (newVal) => {
   (newVal) => {
-    if (newVal && treeList.value.length) {
+    if (newVal) {
       locateNode(newVal)
       locateNode(newVal)
     }
     }
-  },
-  { immediate: true }
+  }
 )
 )
+
+onMounted(getTreeInfo)
 </script>
 </script>
 
 
+<template>
+  <div
+    class="pms-tree-aside relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg transition-all duration-300 ease-in-out overflow-visible"
+    :class="[isCollapsed ? 'is-collapsed' : 'p-4']">
+    <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
+      <div class="shrink-0">
+        <el-input
+          v-model="treeName"
+          clearable
+          placeholder="请输入名称"
+          size="default"
+          :prefix-icon="Search" />
+      </div>
+
+      <div class="tree-body">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <el-scrollbar :style="{ height: `${height}px` }">
+              <el-tree
+                ref="treeRef"
+                :data="treeList"
+                :default-expanded-keys="expandedKeys"
+                :expand-on-click-node="false"
+                :filter-node-method="filterNode"
+                :props="defaultProps"
+                highlight-current
+                node-key="id"
+                @node-click="handleNodeClick">
+                <template #default="{ node }">
+                  <div class="tree-node">
+                    <Icon
+                      :class="getNodeIconClass(node.data.type)"
+                      :icon="getNodeIcon(node.data.type)" />
+                    <span class="tree-node__label">{{ node.data.name }}</span>
+                  </div>
+                </template>
+              </el-tree>
+            </el-scrollbar>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+
+    <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
+      <el-icon size="12">
+        <CaretLeft v-if="!isCollapsed" />
+        <CaretRight v-else />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-.custom-menu {
-  position: fixed;
-  background: white;
-  border: 1px solid #ccc;
-  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
-  z-index: 1000;
+.pms-tree-aside {
+  width: 100%;
+  height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
+  min-width: 240px;
+  box-sizing: border-box;
+  flex-shrink: 0;
 }
 }
-.custom-menu ul {
-  list-style: none;
-  padding: 0;
-  margin: 0;
+
+.pms-tree-aside.is-collapsed {
+  width: 0 !important;
+  min-width: 0 !important;
+  padding: 0 !important;
+  overflow: visible !important;
+  pointer-events: none;
+  box-shadow: none;
+}
+
+:global(.left-tree:has(.pms-tree-aside.is-collapsed)) {
+  width: 0 !important;
+  overflow: visible !important;
+  background: transparent !important;
+}
+
+:global(.left-tree:has(.pms-tree-aside.is-collapsed) + .divider-tree) {
+  display: none;
 }
 }
-.custom-menu li {
-  padding: 2px 10px;
+
+.tree-body {
+  position: relative;
+  min-height: 0;
+  overflow: hidden;
+  flex: 1;
+}
+
+.tree-node {
+  display: flex;
+  width: 100%;
+  min-width: 0;
+  align-items: center;
+}
+
+.node-icon {
+  margin-right: 6px;
+  fill: currentcolor;
+  flex-shrink: 0;
+}
+
+.node-icon--folder {
+  color: #e6a23c;
+}
+
+.node-icon--device {
+  color: #409eff;
+}
+
+.tree-node__label {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.collapse-handle {
+  position: absolute;
+  top: 50%;
+  right: -14px;
+  z-index: 200;
+  display: flex;
+  width: 14px;
+  height: 60px;
+  color: var(--el-text-color-secondary);
+  pointer-events: auto;
   cursor: pointer;
   cursor: pointer;
-  font-size: 12px;
-  margin: 2px;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+  border-left: none;
+  border-radius: 0 12px 12px 0;
+  transform: translateY(-50%);
+  box-shadow: 2px 0 6px rgb(0 0 0 / 5%);
+  transition: right 0.3s;
+  align-items: center;
+  justify-content: center;
 }
 }
-.custom-menu li:hover {
-  background: #77a0ec;
+
+.is-collapsed .collapse-handle {
+  right: -8px;
+  border-left: 1px solid var(--el-border-color-light);
 }
 }
-.tree-container {
-  overflow-y: auto;
-  min-width: 100%;
-  border: none;
-  border-radius: 4px;
+
+.collapse-handle:hover {
+  color: var(--el-color-primary);
+  background-color: var(--el-fill-color-light);
 }
 }
-/* 自定义滚动条 */
-.tree-container::-webkit-scrollbar {
-  width: 6px;
+
+:deep(.el-tree) {
+  --el-tree-node-hover-bg-color: var(--el-fill-color-light);
+
+  background: transparent;
 }
 }
-.tree-container::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
-  border-radius: 3px;
+
+:deep(.el-tree-node__content) {
+  height: 32px;
+  border-radius: 4px;
 }
 }
 
 
-/* 自定义高亮样式 */
-::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
-  background-color: #eaf5ff !important;
-  color: #1890ff !important;
-  font-weight: bold;
+:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
+  font-weight: 600;
+  color: var(--el-color-primary);
+  background-color: var(--el-color-primary-light-9);
 }
 }
 </style>
 </style>

+ 311 - 359
src/views/pms/qhse/data/index.vue

@@ -1,6 +1,63 @@
 <template>
 <template>
-  <ContentWrap>
-    <div style="display: flex; justify-content: space-between; align-items: center">
+  <div
+    class="tree-page grid grid-cols-[auto_1fr] grid-rows-[48px_48px_1fr] gap-x-4 gap-y-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <PmsTree
+      class="row-span-3"
+      @node-click="handleFileNodeClick"
+      @success="successList"
+      :currentId="queryParams.classId"
+      :deviceId="id" />
+
+    <el-form
+      class="search-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 gap-8 flex items-center justify-between"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px">
+      <div class="flex items-center gap-8">
+        <el-form-item :label="t('file.name')" prop="filename">
+          <el-input
+            v-model="queryParams.filename"
+            :placeholder="t('file.nameHolder')"
+            size="default"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-240px" />
+        </el-form-item>
+        <el-form-item v-show="false" :label="t('file.fileType')" prop="fileType">
+          <el-select
+            v-model="queryParams.fileType"
+            :placeholder="t('file.fileTypeHolder')"
+            size="default"
+            clearable
+            class="!w-200px">
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+      </div>
+      <el-form-item>
+        <el-button size="default" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />{{ t('file.search') }}
+        </el-button>
+        <el-button size="default" @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />{{ t('file.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          size="default"
+          :loading="uploadLoading"
+          @click="openForm('create')">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('file.upload') }}
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div
+      class="breadcrumb-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-4 flex items-center justify-between">
       <el-breadcrumb separator=">" class="breadcrumb-container">
       <el-breadcrumb separator=">" class="breadcrumb-container">
         <el-breadcrumb-item
         <el-breadcrumb-item
           v-for="(item, index) in breadcrumbs"
           v-for="(item, index) in breadcrumbs"
@@ -14,169 +71,120 @@
       <el-input
       <el-input
         v-model="queryParams.allName"
         v-model="queryParams.allName"
         :placeholder="'在' + breadcrumbs[breadcrumbs.length - 1].name + '下搜索'"
         :placeholder="'在' + breadcrumbs[breadcrumbs.length - 1].name + '下搜索'"
-        style="width: 250px; height: 30px"
+        size="default"
+        style="width: 250px"
         @input="searchFolderAndFile" />
         @input="searchFolderAndFile" />
     </div>
     </div>
-  </ContentWrap>
-  <div class="container-tree" ref="container">
-    <el-row>
-      <div class="left-tree" :style="{ width: leftWidth + 'px' }">
-        <ContentWrapNoBottom>
-          <PmsTree
-            @node-click="handleFileNodeClick"
-            @success="successList"
-            :currentId="queryParams.classId"
-            :deviceId="id" />
-        </ContentWrapNoBottom>
-      </div>
-      <!--    </el-col>-->
-      <div class="divider-tree" @mousedown="startDrag"></div>
-      <div class="right-tree" :style="{ width: rightWidth + 'px' }">
-        <div class="bg-white rounded-lg pt-4 flex items-center">
-          <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
-            <el-form-item :label="t('file.name')" prop="filename">
-              <el-input
-                v-model="queryParams.filename"
-                :placeholder="t('file.nameHolder')"
-                clearable
-                @keyup.enter="handleQuery"
-                class="!w-240px" />
-            </el-form-item>
-            <el-form-item v-show="false" :label="t('file.fileType')" prop="fileType">
-              <el-select
-                v-model="queryParams.fileType"
-                :placeholder="t('file.fileTypeHolder')"
-                clearable
-                class="!w-200px">
-                <el-option
-                  v-for="dict in getStrDictOptions(DICT_TYPE.PMS_FILE_TYPE)"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item>
-              <el-button @click="handleQuery"
-                ><Icon icon="ep:search" /> {{ t('file.search') }}</el-button
-              >
-              <el-button @click="resetQuery"
-                ><Icon icon="ep:refresh" />{{ t('file.reset') }}</el-button
-              >
-              <el-button type="primary" :loading="uploadLoading" @click="openForm('create')">
-                <Icon icon="ep:plus" /> {{ t('file.upload') }}
+
+    <div class="table-panel bg-white dark:bg-[#1d1e1f] rounded-lg shadow overflow-hidden p-3">
+      <zm-table
+        :loading="formLoading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        settings-cache-key="system-tree-file-list"
+        @row-dblclick="inContent"
+        class="custom-table">
+        <zm-table-column :label="t('file.name')" align="left" prop="filename" min-width="300">
+          <template #default="scope">
+            <div style="display: flex; align-items: center; gap: 5px">
+              <Icon v-if="scope.row.fileType === 'content'" icon="fa:folder-open" color="orange" />
+              <Icon
+                v-else-if="scope.row.fileType === 'device'"
+                icon="fa:folder-open"
+                color="blue" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'pic' ||
+                  scope.row.fileClassify === 'jpg' ||
+                  scope.row.fileClassify === 'png' ||
+                  scope.row.fileClassify === 'JPG'
+                "
+                icon="ep:picture-filled"
+                color="#2183D1" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'pdf' || scope.row.fileClassify === 'PDF')
+                "
+                icon="fa-solid:file-pdf"
+                color="#E20012" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'doc' || scope.row.fileClassify === 'docx')
+                "
+                icon="fa:file-word-o"
+                color="blue" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'xls' || scope.row.fileClassify === 'xlsx')
+                "
+                icon="fa-solid:file-excel"
+                color="#107C41" />
+              <Icon
+                v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'txt'"
+                icon="fa:file-text-o" />
+              <Icon
+                v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'mp4'"
+                icon="fa:play-circle-o"
+                color="#009fff" />
+              <Icon
+                v-else-if="
+                  scope.row.fileType === 'file' &&
+                  (scope.row.fileClassify === 'ppt' || scope.row.fileClassify === 'pptx')
+                "
+                icon="fa-solid:file-powerpoint"
+                color="#C43E1C" />
+              <Icon v-else icon="fa-solid:file-alt" />
+              {{ scope.row.filename }}
+            </div>
+          </template>
+        </zm-table-column>
+        <zm-table-column :label="t('file.fileType')" align="center" prop="fileType">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
+          </template>
+        </zm-table-column>
+        <zm-table-column :label="t('file.fileSize')" align="center" prop="fileSize" />
+        <zm-table-column :label="t('file.dept')" align="center" prop="deptName" />
+        <zm-table-column
+          :label="t('file.device')"
+          align="center"
+          prop="deviceCode"
+          min-width="220" />
+        <zm-table-column
+          :label="t('file.operation')"
+          align="center"
+          width="160"
+          fixed="right"
+          action>
+          <template #default="scope">
+            <div class="flex items-center justify-center">
+              <el-button
+                type="primary"
+                v-if="scope.row.fileType !== 'content'"
+                link
+                @click="handleDownload(scope.row.filePath)"
+                v-hasPermi="['rq:iot-info:download']">
+                <Icon icon="ep:download" />{{ t('file.dow') }}
               </el-button>
               </el-button>
-            </el-form-item>
-          </el-form>
-        </div>
-        <ContentWrap style="margin-top: 10px; border: 0">
-          <zm-table
-            :loading="formLoading"
-            :data="list"
-            :stripe="true"
-            :show-overflow-tooltip="true"
-            @row-dblclick="inContent"
-            class="custom-table">
-            <zm-table-column :label="t('file.name')" align="left" prop="filename" min-width="300">
-              <template #default="scope">
-                <div style="display: flex; align-items: center; gap: 5px">
-                  <Icon
-                    v-if="scope.row.fileType === 'content'"
-                    icon="fa:folder-open"
-                    color="orange" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'device'"
-                    icon="fa:folder-open"
-                    color="blue" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'pic' ||
-                      scope.row.fileClassify === 'jpg' ||
-                      scope.row.fileClassify === 'png' ||
-                      scope.row.fileClassify === 'JPG'
-                    "
-                    icon="ep:picture-filled"
-                    color="#2183D1" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'pdf' || scope.row.fileClassify === 'PDF')
-                    "
-                    icon="fa-solid:file-pdf"
-                    color="#E20012" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'doc' || scope.row.fileClassify === 'docx')
-                    "
-                    icon="fa:file-word-o"
-                    color="blue" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'xls' || scope.row.fileClassify === 'xlsx')
-                    "
-                    icon="fa-solid:file-excel"
-                    color="#107C41" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'txt'"
-                    icon="fa:file-text-o" />
-                  <Icon
-                    v-else-if="scope.row.fileType === 'file' && scope.row.fileClassify === 'mp4'"
-                    icon="fa:play-circle-o"
-                    color="#009fff" />
-                  <Icon
-                    v-else-if="
-                      scope.row.fileType === 'file' &&
-                      (scope.row.fileClassify === 'ppt' || scope.row.fileClassify === 'pptx')
-                    "
-                    icon="fa-solid:file-powerpoint"
-                    color="#C43E1C" />
-                  <Icon v-else icon="fa-solid:file-alt" />
-                  {{ scope.row.filename }}
-                </div>
-              </template>
-            </zm-table-column>
-            <zm-table-column :label="t('file.fileType')" align="center" prop="fileType">
-              <template #default="scope">
-                <dict-tag :type="DICT_TYPE.PMS_FILE_TYPE" :value="scope.row.fileType" />
-              </template>
-            </zm-table-column>
-            <zm-table-column :label="t('file.fileSize')" align="center" prop="fileSize" />
-
-            <zm-table-column :label="t('file.dept')" align="center" prop="deptName" />
-            <zm-table-column
-              :label="t('file.device')"
-              align="center"
-              prop="deviceCode"
-              min-width="220" />
-            />
-            <zm-table-column :label="t('file.operation')" align="center" width="160">
-              <template #default="scope">
-                <div class="flex items-center justify-center">
-                  <el-button
-                    type="primary"
-                    v-if="scope.row.fileType !== 'content'"
-                    link
-                    @click="handleDownload(scope.row.filePath)"
-                    v-hasPermi="['rq:iot-info:download']">
-                    <Icon icon="ep:download" />{{ t('file.dow') }}
-                  </el-button>
-                  <el-button
-                    type="danger"
-                    v-if="scope.row.fileType !== 'content'"
-                    link
-                    @click="deleteInfo(scope.row.id)"
-                    v-hasPermi="['rq:iot-info:download']">
-                    <Icon icon="ep:delete" />{{ t('file.delete') }}
-                  </el-button>
-                </div>
-              </template>
-            </zm-table-column>
-          </zm-table>
-        </ContentWrap>
-      </div>
-    </el-row>
+              <el-button
+                type="danger"
+                v-if="scope.row.fileType !== 'content'"
+                link
+                @click="deleteInfo(scope.row.id)"
+                v-hasPermi="['rq:iot-info:download']">
+                <Icon icon="ep:delete" />{{ t('file.delete') }}
+              </el-button>
+            </div>
+          </template>
+        </zm-table-column>
+      </zm-table>
+    </div>
   </div>
   </div>
+
   <IotInfoFormTree
   <IotInfoFormTree
     ref="formRef"
     ref="formRef"
     @success="getList"
     @success="getList"
@@ -185,48 +193,69 @@
     :classId="clickNodeId" />
     :classId="clickNodeId" />
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
-import { IotDeviceVO } from '@/api/pms/device'
-import IotInfoFormTree from '@/views/pms/iotinfo/IotInfoFormTree.vue'
-import * as IotInfoApi from '@/api/pms/iotinfo'
-import { ref, onMounted, onUnmounted } from 'vue'
+import IotInfoFormTree from './IotInfoFormTree.vue'
+import * as DataLibraryApi from '@/api/pms/qhse/index'
+import { ref, onMounted } from 'vue'
+import { Base64 } from 'js-base64'
 
 
-import PmsTree from '@/views/system/tree/PmsTree.vue'
-import { useCache } from '@/hooks/web/useCache'
+import PmsTree from './PmsTree.vue'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { IotInfoClassifyApi } from '@/api/pms/info'
 import { IotInfoClassifyApi } from '@/api/pms/info'
 import { IotTreeApi } from '@/api/system/tree'
 import { IotTreeApi } from '@/api/system/tree'
 defineOptions({ name: 'IotTree' })
 defineOptions({ name: 'IotTree' })
 
 
-const container = ref(null)
-const leftWidth = ref(350) // 初始左侧宽度
-const rightWidth = ref(window.innerWidth * 0.8)
-let isDragging = false
+type TreeKey = number | string
+
+interface QueryParams {
+  pageNo: number
+  pageSize: number
+  filename: string | null
+  fileType: string | null
+  createTime: string[]
+  deviceId: TreeKey | null
+  classId: TreeKey | null
+  deptId?: TreeKey
+  allName: string | null
+}
+
+interface BreadcrumbItem {
+  id: TreeKey | null
+  name: string
+  type?: string
+}
+
+interface FileRow {
+  id: TreeKey
+  originId?: TreeKey
+  parentId?: TreeKey
+  name?: string
+  filename?: string
+  fileType?: string
+  filePath?: string
+  fileSize?: string
+  fileClassify?: string
+  deptName?: string
+  deviceCode?: string
+  type?: string
+}
+
+interface IotInfoFormTreeExpose {
+  open: (type: string, id?: number) => void
+}
+
 const uploadLoading = ref(false)
 const uploadLoading = ref(false)
 
 
 const searchFolderAndFile = async () => {
 const searchFolderAndFile = async () => {
-  debugger
   formLoading.value = true
   formLoading.value = true
-  const data = await IotInfoApi.IotInfoApi.getAllChildContentFile(queryParams)
-  debugger
+  const data = await DataLibraryApi.DataLibraryApi.getAllChildContentFile(queryParams)
   list.value = data
   list.value = data
   formLoading.value = false
   formLoading.value = false
   queryParams.filename = ''
   queryParams.filename = ''
 }
 }
-const openWeb = (url) => {
-  window.open(
-    'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-  )
-}
-const handleView = (row) => {
-  openForm('detail', row.id)
-}
-const startDrag = (e) => {
-  isDragging = true
-  document.addEventListener('mousemove', onDrag)
-  document.addEventListener('mouseup', stopDrag)
-}
-const topNodeId = ref('')
-const successList = async (id) => {
+const topNodeId = ref<TreeKey | null>(null)
+const successList = async (id?: TreeKey) => {
+  if (id === undefined) return
+
   queryParams.classId = id
   queryParams.classId = id
   topNodeId.value = id
   topNodeId.value = id
   const rootItem = breadcrumbs.value.find((item) => item.type === 'root')
   const rootItem = breadcrumbs.value.find((item) => item.type === 'root')
@@ -238,34 +267,15 @@ const successList = async (id) => {
   // queryParams.classId = ''
   // queryParams.classId = ''
 }
 }
 
 
-const onDrag = (e) => {
-  if (!isDragging) return
-
-  const containerRect = container.value.getBoundingClientRect()
-  const newWidth = e.clientX - containerRect.left
-
-  // 设置最小和最大宽度限制
-  if (newWidth > 300 && newWidth < containerRect.width - 100) {
-    leftWidth.value = newWidth
-  }
-}
-
-const stopDrag = () => {
-  isDragging = false
-  document.removeEventListener('mousemove', onDrag)
-  document.removeEventListener('mouseup', stopDrag)
-}
-
 const queryFormRef = ref() // 搜索的表单
 const queryFormRef = ref() // 搜索的表单
 const { t } = useI18n() // 国际化
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const message = useMessage() // 消息弹窗
-const loading = ref(true) // 列表的加载中
 const { params } = useRoute() // 查询参数
 const { params } = useRoute() // 查询参数
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const list = ref<FileRow[]>([]) // 列表的数据
 // const total = ref(0) // 列表的总页数
 // const total = ref(0) // 列表的总页数
-const id = ref()
-const queryParams = reactive({
+const id = ref(0)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageNo: 1,
   pageSize: 10,
   pageSize: 10,
   filename: null,
   filename: null,
@@ -276,18 +286,8 @@ const queryParams = reactive({
   deptId: undefined,
   deptId: undefined,
   allName: null
   allName: null
 })
 })
-// SPU 表单数据
-const formData = ref({
-  id: undefined,
-  deviceId: undefined,
-  deptId: undefined,
-  filename: undefined,
-  fileType: undefined,
-  filePath: undefined,
-  remark: undefined,
-  classId: undefined
-})
-const handleDownload = async (url) => {
+const handleDownload = async (url?: string) => {
+  if (!url) return
   try {
   try {
     formLoading.value = true
     formLoading.value = true
     const response = await fetch(url)
     const response = await fetch(url)
@@ -296,7 +296,7 @@ const handleDownload = async (url) => {
 
 
     const link = document.createElement('a')
     const link = document.createElement('a')
     link.href = downloadUrl
     link.href = downloadUrl
-    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.download = url.split('/').pop()! // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
     link.click()
     link.click()
 
 
     URL.revokeObjectURL(downloadUrl)
     URL.revokeObjectURL(downloadUrl)
@@ -305,9 +305,9 @@ const handleDownload = async (url) => {
     console.error('下载失败:', error)
     console.error('下载失败:', error)
   }
   }
 }
 }
-const deleteInfo = async (id) => {
+const deleteInfo = async (id: TreeKey) => {
   await message.delConfirm()
   await message.delConfirm()
-  await IotInfoApi.IotInfoApi.deleteIotInfo(id).then((res) => {
+  await DataLibraryApi.DataLibraryApi.deleteIotInfo(Number(id)).then((res) => {
     if (res) {
     if (res) {
       message.success('文件删除成功')
       message.success('文件删除成功')
       getList()
       getList()
@@ -316,23 +316,7 @@ const deleteInfo = async (id) => {
     }
     }
   })
   })
 }
 }
-// const handleFileView = (url: string) => {
-//   window.open(
-//     'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-//   )
-// }
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotInfoApi.IotInfoApi.deleteIotInfo(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
-}
-const formRef = ref()
+const formRef = ref<IotInfoFormTreeExpose>()
 const openForm = async (type: string, id?: number) => {
 const openForm = async (type: string, id?: number) => {
   // if (classType.value==='dept'){
   // if (classType.value==='dept'){
   //   message.error(t('common.deptChoose'))
   //   message.error(t('common.deptChoose'))
@@ -341,10 +325,11 @@ const openForm = async (type: string, id?: number) => {
   uploadLoading.value = true
   uploadLoading.value = true
   if (!queryParams.classId) {
   if (!queryParams.classId) {
     message.error(t('common.leftNode'))
     message.error(t('common.leftNode'))
+    uploadLoading.value = false
     return
     return
   }
   }
-  formRef.value.open(type, id)
-  await new Promise((resolve) => {
+  formRef.value?.open(type, id)
+  await new Promise<void>((resolve) => {
     setTimeout(() => {
     setTimeout(() => {
       resolve() // 操作完成后resolve
       resolve() // 操作完成后resolve
     }, 2000) // 模拟2秒耗时
     }, 2000) // 模拟2秒耗时
@@ -354,14 +339,13 @@ const openForm = async (type: string, id?: number) => {
 const deviceId = ref('')
 const deviceId = ref('')
 const clickNodeId = ref('')
 const clickNodeId = ref('')
 const nodeId = ref('')
 const nodeId = ref('')
-const classType = ref('')
 
 
-const breadcrumbs = ref([
+const breadcrumbs = ref<BreadcrumbItem[]>([
   { id: null, name: '科瑞石油技术', type: 'root' } // 根节点
   { id: null, name: '科瑞石油技术', type: 'root' } // 根节点
 ])
 ])
 
 
 // 共享的面包屑更新逻辑
 // 共享的面包屑更新逻辑
-const updateBreadcrumbs = async (node) => {
+const updateBreadcrumbs = async (node: FileRow) => {
   // 查找当前节点是否已在面包屑中
   // 查找当前节点是否已在面包屑中
   const currentIndex = breadcrumbs.value.findIndex((item) => item.id === node.id)
   const currentIndex = breadcrumbs.value.findIndex((item) => item.id === node.id)
 
 
@@ -370,18 +354,20 @@ const updateBreadcrumbs = async (node) => {
     breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
     breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
   } else {
   } else {
     // 新增节点到面包屑
     // 新增节点到面包屑
-    breadcrumbs.value.push({ id: node.id, name: node.filename || node.name })
+    breadcrumbs.value.push({ id: node.id, name: node.filename || node.name || '' })
   }
   }
 
 
   // 更新表格数据
   // 更新表格数据
   queryParams.classId = node.id
   queryParams.classId = node.id
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
   list.value = data
 }
 }
 
 
 // 表格行点击事件
 // 表格行点击事件
-const inContent = async (row) => {
+const inContent = async (row: FileRow) => {
   if (row.fileType != 'content') {
   if (row.fileType != 'content') {
+    if (!row.filePath) return
+
     window.open(
     window.open(
       'http://doc.deepoil.cc:8012/onlinePreview?url=' +
       'http://doc.deepoil.cc:8012/onlinePreview?url=' +
         encodeURIComponent(Base64.encode(row.filePath))
         encodeURIComponent(Base64.encode(row.filePath))
@@ -394,14 +380,14 @@ const inContent = async (row) => {
   await updateBreadcrumbs(row)
   await updateBreadcrumbs(row)
   // 可以添加其他表格行点击需要的逻辑
   // 可以添加其他表格行点击需要的逻辑
   queryParams.classId = row.id
   queryParams.classId = row.id
-  nodeId.value = row.id
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  nodeId.value = String(row.id)
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   formLoading.value = false
   formLoading.value = false
   list.value = data
   list.value = data
 }
 }
 
 
 // 文件树节点点击事件
 // 文件树节点点击事件
-const handleFileNodeClick = async (row) => {
+const handleFileNodeClick = async (row: FileRow) => {
   queryParams.filename = ''
   queryParams.filename = ''
   const parentItems = await IotTreeApi.getParentIds(row.id)
   const parentItems = await IotTreeApi.getParentIds(row.id)
   breadcrumbs.value = []
   breadcrumbs.value = []
@@ -411,12 +397,11 @@ const handleFileNodeClick = async (row) => {
 
 
   queryParams.classId = row.id
   queryParams.classId = row.id
   if (row.type == 'device') {
   if (row.type == 'device') {
-    id.value = row.originId
+    id.value = Number(row.originId)
   }
   }
 
 
-  classType.value = row.type
   if (row.type === 'device') {
   if (row.type === 'device') {
-    deviceId.value = row.originId
+    deviceId.value = String(row.originId)
     const queryParam = {
     const queryParam = {
       deviceId: row.originId,
       deviceId: row.originId,
       pageNo: 1,
       pageNo: 1,
@@ -425,25 +410,26 @@ const handleFileNodeClick = async (row) => {
     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
     if (data) {
     if (data) {
       const target = data.filter((item) => item.parentId === 0)
       const target = data.filter((item) => item.parentId === 0)
-      clickNodeId.value = target[0].id
+      clickNodeId.value = String(target[0]?.id ?? '')
     }
     }
   } else if (row.type === 'file') {
   } else if (row.type === 'file') {
-    clickNodeId.value = row.originId
+    clickNodeId.value = String(row.originId)
   } else if (row.type === 'dept') {
   } else if (row.type === 'dept') {
     // message.error("请选择设备及文件节点")
     // message.error("请选择设备及文件节点")
     // return
     // return
   }
   }
-  nodeId.value = row.id
+  nodeId.value = String(row.id)
   await getList()
   await getList()
 }
 }
 
 
 // 面包屑点击事件
 // 面包屑点击事件
-const handleBreadcrumbClick = async (index) => {
+const handleBreadcrumbClick = async (index: number) => {
   queryParams.filename = ''
   queryParams.filename = ''
-  formLoading.value = true
   // 忽略当前节点的点击
   // 忽略当前节点的点击
   if (index === breadcrumbs.value.length - 1) return
   if (index === breadcrumbs.value.length - 1) return
 
 
+  formLoading.value = true
+
   // 截断面包屑到点击的节点
   // 截断面包屑到点击的节点
   const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
   const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
   breadcrumbs.value = targetBreadcrumbs
   breadcrumbs.value = targetBreadcrumbs
@@ -453,57 +439,24 @@ const handleBreadcrumbClick = async (index) => {
   if (!targetId) {
   if (!targetId) {
     targetId = topNodeId.value
     targetId = topNodeId.value
   }
   }
+  if (!targetId) {
+    formLoading.value = false
+    return
+  }
+
   queryParams.classId = targetId
   queryParams.classId = targetId
-  clickNodeId.value = targetId
-  nodeId.value = targetId
-  debugger
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  clickNodeId.value = String(targetId)
+  nodeId.value = String(targetId)
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
   list.value = data
   formLoading.value = false
   formLoading.value = false
 }
 }
 
 
-// const handleFileNodeClick = async (row) => {
-//   queryParams.classId = row.id
-//   classType.value = row.type
-//   if (row.type==='device') {
-//     deviceId.value = row.originId
-//     const queryParam = {
-//       deviceId: row.originId,
-//       pageNo: 1,
-//       pagesize: 10,
-//     }
-//     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
-//     debugger
-//     if (data){
-//       const target = data.filter((item)=> item.parentId===0)
-//       clickNodeId.value = target[0].id
-//     }
-//   } else if (row.type === 'file'){
-//     clickNodeId.value = row.originId
-//   } else if (row.type==='dept') {
-//     // message.error("请选择设备及文件节点")
-//     // return
-//   }
-//   nodeId.value = row.id
-//   await getList()
-// }
-/** 获得详情 */
-// const getDetail = async () => {
-//   if (id) {
-//     formLoading.value = true
-//     try {
-//       formData.value = (await IotDeviceApi.getIotDevice(id)) as IotDeviceVO
-//     } finally {
-//       formLoading.value = false
-//     }
-//   }
-// }
 /** 查询列表 */
 /** 查询列表 */
 const getList = async () => {
 const getList = async () => {
   formLoading.value = true
   formLoading.value = true
   try {
   try {
-    const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
-    debugger
+    const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
     list.value = data
     list.value = data
   } finally {
   } finally {
     formLoading.value = false
     formLoading.value = false
@@ -520,90 +473,89 @@ const resetQuery = () => {
   queryFormRef.value?.resetFields()
   queryFormRef.value?.resetFields()
   handleQuery()
   handleQuery()
 }
 }
-const { wsCache } = useCache()
+
 /** 初始化 */
 /** 初始化 */
 onMounted(async () => {
 onMounted(async () => {
-  // await getDetail()
-  // queryParams.deptId = wsCache.get(CACHE_KEY.USER).user.deptId;
-  // await getList()
-  // deviceId.value = params.id as unknown as number
-  id.value = params.id as unknown as number
+  id.value = Number(params.id) || 0
 })
 })
 </script>
 </script>
 <style scoped>
 <style scoped>
-::v-deep .breadcrumb-container {
-  padding: 12px 16px;
-  background-color: #f5f7fa;
-  border-radius: 6px;
-  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.search-panel .el-form-item) {
+  display: flex;
+  align-items: center;
+}
+
+:deep(.search-panel .el-form-item__label) {
+  height: 32px;
+  line-height: 32px;
+  align-items: center;
+}
+
+:deep(.search-panel .el-form-item__content) {
+  display: flex;
+  align-items: center;
 }
 }
-::v-deep .custom-breadcrumb-item {
+
+.tree-page {
+  min-width: 0;
+}
+
+.search-panel,
+.breadcrumb-panel,
+.table-panel {
+  min-width: 0;
+}
+
+:deep(.breadcrumb-container) {
+  min-width: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+:deep(.custom-breadcrumb-item) {
   font-size: 14px;
   font-size: 14px;
-  font-weight: bold;
+  font-weight: 600;
 }
 }
 
 
-/* 面包屑文本样式 */
-::v-deep .el-breadcrumb__item .el-breadcrumb__inner {
+:deep(.el-breadcrumb__item .el-breadcrumb__inner) {
+  padding: 2px 4px;
   color: #101010;
   color: #101010;
   text-decoration: none;
   text-decoration: none;
-  padding: 2px 4px;
   border-radius: 2px;
   border-radius: 2px;
   transition: all 0.2s ease;
   transition: all 0.2s ease;
 }
 }
 
 
-/* 可点击项悬停效果 */
-::v-deep .el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover {
+:deep(.el-breadcrumb__item:not(:last-child) .el-breadcrumb__inner:hover) {
   color: #409eff;
   color: #409eff;
-  background-color: rgba(64, 158, 255, 0.1);
   cursor: pointer;
   cursor: pointer;
+  background-color: rgb(64 158 255 / 10%);
 }
 }
 
 
-/* 当前项样式 */
-::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
-  color: #1890ff;
+:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
   font-weight: 500;
   font-weight: 500;
+  color: #1890ff;
   cursor: default;
   cursor: default;
 }
 }
 
 
-/* 分隔符样式优化 */
-::v-deep .el-breadcrumb__separator {
+:deep(.el-breadcrumb__separator) {
   margin: 0 8px;
   margin: 0 8px;
-  color: #0954f6;
   font-size: 12px;
   font-size: 12px;
+  color: #0954f6;
 }
 }
 
 
-.custom-table {
-  cursor: pointer;
-  --el-table-row-hover-bg-color: #f5f7fa; /* 优化悬停背景色 */
-}
-
-.container-tree {
-  display: flex;
-  height: 100%;
-  user-select: none; /* 防止拖动时选中文本 */
-}
-
-.left-tree {
-  background: #f0f0f0;
+:deep(.zm-table) {
   height: 100%;
   height: 100%;
-  overflow: auto;
-}
 
 
-.right-tree {
-  flex: 1;
-  height: 100%;
-  overflow: auto;
-  margin-left: 15px;
-}
-
-.divider-tree {
-  width: 2px;
-  background: #ccc;
-  cursor: col-resize;
-  position: relative;
-}
+  .el-table__cell {
+    height: 42px;
+  }
 
 
-.divider-tree:hover {
-  background: #666;
+  .el-table__row {
+    cursor: pointer;
+  }
 }
 }
 </style>
 </style>

+ 76 - 4
src/views/pms/qhse/index.vue

@@ -139,7 +139,7 @@
               <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
               <zm-table-column label="编码" align="center" prop="measureCode" fixed="left" />
               <zm-table-column label="序列号" align="center" prop="serialNo" fixed="left" />
               <zm-table-column label="序列号" align="center" prop="serialNo" fixed="left" />
               <zm-table-column label="部门名称" align="center" prop="deptName" />
               <zm-table-column label="部门名称" align="center" prop="deptName" />
-              <zm-table-column label="计量单位" align="center" prop="measureUnit" />
+              <zm-table-column label="精度" align="center" prop="measureUnit" />
               <zm-table-column label="责任人" align="center" prop="dutyPerson" />
               <zm-table-column label="责任人" align="center" prop="dutyPerson" />
               <zm-table-column label="品牌" align="center" prop="brand" />
               <zm-table-column label="品牌" align="center" prop="brand" />
               <zm-table-column label="规格型号" align="center" prop="modelName" />
               <zm-table-column label="规格型号" align="center" prop="modelName" />
@@ -148,6 +148,16 @@
                   <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
                   <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
                 </template>
                 </template>
               </zm-table-column>
               </zm-table-column>
+              <zm-table-column label="状态" align="center" prop="status" min-width="100">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.MEASURE_STATUS" :value="scope.row.status" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="报废时间" align="center" prop="offTime" min-width="120">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.offTime) }}
+                </template>
+              </zm-table-column>
               <zm-table-column label="采购日期" align="center" prop="buyDate">
               <zm-table-column label="采购日期" align="center" prop="buyDate">
                 <template #default="scope">
                 <template #default="scope">
                   {{ formatDateCorrectly(scope.row.buyDate) }}
                   {{ formatDateCorrectly(scope.row.buyDate) }}
@@ -282,8 +292,8 @@
 
 
       <el-row :gutter="20">
       <el-row :gutter="20">
         <el-col :span="12">
         <el-col :span="12">
-          <el-form-item label="计量单位" prop="measureUnit">
-            <el-input v-model="formData.measureUnit" placeholder="请输入计量单位" />
+          <el-form-item label="精度" prop="measureUnit">
+            <el-input v-model="formData.measureUnit" placeholder="请输入精度" />
           </el-form-item>
           </el-form-item>
         </el-col>
         </el-col>
         <el-col :span="12">
         <el-col :span="12">
@@ -313,6 +323,37 @@
         </el-col> -->
         </el-col> -->
       </el-row>
       </el-row>
 
 
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="formData.status"
+              placeholder="请选择状态"
+              clearable
+              class="!w-240px">
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.MEASURE_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="isScrappedStatus(formData.status)">
+          <div class="flex items-center relative">
+            <span class="text-red absolute left-6 top-[-1px]">*</span>
+            <el-form-item label="报废时间" prop="offTime">
+              <el-date-picker
+                v-model="formData.offTime"
+                type="date"
+                value-format="x"
+                placeholder="请选择报废时间"
+                style="width: 100%" />
+            </el-form-item>
+          </div>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20">
       <el-row :gutter="20">
         <el-col :span="12">
         <el-col :span="12">
           <el-form-item label="价格" prop="measurePrice">
           <el-form-item label="价格" prop="measurePrice">
@@ -539,6 +580,8 @@ const formData = ref({
   brand: '',
   brand: '',
   modelName: '',
   modelName: '',
   classify: '',
   classify: '',
+  status: '',
+  offTime: null,
   buyDate: null,
   buyDate: null,
   remark: '',
   remark: '',
   deptId: '',
   deptId: '',
@@ -563,15 +606,31 @@ const formatDateCorrectly = (timestamp) => {
   return formatDate(time).substring(0, 10)
   return formatDate(time).substring(0, 10)
 }
 }
 
 
+const isScrappedStatus = (status) => {
+  const option = getStrDictOptions(DICT_TYPE.MEASURE_STATUS).find((item) => item.value === status)
+  return option?.label === '报废'
+}
+
+const validateOffTime = (_rule, value, callback) => {
+  if (isScrappedStatus(formData.value.status) && !value) {
+    callback(new Error('报废状态必须填写报废时间'))
+    return
+  }
+  callback()
+}
+
 // 表单验证规则
 // 表单验证规则
 const formRules = {
 const formRules = {
   measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
   measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
   dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
   dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
   deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
   deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
   validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
-  serialNo: [{ required: true, message: '序列号不能为空', trigger: 'blur' }]
+  serialNo: [{ required: true, message: '序列号不能为空', trigger: 'blur' }],
+  measureUnit: [{ required: true, message: '精度不能为空', trigger: 'blur' }],
+  offTime: [{ validator: validateOffTime, trigger: ['change', 'blur'] }]
 }
 }
 
 
 /** 查询列表 */
 /** 查询列表 */
@@ -687,6 +746,7 @@ const handleEdit = (row) => {
   formData.value = {
   formData.value = {
     ...row,
     ...row,
     // 确保日期字段正确处理
     // 确保日期字段正确处理
+    offTime: row.offTime ? ensureMillisecondTimestamp(row.offTime) : null,
     buyDate: row.buyDate ? ensureMillisecondTimestamp(row.buyDate) : null,
     buyDate: row.buyDate ? ensureMillisecondTimestamp(row.buyDate) : null,
     validity: row.validity ? ensureMillisecondTimestamp(row.validity) : null
     validity: row.validity ? ensureMillisecondTimestamp(row.validity) : null
   }
   }
@@ -800,6 +860,8 @@ const resetForm = () => {
     brand: '',
     brand: '',
     modelName: '',
     modelName: '',
     classify: '',
     classify: '',
+    status: '',
+    offTime: null,
     buyDate: null,
     buyDate: null,
     remark: '',
     remark: '',
     deptId: '',
     deptId: '',
@@ -818,6 +880,15 @@ const closeDialog = () => {
   resetForm()
   resetForm()
 }
 }
 
 
+watch(
+  () => formData.value.status,
+  (value) => {
+    if (!isScrappedStatus(value)) {
+      formData.value.offTime = null
+    }
+  }
+)
+
 // 提交表单
 // 提交表单
 const submitForm = async () => {
 const submitForm = async () => {
   if (!formRef.value) return
   if (!formRef.value) return
@@ -830,6 +901,7 @@ const submitForm = async () => {
     const submitData = {
     const submitData = {
       ...formData.value,
       ...formData.value,
       // 确保日期字段以正确的格式提交
       // 确保日期字段以正确的格式提交
+      offTime: formData.value.offTime,
       buyDate: formData.value.buyDate,
       buyDate: formData.value.buyDate,
       validity: formData.value.validity
       validity: formData.value.validity
     }
     }

+ 26 - 2
src/views/pms/qhse/iotmeasuredetect/IotMeasureDetectForm.vue

@@ -69,6 +69,11 @@
       </el-row>
       </el-row>
 
 
       <el-row :gutter="20">
       <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="精度" prop="precisions">
+            <el-input v-model="formData.precisions" placeholder="请输入精度" />
+          </el-form-item>
+        </el-col>
         <el-col :span="12">
         <el-col :span="12">
           <el-form-item label="校准金额" prop="detectAmount">
           <el-form-item label="校准金额" prop="detectAmount">
             <el-input v-model="formData.detectAmount" placeholder="请输入校准金额" />
             <el-input v-model="formData.detectAmount" placeholder="请输入校准金额" />
@@ -76,6 +81,20 @@
         </el-col>
         </el-col>
       </el-row>
       </el-row>
 
 
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检测周期" prop="detectCycle">
+            <el-select v-model="formData.detectCycle" placeholder="检测周期" clearable>
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.QHSE_DETECT_CYCLE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
       <el-row :gutter="20">
       <el-row :gutter="20">
         <el-col :span="24">
         <el-col :span="24">
           <el-form-item label="检测/校准内容" prop="detectContent">
           <el-form-item label="检测/校准内容" prop="detectContent">
@@ -228,6 +247,7 @@ import { formatDate } from '@/utils/formatTime'
 import { handleTree } from '@/utils/tree'
 import { handleTree } from '@/utils/tree'
 import { onMounted, ref } from 'vue'
 import { onMounted, ref } from 'vue'
 import * as DeptApi from '@/api/system/dept'
 import * as DeptApi from '@/api/system/dept'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 
 
 /** 计量器具-检测校准明细 表单 */
 /** 计量器具-检测校准明细 表单 */
 defineOptions({ name: 'IotMeasureDetectForm' })
 defineOptions({ name: 'IotMeasureDetectForm' })
@@ -252,7 +272,9 @@ const formData = ref({
   measureName: '',
   measureName: '',
   measureId: '',
   measureId: '',
   measureCertNo: '',
   measureCertNo: '',
-  file: ''
+  file: '',
+  precisions: '',
+  detectCycle: ''
 })
 })
 const formRules = reactive({
 const formRules = reactive({
   measureId: [{ required: true, message: '计量器具不能为空', trigger: 'blur' }],
   measureId: [{ required: true, message: '计量器具不能为空', trigger: 'blur' }],
@@ -262,7 +284,9 @@ const formRules = reactive({
   validityPeriod: [{ required: true, message: '检测/校准有效期不能为空', trigger: 'blur' }],
   validityPeriod: [{ required: true, message: '检测/校准有效期不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
   detectStandard: [{ required: true, message: '检测/校准标准不能为空', trigger: 'blur' }],
   detectStandard: [{ required: true, message: '检测/校准标准不能为空', trigger: 'blur' }],
-  file: [{ required: true, message: '请上传文件', trigger: 'blur' }]
+  file: [{ required: true, message: '请上传文件', trigger: 'blur' }],
+  precisions: [{ required: true, message: '精度不能为空', trigger: 'blur' }],
+  detectCycle: [{ required: true, message: '请选择检测周期', trigger: 'blur' }]
 })
 })
 const formRef = ref() // 表单 Ref
 const formRef = ref() // 表单 Ref
 const measureList = ref([])
 const measureList = ref([])

+ 7 - 0
src/views/pms/qhse/iotmeasuredetect/index.vue

@@ -125,6 +125,12 @@
                   }}</span>
                   }}</span>
                 </template>
                 </template>
               </zm-table-column>
               </zm-table-column>
+              <zm-table-column label="检测周期" align="center" width="140">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.QHSE_DETECT_CYCLE" :value="scope.row.detect_cycle" />
+                </template>
+              </zm-table-column>
+              <zm-table-column label="精度" align="center" prop="precisions" />
               <zm-table-column label="校准金额" align="center" prop="detectAmount" />
               <zm-table-column label="校准金额" align="center" prop="detectAmount" />
               <zm-table-column label="部门名称" align="center" prop="deptName" />
               <zm-table-column label="部门名称" align="center" prop="deptName" />
               <zm-table-column label="附件" align="center" prop="file" min-width="90">
               <zm-table-column label="附件" align="center" prop="file" min-width="90">
@@ -206,6 +212,7 @@ import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 const { ZmTable, ZmTableColumn } = useTableComponents()
 import { useUserStore } from '@/store/modules/user'
 import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE } from '@/utils/dict'
 
 
 /** 计量器具-检测校准明细 列表 */
 /** 计量器具-检测校准明细 列表 */
 defineOptions({ name: 'IotMeasureDetect' })
 defineOptions({ name: 'IotMeasureDetect' })

+ 139 - 3
src/views/pms/qhse/kanban/index.vue

@@ -301,7 +301,7 @@ const bottomCards = ref([
     lines: ['职业健康体检率', '98.7%(达标)']
     lines: ['职业健康体检率', '98.7%(达标)']
   },
   },
   {
   {
-    title: '环境危废',
+    title: '危废管控',
     icon: Flag,
     icon: Flag,
     accent: '#28c98b',
     accent: '#28c98b',
     glow: 'rgba(40, 201, 139, 0.2)',
     glow: 'rgba(40, 201, 139, 0.2)',
@@ -357,7 +357,7 @@ const safeDayBarData = ref<SafeDayBarItem[]>([])
 const summaryPanel = ref<any>(null)
 const summaryPanel = ref<any>(null)
 const total = ref(0)
 const total = ref(0)
 const instrumentExpired = ref(0)
 const instrumentExpired = ref(0)
-const safeDayDisplayOrder = ['瑞气能源', '瑞霖能源', '5#项目', '酸化压裂', '气体增产', '钻修井']
+const safeDayDisplayOrder = ['瑞气能源', '瑞霖技术', '5#项目', '酸化压裂', '气体增产', '钻修井']
 
 
 function buildSafeDayBarData(baseData: SafeDayMap) {
 function buildSafeDayBarData(baseData: SafeDayMap) {
   const mappedData = Object.entries(baseData || {}).map(([name, value]) => ({
   const mappedData = Object.entries(baseData || {}).map(([name, value]) => ({
@@ -1000,6 +1000,21 @@ const dangerDialogQueryParams = reactive({
   deptId: userStore.getUser.deptId
   deptId: userStore.getUser.deptId
 })
 })
 
 
+const instrumentDialogVisible = ref(false)
+const instrumentDialogTitle = ref('')
+const instrumentDialogLoading = ref(false)
+const instrumentDialogList = ref<any[]>([])
+const instrumentDialogTotal = ref(0)
+const instrumentDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  measureName: undefined as string | undefined,
+  measureCertNo: undefined as string | undefined,
+  deptId: userStore.getUser.deptId,
+  expired: undefined as boolean | undefined,
+  alertWarn: undefined as boolean | undefined
+})
+
 const getCertificateTypeText = (type: string) => {
 const getCertificateTypeText = (type: string) => {
   const map: Record<string, string> = {
   const map: Record<string, string> = {
     personal: '个人证书',
     personal: '个人证书',
@@ -1112,6 +1127,17 @@ async function getDangerDialogList() {
   }
   }
 }
 }
 
 
+async function getInstrumentDialogList() {
+  instrumentDialogLoading.value = true
+  try {
+    const data = await IotInstrumentApi.getInstrumentList(instrumentDialogQueryParams)
+    instrumentDialogList.value = data.list || []
+    instrumentDialogTotal.value = data.total || 0
+  } finally {
+    instrumentDialogLoading.value = false
+  }
+}
+
 function openHazardDialog(barLabel: string) {
 function openHazardDialog(barLabel: string) {
   hazardDialogQueryParams.pageNo = 1
   hazardDialogQueryParams.pageNo = 1
   hazardDialogQueryParams.pageSize = 10
   hazardDialogQueryParams.pageSize = 10
@@ -1149,6 +1175,26 @@ function openDangerDialog(zone: { title?: string }) {
   getDangerDialogList()
   getDangerDialogList()
 }
 }
 
 
+function openInstrumentDialog(type: 'all' | 'expired') {
+  instrumentDialogQueryParams.pageNo = 1
+  instrumentDialogQueryParams.pageSize = 10
+  instrumentDialogQueryParams.measureName = undefined
+  instrumentDialogQueryParams.measureCertNo = undefined
+  instrumentDialogQueryParams.deptId = userStore.getUser.deptId
+  instrumentDialogQueryParams.alertWarn = undefined
+
+  if (type === 'expired') {
+    instrumentDialogTitle.value = '待检设备列表'
+    instrumentDialogQueryParams.expired = true
+  } else {
+    instrumentDialogTitle.value = '在用设备列表'
+    instrumentDialogQueryParams.expired = undefined
+  }
+
+  instrumentDialogVisible.value = true
+  getInstrumentDialogList()
+}
+
 function handleHazardBarClick(params: { name?: string }) {
 function handleHazardBarClick(params: { name?: string }) {
   if (!params?.name) return
   if (!params?.name) return
   openHazardDialog(params.name)
   openHazardDialog(params.name)
@@ -1467,7 +1513,23 @@ onUnmounted(() => {
                 <div class="bottom-card__title">{{ card.title }}</div>
                 <div class="bottom-card__title">{{ card.title }}</div>
               </div>
               </div>
               <div class="bottom-card__content">
               <div class="bottom-card__content">
-                <p v-for="line in card.lines" :key="line">{{ line }}</p>
+                <template v-if="card.title === '安全检测'">
+                  <button
+                    type="button"
+                    class="bottom-card__action"
+                    @click="openInstrumentDialog('all')">
+                    {{ card.lines[0] }}
+                  </button>
+                  <button
+                    type="button"
+                    class="bottom-card__action"
+                    @click="openInstrumentDialog('expired')">
+                    {{ card.lines[1] }}
+                  </button>
+                </template>
+                <template v-else>
+                  <p v-for="line in card.lines" :key="line">{{ line }}</p>
+                </template>
               </div>
               </div>
             </article>
             </article>
           </section>
           </section>
@@ -1895,6 +1957,61 @@ onUnmounted(() => {
       </div>
       </div>
     </div>
     </div>
   </Dialog>
   </Dialog>
+
+  <Dialog v-model="instrumentDialogVisible" :title="instrumentDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="instrumentDialogLoading"
+        :data="instrumentDialogList"
+        :row-style="tableRowStyle"
+        :row-class-name="tableRowClassName"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (instrumentDialogQueryParams.pageNo - 1) * instrumentDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="名称" align="center" prop="measureName" min-width="140" />
+        <el-table-column label="编码" align="center" prop="measureCode" min-width="120" />
+        <el-table-column label="序列号" align="center" prop="serialNo" min-width="120" />
+        <el-table-column label="部门名称" align="center" prop="deptName" min-width="140" />
+        <el-table-column label="计量单位" align="center" prop="measureUnit" min-width="100" />
+        <el-table-column label="责任人" align="center" prop="dutyPerson" min-width="100" />
+        <el-table-column label="品牌" align="center" prop="brand" min-width="100" />
+        <el-table-column label="规格型号" align="center" prop="modelName" min-width="120" />
+        <el-table-column label="分类" align="center" min-width="120">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
+          </template>
+        </el-table-column>
+        <el-table-column label="采购日期" align="center" min-width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.buyDate) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="价格" align="center" prop="measurePrice" min-width="100" />
+        <el-table-column
+          label="备注"
+          align="center"
+          prop="remark"
+          min-width="140"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="instrumentDialogTotal"
+          v-model:page="instrumentDialogQueryParams.pageNo"
+          v-model:limit="instrumentDialogQueryParams.pageSize"
+          @pagination="getInstrumentDialogList" />
+      </div>
+    </div>
+  </Dialog>
 </template>
 </template>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -1971,6 +2088,25 @@ onUnmounted(() => {
   color: #5c7393;
   color: #5c7393;
 }
 }
 
 
+.bottom-card__action {
+  display: block;
+  width: 100%;
+  padding: 0;
+  text-align: left;
+  color: #617491;
+  background: transparent;
+  border: none;
+  cursor: pointer;
+  transition: color 0.2s ease;
+  font-size: 18px;
+  font-weight: 700;
+  color: #5c7393;
+}
+
+.bottom-card__action:hover {
+  color: #2d78ea;
+}
+
 .risk-card--clickable {
 .risk-card--clickable {
   cursor: pointer;
   cursor: pointer;
   transition:
   transition: