| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- <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 { DataLibraryApi } from '@/api/pms/qhse/index'
- import { defaultProps, handleTree } from '@/utils/tree'
- import { CommonStatusEnum } from '@/utils/constants'
- defineOptions({ name: 'IotTree' })
- 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 () => {
- // 表单验证逻辑
- 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 {
- }
- }
- interface TreeNode extends IotTreeVO {
- children?: TreeNode[]
- }
- type TreeKey = number | string
- 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 isCollapsed = ref(false)
- const treeName = ref('')
- const treeList = ref<TreeNode[]>([])
- const treeRef = ref<InstanceType<typeof ElTree>>()
- 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 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,
- path: Array<number | string> = []
- ): Array<number | string> | null => {
- for (const node of nodes) {
- const nextPath = [...path, node.id]
- if (node.id === targetId) {
- return nextPath
- }
- if (node.children?.length) {
- const result = findNodePath(node.children, targetId, nextPath)
- if (result) return result
- }
- }
- return null
- }
- const locateNode = (targetId: number | string) => {
- if (!targetId || !treeList.value.length) return
- const pathIds = findNodePath(treeList.value, targetId)
- if (!pathIds) return
- expandedKeys.value = [...new Set([...expandedKeys.value, ...pathIds.slice(0, -1)])]
- nextTick(() => {
- treeRef.value?.setCurrentKey(targetId)
- treeRef.value?.getNode(targetId)?.$el?.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest'
- })
- })
- }
- const getTreeInfo = async () => {
- // const res = await DataLibraryApi.getFileTree()
- const res = await IotTreeApi.getSimpleTreeList()
- treeList.value = filterDeviceNodes(handleTree(res) as TreeNode[])
- expandedKeys.value = treeList.value.map((node) => node.id)
- if (props.currentId) {
- locateNode(props.currentId)
- }
- emits('success', treeList.value[0]?.id)
- }
- const filterNode = (name: string, data: TreeNode) => {
- return !name || data.name.includes(name)
- }
- const handleNodeClick = (row: TreeNode) => {
- emits('node-click', row)
- }
- watch(treeName, (val) => {
- treeRef.value?.filter(val)
- })
- watch(
- () => props.currentId,
- (newVal) => {
- if (newVal) {
- locateNode(newVal)
- }
- }
- )
- 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>
- <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-contextmenu="handleRightClick"
- @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>
- <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" :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" />
- <CaretRight v-else />
- </el-icon>
- </div>
- </div>
- </template>
- <style lang="scss" scoped>
- .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;
- }
- .tree-node__label {
- overflow: hidden;
- text-overflow: ellipsis;
- 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%;
- right: -14px;
- z-index: 200;
- display: flex;
- width: 14px;
- height: 60px;
- color: var(--el-text-color-secondary);
- pointer-events: auto;
- cursor: pointer;
- 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;
- }
- .is-collapsed .collapse-handle {
- right: -8px;
- border-left: 1px solid var(--el-border-color-light);
- }
- .collapse-handle:hover {
- color: var(--el-color-primary);
- background-color: var(--el-fill-color-light);
- }
- :deep(.el-tree) {
- --el-tree-node-hover-bg-color: var(--el-fill-color-light);
- background: transparent;
- }
- :deep(.el-tree-node__content) {
- height: 32px;
- border-radius: 4px;
- }
- :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>
|