| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- <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:orange"
- v-if="node.data.type === 'device'"
- icon="fa:folder-open"
- />
- <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 { 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
- }
- 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' }]
- })
- const openForm = (type: string, id?: number) => {
- formRef.value.open(type, id)
- }
- /** 重置表单 */
- 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()
- }
- defineOptions({ name: 'IotTree' })
- // 接收外部参数
- const props = defineProps({
- deviceId: { type: Number, required: true },
- currentId: { type: [Number, String], default: null } // 新增:接收需要定位的节点ID
- })
- const deptName = ref('')
- const nodeInfo = ref({})
- const treeList = ref<Tree[]>([]) // 树形结构
- 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 findNodePath = (nodes: Tree[], targetId: number | string, path: (number | string)[] = []): (number | string)[] | null => {
- for (const node of nodes) {
- path.push(node.id)
- if (node.id === targetId) {
- return [...path]
- }
- if (node.children && node.children.length) {
- const result = findNodePath(node.children, targetId, path)
- 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
- }
- }
- const handleDeviceClick = async () => {
- const id = nodeInfo.value.originId
- push({ name: 'DeviceDetailInfo', params: { id } })
- deviceVisible.value = false
- menuVisible.value = false
- }
- 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
- }
- /** 获得部门树 */
- const getTreeInfo = async () => {
- const res = await IotTreeApi.getSimpleTreeList()
- treeList.value = []
- treeList.value.push(...handleTree(res))
- // 处理展开逻辑:有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 handleNodeClick = async (row: { [key: string]: any }) => {
- deviceVisible.value = false
- menuVisible.value = false
- 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)
- })
- /** 监听currentId变化,重新定位 */
- watch(
- () => props.currentId,
- (newVal) => {
- if (newVal && treeList.value.length) {
- locateNode(newVal)
- }
- },
- { immediate: true }
- )
- </script>
- <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;
- }
- .custom-menu ul {
- list-style: none;
- padding: 0;
- margin: 0;
- }
- .custom-menu li {
- padding: 2px 10px;
- cursor: pointer;
- font-size: 12px;
- margin: 2px;
- }
- .custom-menu li:hover {
- background: #77a0ec;
- }
- .tree-container {
- overflow-y: auto;
- min-width: 100%;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- }
- /* 自定义滚动条 */
- .tree-container::-webkit-scrollbar {
- width: 6px;
- }
- .tree-container::-webkit-scrollbar-thumb {
- background: #c0c4cc;
- border-radius: 3px;
- }
- /* 自定义高亮样式 */
- ::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;
- }
- </style>
|