瀏覽代碼

feat: 优化设备资料库树和表格布局
精简 PmsTree 为树选择组件,支持搜索、节点定位和展开收起
优化设备资料库页面左右布局、搜索区和表格区域样式
将资料列表切换为 ZmTable,并添加操作列设置
补充页面类型定义,修复节点 ID、文件路径和面包屑相关类型问题

Zimo 1 周之前
父節點
當前提交
b8bc40f258
共有 2 個文件被更改,包括 525 次插入730 次删除
  1. 219 344
      src/views/system/tree/PmsTree.vue
  2. 306 386
      src/views/system/tree/index.vue

+ 219 - 344
src/views/system/tree/PmsTree.vue

@@ -1,409 +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-item label="备注" prop="remark" label-width="110px">-->
-<!--        <el-input-->
-<!--          v-model="formData.remark"-->
-<!--          maxlength="11"-->
-<!--          placeholder="请输入备注"-->
-<!--          type="textarea"-->
-<!--        />-->
-<!--      </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 { 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 { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useI18n } from 'vue-i18n'
-import { useRouter } from 'vue-router'
-import {IotInfoClassifyApi} from "@/api/pms/info";
-
-// 类型定义
-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 openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+interface TreeNode extends IotTreeVO {
+  children?: TreeNode[]
 }
 
-/** 重置表单 */
-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()
-}
+type TreeKey = number | string
 
-defineOptions({ name: 'IotTree' })
+const props = withDefaults(
+  defineProps<{
+    deviceId: number
+    currentId?: TreeKey | null
+  }>(),
+  {
+    currentId: null
+  }
+)
 
-// 接收外部参数
-const props = defineProps({
-  deviceId: { type: Number, required: true },
-  currentId: { type: [Number, String], default: null } // 新增:接收需要定位的节点ID
-})
+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 contextMenuRef = ref(null) // 弹窗DOM引用
-let selectedNode = null
-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[], targetId: number | string, path: (number | string)[] = []): (number | string)[] | null => {
+const findNodePath = (
+  nodes: TreeNode[],
+  targetId: number | string,
+  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 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 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) {
     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 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(
   () => 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: 1px solid #e4e7ed;
-  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>

+ 306 - 386
src/views/system/tree/index.vue

@@ -1,264 +1,261 @@
 <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"
           :key="index"
           @click="handleBreadcrumbClick(index)"
           :class="{ 'current-crumb': index === breadcrumbs.length - 1 }"
-          class="custom-breadcrumb-item"
-        >
+          class="custom-breadcrumb-item">
           {{ item.name }}
         </el-breadcrumb-item>
       </el-breadcrumb>
       <el-input
         v-model="queryParams.allName"
         :placeholder="'在' + breadcrumbs[breadcrumbs.length - 1].name + '下搜索'"
-        style="width: 250px; height: 30px"
-        @input="searchFolderAndFile"
-      />
+        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' }">
-        <ContentWrap>
-          <el-form
-            class="-mb-15px"
-            :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>
-        </ContentWrap>
-        <ContentWrap>
-          <el-table
-            v-loading="formLoading"
-            :data="list"
-            :stripe="true"
-            :show-overflow-tooltip="true"
-            @row-dblclick="inContent"
-            class="custom-table"
-          >
-            <el-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>
-            </el-table-column>
-            <el-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>
-            </el-table-column>
-            <el-table-column :label="t('file.fileSize')" align="center" prop="fileSize" />
-            <!--            <el-table-column :label="t('file.preview') " align="center" prop="filePath" >-->
-            <!--              <template #default="scope">-->
-            <!--                <el-button v-if="scope.row.fileType!=='content'" link type="primary" @click="openWeb(scope.row.filePath)"> <Icon size="19" icon="ep:view"  /> </el-button>-->
-            <!--              </template>-->
-            <!--            </el-table-column>-->
-            <el-table-column :label="t('file.dept')" align="center" prop="deptName" />
-            <el-table-column
-              :label="t('file.device')"
-              align="center"
-              prop="deviceCode"
-              min-width="220"
-            />
-            />
-            <el-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>
-            </el-table-column>
-          </el-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"
     :deviceId="deviceId"
     :nodeId="nodeId"
-    :classId="clickNodeId"
-  />
+    :classId="clickNodeId" />
 </template>
 <script lang="ts" setup>
-import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
-import { dateFormatter } from '@/utils/formatTime'
 import IotInfoFormTree from '@/views/pms/iotinfo/IotInfoFormTree.vue'
 import * as IotInfoApi from '@/api/pms/iotinfo'
-import { IotInfoVO } from '@/api/pms/iotinfo'
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, onMounted } from 'vue'
+import { Base64 } from 'js-base64'
 
 import PmsTree from '@/views/system/tree/PmsTree.vue'
-import { CACHE_KEY, 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: '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 searchFolderAndFile = async () => {
-  debugger
   formLoading.value = true
   const data = await IotInfoApi.IotInfoApi.getAllChildContentFile(queryParams)
-  debugger
   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 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
   topNodeId.value = id
   const rootItem = breadcrumbs.value.find((item) => item.type === 'root')
@@ -270,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,
@@ -308,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)
@@ -328,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)
@@ -337,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 IotInfoApi.IotInfoApi.deleteIotInfo(Number(id)).then((res) => {
     if (res) {
       message.success('文件删除成功')
       getList()
@@ -348,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) => {
   // if (classType.value==='dept'){
   //   message.error(t('common.deptChoose'))
@@ -373,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秒耗时
@@ -386,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)
 
@@ -402,7 +354,7 @@ 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 || '' })
   }
 
   // 更新表格数据
@@ -412,8 +364,10 @@ const updateBreadcrumbs = async (node) => {
 }
 
 // 表格行点击事件
-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))
@@ -426,14 +380,14 @@ const inContent = async (row) => {
   await updateBreadcrumbs(row)
   // 可以添加其他表格行点击需要的逻辑
   queryParams.classId = row.id
-  nodeId.value = row.id
+  nodeId.value = String(row.id)
   const data = await IotInfoApi.IotInfoApi.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 = []
@@ -443,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,
@@ -457,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
@@ -485,57 +439,24 @@ 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
+  clickNodeId.value = String(targetId)
+  nodeId.value = String(targetId)
   const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
   list.value = data
   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 () => {
   formLoading.value = true
   try {
     const data = await IotInfoApi.IotInfoApi.getChildContentFile(queryParams)
-    debugger
     list.value = data
   } finally {
     formLoading.value = false
@@ -552,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>