瀏覽代碼

qhse资料库

yanghao 1 周之前
父節點
當前提交
7aabb273d9

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.200:48080  https://iot.deepoil.cc:5443  http://172.26.0.56:48080  http://192.168.188.198:48080
-VITE_BASE_URL='https://iot.deepoil.cc:5443'
+VITE_BASE_URL='http://172.26.0.56:48080'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 63 - 6
src/api/pms/qhse/index.ts

@@ -666,16 +666,73 @@ export const EmergencyDrillCertApi = {
 
 // 资料库
 export const DataLibraryApi = {
-  // 获得资料库分页
-  getDataLibraryList: async (params) => {
+  // // 获得资料库分页
+  // 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 })
   },
-  // 删除资料库
-  deleteDataLibrary: async (id) => {
+  // 查询资料库的子目录及文件
+  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 })
   },
 
-  getFileTree: async () => {
-    return await request.get({ url: '/rq/qhse-file-tree/page' })
+  // 导出资料 Excel
+  exportIotInfo: async (params) => {
+    return await request.download({ url: `/rq/qhse-info/export-excel`, params })
   }
 }

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

+ 219 - 339
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="请输入名称66">
-      <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>
 import { ElTree } from 'element-plus'
+import { CaretLeft, CaretRight, Search } from '@element-plus/icons-vue'
+import type { IotTreeVO } from '@/api/system/tree'
+import { IotTreeApi } from '@/api/system/tree'
 import { defaultProps, handleTree } from '@/utils/tree'
-import { CommonStatusEnum } from '@/utils/constants'
-import { DataLibraryApi } from '@/api/pms/qhse/index'
-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
-}
 
-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 emits = defineEmits<{
+  (event: 'node-click', value: TreeNode): void
+  (event: 'success', value?: number): void
+}>()
 
-const deptName = ref('')
-const nodeInfo = ref({})
-const treeList = ref<Tree[]>([]) // 树形结构
+const isCollapsed = ref(false)
+const treeName = ref('')
+const treeList = ref<TreeNode[]>([])
 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 = (
-  nodes: Tree[],
+  nodes: TreeNode[],
   targetId: number | string,
-  path: (number | string)[] = []
-): (number | string)[] | null => {
+  path: Array<number | string> = []
+): Array<number | string> | null => {
   for (const node of nodes) {
-    path.push(node.id)
+    const nextPath = [...path, node.id]
+
     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
 }
 
-// 新增:定位节点并高亮
 const locateNode = (targetId: number | string) => {
   if (!targetId || !treeList.value.length) return
 
   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' })
-        }
-      }
-    })
-  }
-}
-
-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
-  }
-}
+  if (!pathIds) return
 
-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 DataLibraryApi.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 res = await DataLibraryApi.getFileTree()
-  treeList.value = []
-  treeList.value.push(...handleTree(res))
+  const res = await IotTreeApi.getSimpleTreeList()
+  treeList.value = handleTree(res) as TreeNode[]
+  expandedKeys.value = treeList.value.map((node) => node.id)
 
-  // 处理展开逻辑:有currentId则展开对应路径,否则展开一级节点
   if (props.currentId) {
     locateNode(props.currentId)
-  } else {
-    firstLevelKeys.value = treeList.value.map((node) => node.id)
-    expandedKeys.value = [...firstLevelKeys.value]
   }
+
   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)
 }
 
-/** 提交表单 */
-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 DataLibraryApi.createIotTree(formData.value)
-    } else {
-      await DataLibraryApi.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(
   () => props.currentId,
   (newVal) => {
-    if (newVal && treeList.value.length) {
+    if (newVal) {
       locateNode(newVal)
     }
-  },
-  { immediate: true }
+  }
 )
+
+onMounted(getTreeInfo)
 </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>
-.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;
+}
+
+.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;
+}
+
+.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;
 }
