Browse Source

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

yanghao 1 tuần trước cách đây
mục cha
commit
18ca5dd9cd

+ 14 - 31
src/api/pms/qhse/index.ts

@@ -666,37 +666,6 @@ export const EmergencyDrillCertApi = {
 
 // 资料库
 export const DataLibraryApi = {
-  // // 获得资料库分页
-  // getDataLibraryList: async (params) => {
-  //   return await request.get({ url: `/rq/qhse-info/page`, params })
-  // },
-  // // 删除资料库
-  // deleteDataLibrary: async (id) => {
-  //   return await request.delete({ url: `/rq/qhse-info/delete?id=` + id })
-  // },
-
-  // // 添加资料库
-  // createDataLibrary: async (data) => {
-  //   return await request.post({ url: `/rq/qhse-info/create`, data })
-  // },
-  // // 获取详情
-  // getDataLibrary: async (id) => {
-  //   return await request.get({ url: `/rq/qhse-info/get?id=` + id })
-  // },
-  // // 修改资料库
-  // updateDataLibrary: async (data) => {
-  //   return await request.put({ url: `/rq/qhse-info/update`, data })
-  // },
-
-  // // 导出资料库 Excel
-  // exportDataLibrary: async (params) => {
-  //   return await request.download({ url: `/rq/qhse-info/export-excel`, params })
-  // },
-
-  // getFileTree: async () => {
-  //   return await request.get({ url: '/rq/qhse-file-tree/page' })
-  // }
-
   // 查询资料分页
   getIotInfoPage: async (params: any) => {
     return await request.get({ url: `/rq/qhse-info/page`, params })
@@ -734,5 +703,19 @@ export const DataLibraryApi = {
   // 导出资料 Excel
   exportIotInfo: async (params) => {
     return await request.download({ url: `/rq/qhse-info/export-excel`, params })
+  },
+
+  // 新增目录
+  createFileTree: async (data) => {
+    return await request.post({ url: `/rq/qhse-file-tree/create`, data })
+  },
+
+  // 修改目录
+  updateFileTree: async (data) => {
+    return await request.put({ url: `/rq/qhse-file-tree/update`, data })
+  },
+  // 删除目录
+  deleteFileTree: async (id: number) => {
+    return await request.delete({ url: `/rq/qhse-file-tree/delete?id=` + id })
   }
 }

+ 40 - 64
src/views/pms/qhse/data/IotInfoFormTree.vue

@@ -7,16 +7,9 @@
       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-input v-model="displayNodeName" disabled placeholder="请选择文件目录" />
       </el-form-item>
-      <el-form-item :label="t('fileInfo.appendix')" prop="filePath" style="">
+      <el-form-item :label="t('fileInfo.appendix')" prop="filePath">
         <file-upload
           style="height: 300px"
           @uploadSuccess="handleUploadSuccess"
@@ -46,35 +39,35 @@
     </el-form>
     <template #footer>
       <el-button
-        @click="submitForm"
-        type="primary"
         v-if="formType !== 'detail'"
+        type="primary"
         :disabled="formLoading"
-        >{{ t('common.ok') }}</el-button
-      >
+        @click="submitForm">
+        {{ 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 { t } = useI18n()
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const displayNodeName = ref('')
+const formRef = ref()
+const formRules = reactive({})
 
-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,
@@ -88,69 +81,52 @@ const formData = ref({
   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
+  deptId: string
+  nodeName: string
 }>()
-const classIdd = ref('')
-/** 打开弹窗 */
+
+const emit = defineEmits(['success'])
+
 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)
+      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))
+
+  displayNodeName.value = props.nodeName
+  formData.value.classId = props.classId
+  formData.value.deptId = props.deptId
 }
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
+defineExpose({ open })
+
 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)
+      data.classId = props.classId
+      data.deptId = props.deptId
+      data.folderJson = JSON.stringify(data.folderTree)
       await DataLibraryApi.createIotInfo(data)
       message.success(t('common.createSuccess'))
     } else {
@@ -158,27 +134,27 @@ const submitForm = async () => {
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
-    // 发送操作成功的事件
     emit('success')
-    debugger
   } finally {
     formLoading.value = false
   }
 }
 
