|
@@ -0,0 +1,411 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <el-input v-model="deptName" class="mb-20px" clearable placeholder="请输入BOM名称">
|
|
|
+ <template #prefix>
|
|
|
+ <Icon icon="ep:search" />
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ <div ref="treeContainer" class="tree-container">
|
|
|
+ <el-tree
|
|
|
+ ref="treeRef"
|
|
|
+ :data="bomList"
|
|
|
+ :expand-on-click-node="false"
|
|
|
+ :filter-node-method="filterNode"
|
|
|
+ :props="defaultProps"
|
|
|
+ default-expand-all
|
|
|
+ highlight-current
|
|
|
+ node-key="id"
|
|
|
+ @node-click="handleNodeClick"
|
|
|
+ @node-contextmenu="handleRightClick"
|
|
|
+ style="height: 33em"
|
|
|
+ draggable
|
|
|
+ :allow-drop="allowDrop"
|
|
|
+ @node-drop="handleNodeDrop"
|
|
|
+ @node-drag-start="handleDragStart"
|
|
|
+ @node-drag-over="handleDragOver"
|
|
|
+ @node-drag-end="handleDragEnd"
|
|
|
+
|
|
|
+ >
|
|
|
+ <template #default="{ node}">
|
|
|
+ <div style="display: flex; justify-content: space-between;align-items: center;width: 100%">
|
|
|
+ <div>
|
|
|
+ <el-icon style="vertical-align: middle"><Folder /></el-icon> <!-- 文件夹图标 -->
|
|
|
+ <span style="vertical-align: middle;margin-left: 3px">{{ node.data.name }}</span>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <icon style="vertical-align: middle" @click="handleRightClick" icon="ep:edit" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ <div v-if="dragTip.visible" class="drag-tip" :style="dragTip.style">
|
|
|
+ 移动到 {{ dragTip.text }}
|
|
|
+ </div>
|
|
|
+ </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>
|
|
|
+ <li @click="handleMenuClick('copy')">复制</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, FormRules } from 'element-plus'
|
|
|
+import { IotInfoApi, IotInfoVO } from '@/api/pms/iotinfo'
|
|
|
+import * as InfoClassifyApi from '@/api/pms/info'
|
|
|
+import { defaultProps, handleTree } from '@/utils/tree'
|
|
|
+import { CommonStatusEnum } from '@/utils/constants'
|
|
|
+import {IotInfoClassifyApi} from "@/api/pms/info";
|
|
|
+import { Document, Folder, Search } from '@element-plus/icons-vue'
|
|
|
+import * as BomApi from '@/api/pms/bom'
|
|
|
+import {IotDeviceApi} from "@/api/pms/device";
|
|
|
+
|
|
|
+const { t } = useI18n() // 国际化
|
|
|
+const message = useMessage() // 消息弹窗
|
|
|
+const dialogVisible = ref(false) // 弹窗的是否展示
|
|
|
+const dialogTitle = ref('') // 弹窗的标题
|
|
|
+const formRef = ref() // 搜索的表单
|
|
|
+const openForm = (type: string, id?: number) => {
|
|
|
+ formRef.value.open(type, id)
|
|
|
+}
|
|
|
+const handleDragStart = (node) => {
|
|
|
+ highlightedNode.value = null;
|
|
|
+ dragTip.value.visible = true;
|
|
|
+};
|
|
|
+const highlightedNode = ref(null);
|
|
|
+const handleDragOver = (dragNode, targetNode, event) => {
|
|
|
+ // 高亮目标节点
|
|
|
+ highlightedNode.value = targetNode;
|
|
|
+
|
|
|
+ // 更新提示位置和内容
|
|
|
+ dragTip.value.text = targetNode.label;
|
|
|
+ dragTip.value.style = {
|
|
|
+ left: `${event.clientX + 15}px`,
|
|
|
+ top: `${event.clientY}px`
|
|
|
+ };
|
|
|
+};
|
|
|
+const handleDragEnd = () => {
|
|
|
+ highlightedNode.value = null;
|
|
|
+ dragTip.value.visible = false;
|
|
|
+};
|
|
|
+const dragTip = ref({
|
|
|
+ visible: false,
|
|
|
+ text: '',
|
|
|
+ style: {}
|
|
|
+});
|
|
|
+const handleNodeDrop = async (draggingNode, dropNode, dropType, event) => {
|
|
|
+ // 获取拖拽后的新父节点ID和目标位置
|
|
|
+ const newParentId = dropNode.data.id;
|
|
|
+ const targetIndex = dropNode.parent.childNodes.findIndex(n => n.data.id === dropNode.data.id);
|
|
|
+ // 更新本地数据
|
|
|
+ draggingNode.data.parentId = newParentId;
|
|
|
+ debugger
|
|
|
+ console.log(JSON.stringify(draggingNode.data))
|
|
|
+ await InfoClassifyApi.IotInfoClassifyApi.removeIotInfoClassify(draggingNode.data)
|
|
|
+};
|
|
|
+const allowDrop = (draggingNode, dropNode, type) => {
|
|
|
+ // 禁止拖拽到非文件夹节点内部
|
|
|
+ //if (dropNode.data.type === 'dept') return false;
|
|
|
+
|
|
|
+ // 限制最大层级(如不超过3层)
|
|
|
+ return true
|
|
|
+};
|
|
|
+// 动态高度计算
|
|
|
+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.50}px`; // 60px 底部预留
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await getTree()
|
|
|
+ setHeight();
|
|
|
+ if (bomList.value.length > 0) {
|
|
|
+ debugger
|
|
|
+ emits('success', bomList.value[0].id)
|
|
|
+ }
|
|
|
+ window.addEventListener('resize', setHeight);
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', setHeight);
|
|
|
+});
|
|
|
+
|
|
|
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|
|
+const formData = ref({
|
|
|
+ id: undefined,
|
|
|
+ parentId: undefined,
|
|
|
+ deviceId: undefined,
|
|
|
+ name: undefined,
|
|
|
+ sort: undefined,
|
|
|
+ status: CommonStatusEnum.ENABLE,
|
|
|
+ remark: undefined
|
|
|
+})
|
|
|
+const formRules = reactive<FormRules>({
|
|
|
+ name: [{ required: true, message: '资料分类名称不能为空', trigger: 'blur' }],
|
|
|
+ sort: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
|
|
|
+ status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
|
|
+})
|
|
|
+const submitForm = async () => {
|
|
|
+ // 校验表单
|
|
|
+ if (!formRef) return
|
|
|
+ const valid = await formRef.value.validate()
|
|
|
+ if (!valid) return
|
|
|
+ // 提交请求
|
|
|
+ formLoading.value = true
|
|
|
+ try {
|
|
|
+ const data = formData.value as unknown as InfoClassifyApi.IotInfoClassifyVO
|
|
|
+ debugger
|
|
|
+ if (formType.value === 'create') {
|
|
|
+ data.parentId = nodeInfo.value.id
|
|
|
+ data.deviceId = props.deviceId
|
|
|
+ await InfoClassifyApi.IotInfoClassifyApi.createIotInfoClassify(data)
|
|
|
+ message.success(t('common.createSuccess'))
|
|
|
+ } else {
|
|
|
+ await InfoClassifyApi.IotInfoClassifyApi.updateIotInfoClassify(data)
|
|
|
+ message.success(t('common.updateSuccess'))
|
|
|
+ }
|
|
|
+ dialogVisible.value = false
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ await getTree()
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 重置表单 */
|
|
|
+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: 'BomTree' })
|
|
|
+const props = defineProps({
|
|
|
+ deviceId: { type: Number, required: true }
|
|
|
+})
|
|
|
+const deptName = ref('')
|
|
|
+const nodeInfo = ref({})
|
|
|
+const bomList = ref<Tree[]>([]) // 树形结构
|
|
|
+const treeRef = ref<InstanceType<typeof ElTree>>()
|
|
|
+const menuVisible = ref(false)
|
|
|
+const menuX = ref(0)
|
|
|
+const menuY = ref(0)
|
|
|
+const contextMenuRef = ref(null) // 弹窗DOM引用
|
|
|
+let selectedNode = null
|
|
|
+const handleRightClick = (event, node, data) => {
|
|
|
+ nodeInfo.value = node;
|
|
|
+ console.log(JSON.stringify(nodeInfo.value))
|
|
|
+ event.preventDefault()
|
|
|
+ menuX.value = event.clientX
|
|
|
+ menuY.value = event.clientY
|
|
|
+ selectedNode = data
|
|
|
+ // menuVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+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
|
|
|
+ console.log(JSON.stringify(formData.value))
|
|
|
+ break
|
|
|
+ case 'delete':
|
|
|
+ // 删除的二次确认
|
|
|
+ await message.delConfirm()
|
|
|
+ // 发起删除
|
|
|
+ await IotInfoClassifyApi.deleteIotInfoClassify(nodeInfo.value.id)
|
|
|
+ message.success(t('common.delSuccess'))
|
|
|
+ // 刷新列表
|
|
|
+ await getTree()
|
|
|
+ break
|
|
|
+ }
|
|
|
+ menuVisible.value = false
|
|
|
+}
|
|
|
+const queryParams = reactive({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ name: undefined,
|
|
|
+ status: undefined,
|
|
|
+ deviceCategoryId: undefined,
|
|
|
+})
|
|
|
+/** 获得部门树 */
|
|
|
+const getTree = async () => {
|
|
|
+ IotDeviceApi.getIotDevice(props.deviceId).then((re) => {
|
|
|
+ queryParams.deviceCategoryId = re.assetClass
|
|
|
+ BomApi.getBomPage(queryParams).then((res) => {
|
|
|
+ console.log(JSON.stringify(res))
|
|
|
+ debugger
|
|
|
+ // bomList.value = []
|
|
|
+ // bomList.value.push(...handleTree(res))
|
|
|
+ bomList.value = handleTree(res)
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/** 基于名字过滤 */
|
|
|
+const filterNode = (name: string, data: Tree) => {
|
|
|
+ if (!name) return true
|
|
|
+ return data.name.includes(name)
|
|
|
+}
|
|
|
+
|
|
|
+/** 处理部门被点击 */
|
|
|
+const handleNodeClick = async (row: { [key: string]: any }) => {
|
|
|
+ console.log(row)
|
|
|
+ emits('node-click', row)
|
|
|
+}
|
|
|
+const emits = defineEmits(['node-click','success'])
|
|
|
+
|
|
|
+/** 监听deptName */
|
|
|
+watch(deptName, (val) => {
|
|
|
+ treeRef.value!.filter(val)
|
|
|
+})
|
|
|
+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)
|
|
|
+ }
|
|
|
+})
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ document.removeEventListener('click', handleClickOutside)
|
|
|
+})
|
|
|
+/** 初始化 */
|
|
|
+// onMounted(async () => {
|
|
|
+// await getTree()
|
|
|
+// })
|
|
|
+</script>
|
|
|
+<style lang="scss" scoped>
|
|
|
+.custom-menu {
|
|
|
+ position: fixed;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+.custom-menu ul {
|
|
|
+ list-style: none;
|
|
|
+ padding: 0;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+.custom-menu li {
|
|
|
+ padding: 8px 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ margin: 5px
|
|
|
+}
|
|
|
+.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;
|
|
|
+}
|
|
|
+.highlight {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+ color: #409eff;
|
|
|
+ padding: 2px 5px;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 拖拽提示框样式 */
|
|
|
+.drag-tip {
|
|
|
+ position: fixed;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #409eff;
|
|
|
+ padding: 5px 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ z-index: 9999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 调整拖拽指示线样式 */
|
|
|
+:deep(.el-tree-node__content) .el-tree-node__expand-icon {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-tree-node__drop-indicator) {
|
|
|
+ background-color: #409eff;
|
|
|
+ height: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 禁止拖拽到内部时的缩进 */
|
|
|
+:deep(.is-drop-inner) .el-tree-node__drop-indicator {
|
|
|
+ background-color: #409eff;
|
|
|
+ height: 100%;
|
|
|
+ width: 4px;
|
|
|
+ left: -8px !important;
|
|
|
+}
|
|
|
+</style>
|