-.custom-menu ul {
-  list-style: none;
-  padding: 0;
-  margin: 0;
+
+.tree-node__label {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
-.custom-menu li {
-  padding: 2px 10px;
+
+.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;
-  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>

+ 310 - 304
src/views/pms/qhse/data/index.vue

@@ -1,6 +1,63 @@
 <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-item
           v-for="(item, index) in breadcrumbs"
@@ -14,169 +71,120 @@
       <el-input
         v-model="queryParams.allName"
         :placeholder="'在' + breadcrumbs[breadcrumbs.length - 1].name + '下搜索'"
-        style="width: 250px; height: 30px"
+        size="default"
+        style="width: 250px"
         @input="searchFolderAndFile" />
     </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-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>
+
   <IotInfoFormTree
     ref="formRef"
     @success="getList"
@@ -185,45 +193,69 @@
     :classId="clickNodeId" />
 </template>
 <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 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 './PmsTree.vue'
-import { useCache } from '@/hooks/web/useCache'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { IotInfoClassifyApi } from '@/api/pms/info'
 import { IotTreeApi } from '@/api/system/tree'
-defineOptions({ name: 'QHSEFile' })
+defineOptions({ name: 'IotTree' })
+
+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 container = ref(null)
-const leftWidth = ref(350) // 初始左侧宽度
-const rightWidth = ref(window.innerWidth * 0.8)
-let isDragging = false
 const uploadLoading = ref(false)
 
 const searchFolderAndFile = async () => {
-  debugger
   formLoading.value = true
-  const data = await IotInfoApi.IotInfoApi.getAllChildContentFile(queryParams)
-  debugger
+  const data = await DataLibraryApi.DataLibraryApi.getAllChildContentFile(queryParams)
   list.value = data
   formLoading.value = false
   queryParams.filename = ''
 }
-const openWeb = (url) => {
-  window.open(
-    'http://1.94.244.160:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(url))
-  )
-}
-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
   topNodeId.value = id
   const rootItem = breadcrumbs.value.find((item) => item.type === 'root')
@@ -235,34 +267,15 @@ const successList = async (id) => {
   // 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 { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const loading = ref(true) // 列表的加载中
 const { params } = useRoute() // 查询参数
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const list = ref<IotDeviceVO[]>([]) // 列表的数据
+const list = ref<FileRow[]>([]) // 列表的数据
 // const total = ref(0) // 列表的总页数
-const id = ref()
-const queryParams = reactive({
+const id = ref(0)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   filename: null,
@@ -273,18 +286,8 @@ const queryParams = reactive({
   deptId: undefined,
   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 {
     formLoading.value = true
     const response = await fetch(url)
@@ -293,7 +296,7 @@ const handleDownload = async (url) => {
 
     const link = document.createElement('a')
     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()
 
     URL.revokeObjectURL(downloadUrl)
@@ -302,9 +305,9 @@ const handleDownload = async (url) => {
     console.error('下载失败:', error)
   }
 }
-const deleteInfo = async (id) => {
+const deleteInfo = async (id: TreeKey) => {
   await message.delConfirm()
-  await IotInfoApi.IotInfoApi.deleteIotInfo(id).then((res) => {
+  await DataLibraryApi.DataLibraryApi.deleteIotInfo(Number(id)).then((res) => {
     if (res) {
       message.success('文件删除成功')
       getList()
@@ -313,8 +316,7 @@ const deleteInfo = async (id) => {
     }
   })
 }
-
-const formRef = ref()
+const formRef = ref<IotInfoFormTreeExpose>()
 const openForm = async (type: string, id?: number) => {
   // if (classType.value==='dept'){
   //   message.error(t('common.deptChoose'))
@@ -323,10 +325,11 @@ const openForm = async (type: string, id?: number) => {
   uploadLoading.value = true
   if (!queryParams.classId) {
     message.error(t('common.leftNode'))
+    uploadLoading.value = false
     return
   }
-  formRef.value.open(type, id)
-  await new Promise((resolve) => {
+  formRef.value?.open(type, id)
+  await new Promise<void>((resolve) => {
     setTimeout(() => {
       resolve() // 操作完成后resolve
     }, 2000) // 模拟2秒耗时
@@ -336,14 +339,13 @@ const openForm = async (type: string, id?: number) => {
 const deviceId = ref('')
 const clickNodeId = ref('')
 const nodeId = ref('')
-const classType = ref('')
 
-const breadcrumbs = ref([
+const breadcrumbs = ref<BreadcrumbItem[]>([
   { id: null, name: '科瑞石油技术', type: 'root' } // 根节点
 ])
 
 // 共享的面包屑更新逻辑
-const updateBreadcrumbs = async (node) => {
+const updateBreadcrumbs = async (node: FileRow) => {
   // 查找当前节点是否已在面包屑中
   const currentIndex = breadcrumbs.value.findIndex((item) => item.id === node.id)
 
@@ -352,18 +354,20 @@ const updateBreadcrumbs = async (node) => {
     breadcrumbs.value = breadcrumbs.value.slice(0, currentIndex + 1)
   } 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
-  const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
+  const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
 }
 
 // 表格行点击事件
-const inContent = async (row) => {
+const inContent = async (row: FileRow) => {
   if (row.fileType != 'content') {
+    if (!row.filePath) return
+
     window.open(
       'http://doc.deepoil.cc:8012/onlinePreview?url=' +
         encodeURIComponent(Base64.encode(row.filePath))
@@ -376,14 +380,14 @@ const inContent = async (row) => {
   await updateBreadcrumbs(row)
   // 可以添加其他表格行点击需要的逻辑
   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
   list.value = data
 }
 
 // 文件树节点点击事件
-const handleFileNodeClick = async (row) => {
+const handleFileNodeClick = async (row: FileRow) => {
   queryParams.filename = ''
   const parentItems = await IotTreeApi.getParentIds(row.id)
   breadcrumbs.value = []
@@ -393,12 +397,11 @@ const handleFileNodeClick = async (row) => {
 
   queryParams.classId = row.id
   if (row.type == 'device') {
-    id.value = row.originId
+    id.value = Number(row.originId)
   }
 
-  classType.value = row.type
   if (row.type === 'device') {
-    deviceId.value = row.originId
+    deviceId.value = String(row.originId)
     const queryParam = {
       deviceId: row.originId,
       pageNo: 1,
@@ -407,25 +410,26 @@ const handleFileNodeClick = async (row) => {
     const data = await IotInfoClassifyApi.getIotInfoClassifyPage(queryParam)
     if (data) {
       const target = data.filter((item) => item.parentId === 0)
-      clickNodeId.value = target[0].id
+      clickNodeId.value = String(target[0]?.id ?? '')
     }
   } else if (row.type === 'file') {
-    clickNodeId.value = row.originId
+    clickNodeId.value = String(row.originId)
   } else if (row.type === 'dept') {
     // message.error("请选择设备及文件节点")
     // return
   }
-  nodeId.value = row.id
+  nodeId.value = String(row.id)
   await getList()
 }
 
 // 面包屑点击事件
-const handleBreadcrumbClick = async (index) => {
+const handleBreadcrumbClick = async (index: number) => {
   queryParams.filename = ''
-  formLoading.value = true
   // 忽略当前节点的点击
   if (index === breadcrumbs.value.length - 1) return
 
+  formLoading.value = true
+
   // 截断面包屑到点击的节点
   const targetBreadcrumbs = breadcrumbs.value.slice(0, index + 1)
   breadcrumbs.value = targetBreadcrumbs
@@ -435,11 +439,15 @@ const handleBreadcrumbClick = async (index) => {
   if (!targetId) {
     targetId = topNodeId.value
   }
+  if (!targetId) {
+    formLoading.value = false
+    return
+  }
+
   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
   formLoading.value = false
 }
@@ -448,8 +456,7 @@ const handleBreadcrumbClick = async (index) => {
 const getList = async () => {
   formLoading.value = true
   try {
-    const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
-    debugger
+    const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
     list.value = data
   } finally {
     formLoading.value = false
@@ -466,90 +473,89 @@ const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
-const { wsCache } = useCache()
+
 /** 初始化 */
 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>
 <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-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;
   text-decoration: none;
-  padding: 2px 4px;
   border-radius: 2px;
   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;
-  background-color: rgba(64, 158, 255, 0.1);
   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;
+  color: #1890ff;
   cursor: default;
 }
 
-/* 分隔符样式优化 */
-::v-deep .el-breadcrumb__separator {
+:deep(.el-breadcrumb__separator) {
   margin: 0 8px;
-  color: #0954f6;
   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%;
-  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>