-/** 重置表单 */
 const resetForm = () => {
   formData.value = {
     id: undefined,
     deviceId: undefined,
+    classId: undefined,
     deptId: undefined,
     filename: undefined,
     fileType: undefined,
     filePath: undefined,
     remark: undefined,
     fileList: [],
-    folderTree: []
+    folderTree: [],
+    folderJson: undefined
   }
+  displayNodeName.value = ''
   formRef.value?.resetFields()
 }
 

+ 238 - 1
src/views/pms/qhse/data/PmsTree.vue

@@ -3,10 +3,141 @@ 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 { DataLibraryApi } from '@/api/pms/qhse/index'
 import { defaultProps, handleTree } from '@/utils/tree'
+import { CommonStatusEnum } from '@/utils/constants'
 
 defineOptions({ name: 'IotTree' })
 
+const formLoading = ref(false)
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formRef = ref() // 搜索的表单
+const message = useMessage()
+const nodeInfo = ref({})
+const menuVisible = ref(false)
+const menuX = ref(0)
+const menuY = ref(0)
+const contextMenuRef = ref(null) // 弹窗DOM引用
+const { t } = useI18n()
+let selectedNode = null
+const parentId = ref()
+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' }]
+})
+const handleRightClick = (event, data, node) => {
+  nodeInfo.value = { ...data }
+  event.preventDefault()
+  const nodeContent = (event.target as HTMLElement)?.closest(
+    '.el-tree-node__content'
+  ) as HTMLElement | null
+  if (nodeContent) {
+    const rect = nodeContent.getBoundingClientRect()
+    menuX.value = rect.right + 8
+    menuY.value = rect.top
+  } else {
+    menuX.value = event.clientX
+    menuY.value = event.clientY
+  }
+  selectedNode = data
+  parentId.value = data.id
+
+  menuVisible.value = true
+}
+
+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()
+}
+
+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 }
+
+      break
+    case 'delete':
+      // 删除的二次确认
+      await message.delConfirm()
+      // 假设存在删除接口
+      await IotTreeApi.deleteIotTree(nodeInfo.value.id)
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      await getTreeInfo()
+      break
+  }
+
+  menuVisible.value = false
+  // nextTick(() => {
+  //   treeRef.value?.setCurrentKey(targetId)
+  //   treeRef.value?.getNode(targetId)?.$el?.scrollIntoView({
+  //     behavior: 'smooth',
+  //     block: 'nearest'
+  //   })
+  // })
+}
+
+const submitForm = async () => {
+  // 表单验证逻辑
+  formLoading.value = true
+  try {
+    const targetParentId = formType.value === 'create' ? parentId.value : formData.value.parentId
+    formData.value.type = 'file'
+    if (formType.value === 'create') {
+      formData.value.parentId = parentId.value
+      await IotTreeApi.createIotTree(formData.value)
+    } else {
+      await IotTreeApi.updateIotTree(formData.value)
+    }
+    message.success(t(formType.value === 'create' ? '添加成功' : '修改成功'))
+    dialogVisible.value = false
+    menuVisible.value = false
+    await getTreeInfo()
+    if (targetParentId) {
+      expandedKeys.value = [...new Set([...expandedKeys.value, targetParentId])]
+      locateNode(targetParentId)
+    }
+  } catch (error) {
+    console.error(error)
+  } finally {
+    formLoading.value = false
+  }
+}
+
 interface TreeNode extends IotTreeVO {
   children?: TreeNode[]
 }
@@ -43,6 +174,22 @@ const getNodeIconClass = (type: string) => {
   return type === 'device' ? 'node-icon node-icon--device' : 'node-icon node-icon--folder'
 }
 
+const filterDeviceNodes = (nodes: TreeNode[]): TreeNode[] => {
+  return nodes.reduce<TreeNode[]>((result, node) => {
+    if (node.type === 'device') {
+      return result
+    }
+
+    const nextNode: TreeNode = { ...node }
+    if (node.children?.length) {
+      nextNode.children = filterDeviceNodes(node.children)
+    }
+
+    result.push(nextNode)
+    return result
+  }, [])
+}
+
 const findNodePath = (
   nodes: TreeNode[],
   targetId: number | string,
@@ -82,8 +229,9 @@ const locateNode = (targetId: number | string) => {
 }
 
 const getTreeInfo = async () => {
+  // const res = await DataLibraryApi.getFileTree()
   const res = await IotTreeApi.getSimpleTreeList()
-  treeList.value = handleTree(res) as TreeNode[]
+  treeList.value = filterDeviceNodes(handleTree(res) as TreeNode[])
   expandedKeys.value = treeList.value.map((node) => node.id)
 
   if (props.currentId) {
@@ -114,7 +262,24 @@ watch(
   }
 )
 
+const handleClickOutside = (event) => {
+  if (menuVisible.value && contextMenuRef.value && !contextMenuRef.value.contains(event.target)) {
+    menuVisible.value = false
+  }
+}
+
+watch(menuVisible, (visible) => {
+  if (visible) {
+    document.addEventListener('click', handleClickOutside)
+  } else {
+    document.removeEventListener('click', handleClickOutside)
+  }
+})
+
 onMounted(getTreeInfo)
+onBeforeUnmount(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
 </script>
 
 <template>
@@ -144,6 +309,7 @@ onMounted(getTreeInfo)
                 :props="defaultProps"
                 highlight-current
                 node-key="id"
+                @node-contextmenu="handleRightClick"
                 @node-click="handleNodeClick">
                 <template #default="{ node }">
                   <div class="tree-node">
@@ -154,12 +320,48 @@ onMounted(getTreeInfo)
                   </div>
                 </template>
               </el-tree>
+
+              <div
+                v-show="menuVisible"
+                ref="contextMenuRef"
+                class="custom-menu"
+                :style="{ left: menuX + 'px', top: menuY + 'px' }">
+                <ul>
+                  <li @click="handleMenuClick('add')">新建目录</li>
+
+                  <li @click="handleMenuClick('edit')">编辑</li>
+
+                  <li @click="handleMenuClick('delete')">删除目录</li>
+                </ul>
+              </div>
             </el-scrollbar>
           </template>
         </el-auto-resizer>
       </div>
     </div>
 
+    <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        :rules="formRules"
+        label-width="80px">
+        <div class="flex flex-col gap-4">
+          <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>
+        </div>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </template>
+    </Dialog>
+
     <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
       <el-icon size="12">
         <CaretLeft v-if="!isCollapsed" />
@@ -233,6 +435,41 @@ onMounted(getTreeInfo)
   white-space: nowrap;
 }
 
+.custom-menu {
+  position: fixed;
+  min-width: 144px;
+  padding: 6px;
+  background: #fff;
+  border: 1px solid rgb(15 23 42 / 8%);
+  border-radius: 10px;
+  box-shadow: 0 14px 30px rgb(15 23 42 / 16%);
+  z-index: 3000;
+}
+
+.custom-menu ul {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.custom-menu li {
+  padding: 8px 12px;
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.2;
+  color: #334155;
+  cursor: pointer;
+  border-radius: 8px;
+  transition:
+    background-color 0.18s ease,
+    color 0.18s ease;
+}
+
+.custom-menu li:hover {
+  color: #1677ff;
+  background: #eff6ff;
+}
+
 .collapse-handle {
   position: absolute;
   top: 50%;

+ 33 - 17
src/views/pms/qhse/data/index.vue

@@ -19,7 +19,7 @@
           <el-input
             v-model="queryParams.filename"
             :placeholder="t('file.nameHolder')"
-            size="default"
+            size="small"
             clearable
             @keyup.enter="handleQuery"
             class="!w-240px" />
@@ -40,19 +40,17 @@
         </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>
+        <div class="flex">
+          <el-button @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />{{ t('file.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />{{ t('file.reset') }}
+          </el-button>
+          <el-button type="primary" :loading="uploadLoading" @click="openForm('create')">
+            <Icon icon="ep:plus" class="mr-5px" />{{ t('file.upload') }}
+          </el-button>
+        </div>
       </el-form-item>
     </el-form>
 
@@ -149,11 +147,11 @@
         </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
+        <!-- <zm-table-column
           :label="t('file.device')"
           align="center"
           prop="deviceCode"
-          min-width="220" />
+          min-width="220" /> -->
         <zm-table-column
           :label="t('file.operation')"
           align="center"
@@ -190,7 +188,9 @@
     @success="getList"
     :deviceId="deviceId"
     :nodeId="nodeId"
-    :classId="clickNodeId" />
+    :deptId="deptId"
+    :nodeName="nodeName"
+    :classId="nodeId" />
 </template>
 <script lang="ts" setup>
 import IotInfoFormTree from './IotInfoFormTree.vue'
@@ -244,9 +244,15 @@ interface IotInfoFormTreeExpose {
 }
 
 const uploadLoading = ref(false)
+const currentTreeOriginId = ref<TreeKey | undefined>(undefined)
+
+const syncSelectedTreeOriginId = () => {
+  queryParams.deptId = currentTreeOriginId.value
+}
 
 const searchFolderAndFile = async () => {
   formLoading.value = true
+  syncSelectedTreeOriginId()
   const data = await DataLibraryApi.DataLibraryApi.getAllChildContentFile(queryParams)
   list.value = data
   formLoading.value = false
@@ -339,6 +345,8 @@ const openForm = async (type: string, id?: number) => {
 const deviceId = ref('')
 const clickNodeId = ref('')
 const nodeId = ref('')
+const deptId = ref('')
+const nodeName = ref('')
 
 const breadcrumbs = ref<BreadcrumbItem[]>([
   { id: null, name: '科瑞石油技术', type: 'root' } // 根节点
@@ -359,6 +367,7 @@ const updateBreadcrumbs = async (node: FileRow) => {
 
   // 更新表格数据
   queryParams.classId = node.id
+  syncSelectedTreeOriginId()
   const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
 }
@@ -381,6 +390,7 @@ const inContent = async (row: FileRow) => {
   // 可以添加其他表格行点击需要的逻辑
   queryParams.classId = row.id
   nodeId.value = String(row.id)
+  syncSelectedTreeOriginId()
   const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   formLoading.value = false
   list.value = data
@@ -396,6 +406,9 @@ const handleFileNodeClick = async (row: FileRow) => {
   })
 
   queryParams.classId = row.id
+  currentTreeOriginId.value = row.originId
+  deptId.value = row.originId != null ? String(row.originId) : ''
+  nodeName.value = row.name || row.filename || ''
   if (row.type == 'device') {
     id.value = Number(row.originId)
   }
@@ -447,6 +460,8 @@ const handleBreadcrumbClick = async (index: number) => {
   queryParams.classId = targetId
   clickNodeId.value = String(targetId)
   nodeId.value = String(targetId)
+  nodeName.value = targetBreadcrumbs[index].name || ''
+  syncSelectedTreeOriginId()
   const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
   list.value = data
   formLoading.value = false
@@ -456,6 +471,7 @@ const handleBreadcrumbClick = async (index: number) => {
 const getList = async () => {
   formLoading.value = true
   try {
+    syncSelectedTreeOriginId()
     const data = await DataLibraryApi.DataLibraryApi.getChildContentFile(queryParams)
     list.value = data
   } finally {