Lijy 2 月之前
父節點
當前提交
74cfc9095c

+ 209 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/package-lock.json

@@ -9,6 +9,8 @@
       "version": "0.0.0",
       "dependencies": {
         "@element-plus/icons-vue": "^2.3.1",
+        "@vue-flow/background": "^1.3.2",
+        "@vue-flow/core": "^1.42.2",
         "@zmjs/form-design": "file:../plugIn/form-design-master/update",
         "animate.css": "^4.1.1",
         "axios": "^1.6.7",
@@ -920,6 +922,117 @@
         "path-browserify": "^1.0.1"
       }
     },
+    "node_modules/@vue-flow/background": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/@vue-flow/background/-/background-1.3.2.tgz",
+      "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==",
+      "peerDependencies": {
+        "@vue-flow/core": "^1.23.0",
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/@vue-flow/core": {
+      "version": "1.42.2",
+      "resolved": "https://registry.npmmirror.com/@vue-flow/core/-/core-1.42.2.tgz",
+      "integrity": "sha512-l2hwxYRDT+iJIOy52JkxPFWWiA0DwUo9XPK2qa8+0TmHx6xy4L11PWjB/wHJYvvrz+wONs0Fob9QPVJFDLiE6Q==",
+      "dependencies": {
+        "@vueuse/core": "^10.5.0",
+        "d3-drag": "^3.0.0",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
+    },
+    "node_modules/@vue-flow/core/node_modules/@vueuse/core": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz",
+      "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.11.1",
+        "@vueuse/shared": "10.11.1",
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vue-flow/core/node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue-flow/core/node_modules/@vueuse/metadata": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz",
+      "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vue-flow/core/node_modules/@vueuse/shared": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz",
+      "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+      "dependencies": {
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vue-flow/core/node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@vue/compiler-core": {
       "version": "3.4.26",
       "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.26.tgz",
@@ -1435,6 +1548,102 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
     },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/dayjs": {
       "version": "1.11.11",
       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",

+ 2 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/package.json

@@ -12,6 +12,8 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "@vue-flow/background": "^1.3.2",
+    "@vue-flow/core": "^1.42.2",
     "@zmjs/form-design": "file:../plugIn/form-design-master/update",
     "animate.css": "^4.1.1",
     "axios": "^1.6.7",

二進制
fhKeeper/formulahousekeeper/customerBuler-crm/src/assets/expand.png


+ 288 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/flowChart.vue

@@ -0,0 +1,288 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { VueFlow, Handle, Position, type Node, Edge } from '@vue-flow/core'
+import { Background } from '@vue-flow/background'
+import { post, get } from "@/utils/request";
+import { GET_STRUCT_BY_TABLE_NAME, GET_ALL_BUS_TABLE, GET_RELATE_TABLE_BY_FROM_COLUMN, GET_RELATE_BUS_TABLE_BY_FROM_TABLE } from "../api"
+
+import '@vue-flow/core/dist/style.css';
+import '@vue-flow/core/dist/theme-default.css';
+
+interface selectedNode {
+  id: string;
+  data: any
+}
+const FixedConfiguration = {
+  type: 'custom',
+  style: {
+    width: '120px', padding: '5px 20px', fontSize: '14px',
+    border: '1px solid #dcdfe6', borderRadius: '8px'
+  },
+  sourcePosition: Position.Right,
+  targetPosition: Position.Left,
+  hidden: false
+}
+const fixedLabel = {
+  selected: false, // 是否选中
+}
+const connectionType = 'step'
+const nodes = ref<Node[]>([])
+const edges = ref<Edge[]>([])
+const clickedNodes = ref<selectedNode[]>([]) // 记录点击的节点
+const selectNodes = ref<any[]>([]) // 记录选中的节点
+
+const businessTableList = ref<optionType[]>([])
+const businessObject = ref<any>({})
+
+async function onNodeClick(event: any) {
+  const { x, y } = event.node.position // 点击的位置
+  const nodeId = event.node.id
+
+  // 点击的节点Id
+  const clickedNodesStr = clickedNodes.value.map(node => node.id)
+  // 选中的节点Id
+  const selectNodeIdList = selectNodes.value.map(node => node.id)
+  // 点击长度相同的节点Id
+  const clickedNodesLengthStr = clickedNodesStr.filter(strId => strId.length == nodeId.length)
+
+  if (clickedNodesStr.includes(nodeId)) {
+    if (selectNodeIdList.includes(nodeId)) {
+      for (let i in selectNodeIdList) {
+        if (nodeId !== selectNodeIdList[i] && nodeId.length === selectNodeIdList[i].length) {
+          expandNode(selectNodeIdList[i], true)
+        } else if (nodeId == selectNodeIdList[i]) {
+          expandNode(nodeId, false)
+        }
+      }
+
+      const notSelectedToDelete = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item));
+      for (let i in notSelectedToDelete) {
+        deteleData(notSelectedToDelete[i])
+      }
+    }
+  } else {
+    clickedNodes.value.push({ id: nodeId, data: event.node.data })
+    clickedNodesStr.push(nodeId)
+
+    // 隐藏和删除其他节点
+    const hiddenNode = clickedNodesLengthStr.filter(item => selectNodeIdList.includes(item))
+    const deleteNode = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item))
+
+    hiddenNode.forEach(item => expandNode(item, true))
+    deleteNode.forEach(item => deteleData(item))
+
+    const eventData = event.node.data
+    // console.log(eventData.id, '<==== id')
+    // if(eventData.id) {
+    const addData = await post(GET_RELATE_BUS_TABLE_BY_FROM_TABLE, { tableName: eventData.tblName })
+    const fieldList = addData.data || []
+    AddData(fieldList, x, y, nodeId)
+    // }
+    
+    // if(!eventData.id) {
+    //   const tableName = eventData.tblName
+    //   const columnName = eventData.columnName
+    //   const addData = await post(GET_RELATE_TABLE_BY_FROM_COLUMN, { tableName, columnName })
+    //   console.log(addData, '<===== 返回的数据')
+    // }
+  }
+}
+
+// 处理级之间的联动勾选
+function processSelectedNodes(nodeId: string) {
+  const item = nodes.value.find(item => item.id === nodeId)
+  if (item?.data?.selected) { // 选中状态将之前的节点选中
+    const selectNodes = item.id.split('-').map((_: any, index: number, arr: any) => arr.slice(0, index + 1).join('-'));
+    nodes.value = nodes.value.map(node => {
+      if (selectNodes.includes(node.id)) {
+        return {
+          ...node,
+          data: { ...node.data, selected: true }
+        }
+      } else {
+        return node
+      }
+    })
+  }
+
+  if(!item?.data?.selected) {
+    const cancelSelectNodes = nodes.value.filter(node => (node.id + '').indexOf((item?.id + '')) !== -1).map(node => node.id)
+    nodes.value = nodes.value.map(node => {
+      if (cancelSelectNodes.includes(node.id)) {
+        return {
+          ...node,
+          data: { ...node.data, selected: false }
+        }
+      } else {
+        return node
+      }
+    })
+  }
+  switchColors()
+  changeEdgeStyle()
+}
+
+// 更改连线样式
+function changeEdgeStyle() {
+  const selectNodeIdList = selectNodes.value.map(node => node.id)
+  edges.value = edges.value.map(edge => {
+    const { source, target } = edge
+    const isSelected = selectNodeIdList.includes(source) && selectNodeIdList.includes(target)
+    return {
+      ...edge,
+      style: {
+        ...edge.style,
+        strokeWidth: isSelected ? 4 : 1,
+        stroke: isSelected ? '#01517f' : '',
+        class: isSelected ? 'high-z-index' : ''
+      },
+    }
+  })
+}
+
+// 设置选中背景色
+function switchColors() {
+  selectNodes.value = nodes.value.filter(node => node.data.selected)
+  nodes.value = nodes.value.map(node => ({
+    ...node,
+    style: {
+      ...node.style,
+      backgroundColor: node.data.selected ? '#01517f' : '#fff',
+    },
+  }))
+}
+
+// 添加节点和连线数据
+function AddData(list: any[], x: number, y: number, nodeId: string) {
+  const newNodes: Node[] = []
+  const newEdges = [...edges.value]
+
+  const spacing = 80
+  const startY = y - (list.length * spacing) / 2
+
+  list.forEach((item, index) => {
+    const newNodeId = `${nodeId}-${index + 1}` // 生成唯一 ID
+    const newY = startY + index * spacing
+
+    newNodes.push({
+      id: newNodeId,
+      data: { label: item.fromColBusName, tblName: item.toTbl, ...item, ...fixedLabel, },
+      position: { x: x + 240, y: newY },
+      ...FixedConfiguration,
+    })
+
+    newEdges.push({
+      id: `e${nodeId}-${newNodeId}`,
+      source: nodeId,
+      target: newNodeId,
+      type: connectionType
+    })
+  })
+
+  nodes.value = [...nodes.value, ...newNodes]
+  edges.value = newEdges
+}
+
+// 删除节点和连线数据
+function deteleData(nodeId: string) {
+  clickedNodes.value = clickedNodes.value.filter(item => (item.id + '').indexOf(nodeId) === -1)
+  nodes.value = nodes.value.filter(node => (node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId)))
+  edges.value = edges.value.filter(edge => edge.source !== nodeId && edge.source.indexOf(nodeId) === -1)
+}
+
+// 展开收起节点
+function expandNode(nodeId: string, val: boolean) {
+  const filterNodes = nodes.value.filter(node => !(node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId))).map(item => item.id)
+  nodes.value = nodes.value.map(node => {
+    if (filterNodes.includes(node.id)) {
+      return {
+        ...node,
+        hidden: val
+      }
+    } else {
+      return node
+    }
+  })
+}
+
+// 获取所有业务对象
+function getAllBusTable() {
+  post(GET_ALL_BUS_TABLE, {}).then(res => {
+    businessTableList.value = (res.data || []).map((item: any) => {
+      return {
+        label: item.name,
+        value: item.id,
+        ...item
+      }
+    })
+  })
+}
+
+// 切换事件
+function businessObjectSwitching(val: any) {
+  const row = businessTableList.value.find((item: any) => item.id === val)
+  nodes.value = [{ id: '1', data: { ...row, ...fixedLabel }, position: { x: 100, y: 50 },  ...FixedConfiguration }]
+  edges.value = []
+  clickedNodes.value = []
+  selectNodes.value = []
+}
+
+// 新建初始化数据
+function initData(row: any) {
+  businessObject.value = row.id
+  nodes.value = [{ id: '1', data: { ...row, ...fixedLabel }, position: { x: 100, y: 50 },  ...FixedConfiguration }]
+  edges.value = []
+  clickedNodes.value = []
+  selectNodes.value = []
+}
+
+onMounted(() => {
+  getAllBusTable()
+})
+
+// 向外暴露方法
+defineExpose({
+  initData,
+});
+</script>
+
+<template>
+  <div style="width: 100%; height: 100%;" class="relative">
+    <!-- <div class="flex items-center absolute top-0 left-2 z-10">
+      <div class="mr-2">业务对象:</div>
+      <el-select v-model="businessObject" placeholder="请选择" style="width: 200px;" @change="businessObjectSwitching">
+        <el-option v-for="item in businessTableList" :key="item.value" :label="item.label" :value="item.value" />
+      </el-select>
+    </div> -->
+    <VueFlow :nodes="nodes" :edges="edges" @node-click="onNodeClick" fit-view-on-init :edge-updater-layer="true">
+      <template #node-custom="{ data, id }">
+        <div class="flex flex-row">
+          <Handle class="handle left-handle" type="target" :position="Position.Left" :id="`${id}`" />
+          <div class="flex items-center">
+            <el-checkbox v-model="data.selected" size="large" @change="processSelectedNodes(id)" @click.stop
+              class="pr-2 tops"></el-checkbox>
+            <span :class="{ 'text-white': data.selected }">{{ data.label }}</span>
+          </div>
+          <Handle class="handle right-handle" type="source" :position="Position.Right" :id="`${id}`" />
+        </div>
+      </template>
+      <Background />
+    </VueFlow>
+  </div>
+</template>
+
+<style lang='scss' scoped>
+.tops {
+  top: 2px
+}
+
+::deep(.vue-flow__node-custom) {
+  position: relative;
+}
+
+.high-z-index {
+  position: relative;
+  z-index: 9999 !important;
+  /* 提高连线层级 */
+}
+</style>

+ 188 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/index.vue

@@ -0,0 +1,188 @@
+<script lang="ts" setup>
+import { ref, reactive, onMounted, inject, nextTick } from "vue";
+import type { ComponentSize, FormInstance, FormRules } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { post, get } from "@/utils/request";
+import { GET_ALL_STORES_TREE, GET_ALL_BUS_TABLE } from "../api"
+
+import flowChart from './flowChart.vue'
+import selectDeptUser from "./selectDeptUser.vue";
+
+const router = useRouter()
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const formVal = ref({
+  reportFormName: '',
+  privilege: false,
+  storeType: '',
+  description: '',
+  deptAccessList: [],
+  userAccessList: [],
+  BusinessObject: []
+})
+
+const allVisable = reactive({
+  mindMapVoisable: false,
+  treeSelectUserVisable: false
+})
+
+const businessTableList = ref<optionType[]>([])
+const treeSelectData = ref([])
+const selectDeptUserRef = ref<InstanceType<typeof selectDeptUser> | null>(null);
+const flowChartRef = ref<InstanceType<typeof flowChart> | null>()
+const visibleRangeData = ref<any>([])
+
+function openReport(val: boolean) {
+  if(val) {
+    visibleRangeData.value = []
+  }
+}
+function visibleRangeDetermination() {
+  const val = (selectDeptUserRef.value && selectDeptUserRef.value.getSelectData()) || []
+  if (val.length == 0) {
+    globalPopup?.showWarning('请先选择可见范围')
+    return
+  }
+  val.sort((a, b) => {
+    const isUserA = a.isUser === undefined ? -1 : a.isUser;
+    const isUserB = b.isUser === undefined ? -1 : b.isUser;
+    if (isUserA === isUserB) {
+      return 0;
+    }
+    return isUserA < isUserB ? -1 : 1;
+  });
+  visibleRangeData.value = val
+  allVisable.treeSelectUserVisable = false
+}
+
+function visibilityClose(index: number) {
+  visibleRangeData.value.splice(index, 1)
+}
+
+function businessObjectSelectChange(val: any) {
+  const row = businessTableList.value.find((item: any) => item.id === val)
+  allVisable.mindMapVoisable = true
+  nextTick(() => {
+    flowChartRef.value?.initData(row)
+  })
+}
+
+function showTreeDeptUserVis() {
+  allVisable.treeSelectUserVisable = true
+}
+
+function getTreeClassification() {
+  post(GET_ALL_STORES_TREE, {}).then((res) => {
+    treeSelectData.value = res.data || []
+  })
+}
+
+function getAllBusTable() {
+  post(GET_ALL_BUS_TABLE, {}).then(res => {
+    businessTableList.value = (res.data || []).map((item: any) => {
+      return {
+        label: item.name,
+        value: item.id,
+        ...item
+      }
+    })
+  })
+}
+
+function dragAndDropEditing() {
+  router.push('/biReport/dragEdit')
+}
+
+onMounted(() => {
+  getTreeClassification()
+  getAllBusTable()
+})
+</script>
+
+<template>
+  <div class="w-full h-full flex flex-col bg-white rounded-md">
+    <div class="p-5 text-[18px] border-b-2">新建报表</div>
+    <div class="flex-1 py-5 px-16 flex-col flex h-[90%]">
+      <div class="flex-1 h-full overflow-auto mb-8 scroll-bar">
+        <el-form style="max-width: 600px" :model="formVal" label-width="auto">
+          <el-form-item label="名称">
+            <el-input v-model="formVal.reportFormName" placeholder="请输入" />
+          </el-form-item>
+          <el-form-item label="分类">
+            <el-tree-select v-model="formVal.storeType" :data="treeSelectData" check-strictly
+              :render-after-expand="false" show-checkbox style="width: 100%" :props="{
+                label: 'storeName', value: 'id', children: 'childStoreList'
+              }" />
+          </el-form-item>
+          <el-form-item label="描述">
+            <el-input v-model="formVal.description" :rows="2" type="textarea" placeholder="请输入" />
+          </el-form-item>
+          <el-form-item label="可见范围">
+            <div class="flex items-center w-full">
+              <el-input placeholder="+选择可见部门和人员" readonly :disabled="formVal.privilege" @click="showTreeDeptUserVis()" />
+              <el-checkbox v-model="formVal.privilege" label="公开" size="large" class="ml-4" @change="openReport" />
+            </div>
+          </el-form-item>
+          <el-form-item label=" " v-if="visibleRangeData.length">
+            <div class="flex flex-wrap w-full">
+              <template v-for="(item, index) in visibleRangeData">
+                <el-tag :type="`${item.isUser ? 'warning' : 'primary'}`" closable class="mr-2 mb-2"  @close="visibilityClose(index)">
+                  <TextTranslation :translationTypes="`${item.isUser ? 'userName' : 'departmentName'}`" :translationValue="item.label"></TextTranslation>
+                </el-tag>
+              </template>
+            </div>
+          </el-form-item>
+          <el-form-item label="业务对象">
+            <el-select v-model="formVal.BusinessObject" placeholder="请选择" @change="businessObjectSelectChange">
+              <el-option v-for="item in businessTableList" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+
+        <div class="h-[500px] border mr-6" v-if="formVal.BusinessObject">
+          <flowChart ref="flowChartRef"></flowChart>
+        </div>
+      </div>
+      <div class="justify-end flex">
+        <el-button type="primary" @click="dragAndDropEditing()">下一步</el-button>
+      </div>
+    </div>
+
+    <!-- 全屏对话框 -->
+    <!-- <el-dialog v-model="allVisable.mindMapVoisable" fullscreen draggable class="fullScreen" :show-close="false">
+      <div class="w-full h-full relative">
+        <flowChart ref="flowChartRef"></flowChart>
+        <div class="absolute flex justify-end right-2 bottom-2">
+          <el-button @click="allVisable.mindMapVoisable = false">关闭</el-button>
+          <el-button type="primary" @click="allVisable.mindMapVoisable = false">
+            确定
+          </el-button>
+        </div>
+      </div>
+    </el-dialog> -->
+
+    <!-- 选择部门人员 -->
+    <el-dialog v-model="allVisable.treeSelectUserVisable" width="600" :show-close="false" top="10vh">
+      <template #header="{ close, titleId, titleClass }">
+        <div class="flex justify-between items-center border-b pb-3 dialog-header">
+          <h4 :id="titleId">选择可见范围</h4>
+          <div>
+            <el-button type="primary" @click="visibleRangeDetermination()">确定</el-button>
+            <el-button @click="allVisable.treeSelectUserVisable = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="scroll-bar m-6">
+        <selectDeptUser ref="selectDeptUserRef" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang='scss' scoped>
+/* 样式代码 */
+.fullScreen :deep(.el-dialog__body) {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 85 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/selectDeptUser.vue

@@ -0,0 +1,85 @@
+<script setup lang="ts">
+import { provide, onMounted, ref } from 'vue'
+import { ElTree } from 'element-plus'
+import { post } from "@/utils/request";
+
+const departmentData = ref<any[]>([])
+const filterText = ref('')
+const treeRef = ref<InstanceType<typeof ElTree>>()
+
+const defaultProps = {
+  children: 'children',
+  label: 'label',
+  value: 'id'
+}
+
+function getDepartmentData() {
+  post(`/department/listAllMemb`, { keyword: '' }).then(res => {
+    departmentData.value = setUserToDept(res.data || [])
+  })
+}
+
+function setUserToDept(list: any): any[] {
+  let newList = JSON.parse(JSON.stringify(list))
+  for (let item of newList) {
+    if (item.children != null) {
+      item.children = setUserToDept(item.children);
+    }
+
+    if (item.userList != null) {
+      if (item.children == null) {
+        item.children = [];
+      }
+      item.userList.forEach((element: any) => {
+        var obj = { id: element.id, label: element.name, parentId: element.departmentId, isUser: 1 };
+        item.children.push(obj);
+      });
+    }
+  }
+  return newList;
+}
+
+onMounted(() => {
+  getDepartmentData()
+})
+
+function getSelectData() {
+  return treeRef.value!.getCheckedNodes()
+}
+
+// 向外暴露方法
+defineExpose({
+  getSelectData,
+});
+</script>
+
+<template>
+  <div class="w-full h-[60vh] flex flex-col">
+    <!-- <el-input v-model="filterText" placeholder="请输入" class="mb-4" /> -->
+    <div class="flex-1 overflow-auto">
+      <el-tree ref="treeRef" :data="departmentData" :props="defaultProps" :check-on-click-node="true" :expand-on-click-node="false" :highlight-current="true" node-key="id" :show-checkbox="true" :check-strictly="true">
+        <template #default="{ node, data }">
+          <template v-if="node.data.isUser">
+            <div class="flex items-center">
+              <span class="pr-2"><el-icon color="#e6a23c"><Avatar /></el-icon></span>
+              <TextTranslation translationTypes="userName" :translationValue="node.label"></TextTranslation>
+            </div>
+          </template>
+          <template v-if="!node.data.isUser">
+            <div class="flex items-center">
+              <span class="pr-2"><el-icon color="#075985"><Grid /></el-icon></span>
+              <TextTranslation translationTypes="departmentName" :translationValue="node.label"></TextTranslation>
+            </div>
+          </template>
+        </template>
+      </el-tree>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-tree-node) {
+  padding-top: 8px;
+  font-size: 14px;
+}
+</style>

+ 9 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/api.ts

@@ -0,0 +1,9 @@
+export const GET_FORM_STORE_PAGE = `/tableColumn/getFormStorePage` // 获取文件存储页数据
+export const ADD_OR_UPDATE_FORM_STORE = `/tableColumn/addOrUpdateFormStore` // 新增或修改分类文件夹
+export const DELETE_REPORT_FORM = `/tableColumn/deleteReportForm` // 删除报表
+export const DELETE_FORM_STORE = `/tableColumn/deleteFormStore` // 删除文件夹
+export const GET_ALL_STORES_TREE = `/tableColumn/getAllStoresTree` // 获取所有文件夹-树形
+export const GET_ALL_BUS_TABLE = `/tableColumn/getAllBusTable` // 获取所有业务表
+export const GET_STRUCT_BY_TABLE_NAME = `/tableColumn/getStructByTableName` // 查询表字段
+export const GET_RELATE_TABLE_BY_FROM_COLUMN = `/tableColumn/getRelateTableByFromColumn` // 通过来源字段获取关联表
+export const GET_RELATE_BUS_TABLE_BY_FROM_TABLE = `/tableColumn/getRelateBusTableByFromTable` // 根据来源表名获取关联表

+ 270 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/cusReportForm/index.vue

@@ -0,0 +1,270 @@
+<script lang="ts" setup>
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from 'vue-router'
+import { ArrowDown } from "@element-plus/icons-vue";
+import type { FormInstance } from 'element-plus'
+import { ElMessage, ElMessageBox } from "element-plus";
+import { ArrowRight } from '@element-plus/icons-vue'
+import { post } from "@/utils/request";
+import { GET_FORM_STORE_PAGE, ADD_OR_UPDATE_FORM_STORE, DELETE_REPORT_FORM, DELETE_FORM_STORE } from "../api"
+
+const router = useRouter()
+
+const allParentStoreId = ref(0)
+const allParentStoreList = ref<any>([
+  { id: 0, storeName: '全部' }
+]); // 面包屑导航数组
+const tableList = ref([]);
+const classificationFormRef = ref<FormInstance>()
+const classificationForm = ref({
+  storeName: "",
+  description: "",
+  parentStoreId: 0,
+});
+const paging = ref({
+  pageIndex: 1,
+  pageSize: 10,
+});
+const pagingTotal = ref(0);
+const allVisible = reactive({
+  addEditingCategoryVisable: false,
+});
+const allLoading = reactive({
+  addEditingCategoryLoading: false,
+});
+
+function goToTheNextLevel(row: any) {
+  allParentStoreId.value = row.id
+  allParentStoreList.value.push(row)
+  paging.value = {
+    pageIndex: 1,
+    pageSize: 10,
+  }
+  getTableList()
+}
+
+function breadCrumbsClick(item: any, index: number) {
+  allParentStoreId.value = item.id
+  allParentStoreList.value.splice(index + 1, allParentStoreList.value.length - index - 1)
+  paging.value = {
+    pageIndex: 1,
+    pageSize: 10,
+  }
+  getTableList()
+}
+
+function deleteTableItem(row: any) {
+  const { storeType, storeName } = row
+  ElMessageBox.confirm(`确定删除【${storeName}】${['', '文件夹', '报表'][storeType]}`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(() => {
+    const url = ['', DELETE_FORM_STORE, DELETE_REPORT_FORM][storeType]
+    const parameter = storeType == 1 ? row.id : row.relateFormId
+    const parameterName = ['', 'storeId', 'formId'][storeType]
+    post(`${url}`, {
+      [parameterName]: parameter,
+    }).then(() => {
+      ElMessage({
+        type: 'success',
+        message: '删除成功',
+      })
+      getTableList()
+    })
+  })
+}
+
+function rename(row: any) {
+  ElMessageBox.prompt('', '重命名', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    inputPattern: /\S/,
+    inputErrorMessage: '请输入',
+  }).then(({ value }) => {
+    post(ADD_OR_UPDATE_FORM_STORE, {
+      id: row.id,
+      storeType: row.storeType,
+      storeName: value,
+      description: row.description,
+      parentStoreId: allParentStoreId.value
+    }).then(_res => {
+      ElMessage.success('操作成功')
+      getTableList()
+    })
+  })
+}
+
+function saveClassification(formEl: FormInstance | undefined) {
+  if (!formEl) return
+  formEl.validate((valid) => {
+    if (valid) {
+      const { storeName, description, parentStoreId } = classificationForm.value
+      post(ADD_OR_UPDATE_FORM_STORE, {
+        storeType: 1,
+        storeName: storeName,
+        description: description,
+        parentStoreId
+      }).then(_res => {
+        ElMessage.success('操作成功')
+        allVisible.addEditingCategoryVisable = false
+        getTableList()
+      })
+    }
+  })
+}
+
+function addEditingCategory(row: any = {}) {
+  if (Object.keys(row).length) {
+    // 如果有值
+    console.log(row, '<==== 123321')
+  } else {
+    classificationForm.value = {
+      storeName: "",
+      description: "",
+      parentStoreId: 0
+    }
+  }
+  allVisible.addEditingCategoryVisable = true;
+}
+
+function handleSizeChange(val: number) {
+  paging.value = {
+    pageIndex: 1,
+    pageSize: val,
+  };
+}
+
+function handleCurrentChange(val: number) {
+  paging.value.pageIndex = val;
+}
+
+function getTableList() {
+  post(GET_FORM_STORE_PAGE, { ...paging.value, parentStoreId: allParentStoreId.value }).then(res => {
+    tableList.value = res.data.records || []
+  })
+}
+
+function newReport() {
+  router.push('/biReport/addEdit')
+}
+
+onMounted(() => {
+  getTableList()
+})
+</script>
+
+<template>
+  <div class="w-full h-full flex flex-col bg-white rounded-md">
+    <div class="flex justify-between p-5">
+      <div>
+        <el-breadcrumb :separator-icon="ArrowRight">
+          <el-breadcrumb-item v-for="(item, index) in allParentStoreList" :key="index" @click="breadCrumbsClick(item, index)">
+            <div class="cursor-pointer">{{ item.storeName }}</div>
+          </el-breadcrumb-item>
+        </el-breadcrumb>
+      </div>
+      <div>
+        <el-button type="primary" @click="newReport()">新建报表</el-button>
+        <el-button type="primary" @click="addEditingCategory()">添加分类</el-button>
+      </div>
+    </div>
+    <div class="flex-1 px-5">
+      <el-table :data="tableList" border style="width: 100%; height: 100%">
+        <el-table-column prop="storeName" label="名称">
+          <template #default="scope">
+            <div class="flex items-center">
+              <template v-if="scope.row.storeType == 1">
+                <el-icon size="18" color="#e6a23c" class="mr-2"><FolderOpened /></el-icon>
+                <el-link type="warning" :underline="false" @click="goToTheNextLevel(scope.row)">{{ scope.row.storeName }}</el-link>
+              </template>
+              <template v-else>
+                <el-icon size="18" color="#409eff" class="mr-2"><Document /></el-icon>
+                <el-link type="primary" :underline="false">{{ scope.row.storeName }}</el-link>
+              </template>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createName" label="创建人">
+          <template #default="scope">
+            <TextTranslation translationTypes="userName" :translationValue="scope.row.createName"></TextTranslation>
+          </template>
+        </el-table-column>
+        <el-table-column prop="storeName" label="权限" />
+        <el-table-column prop="storeName" label="可见人" />
+        <el-table-column prop="storeName" label="可见部门" />
+        <el-table-column prop="createTime" label="创建时间" width="180" />
+        <el-table-column prop="updateName" label="修改人">
+          <template #default="scope">
+            <TextTranslation translationTypes="userName" :translationValue="scope.row.createName"></TextTranslation>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updateTime" label="修改时间" width="180" />
+        <el-table-column prop="description" label="描述" />
+        <el-table-column prop="storeName" label="操作" fixed="right">
+          <template #default="scope">
+            <el-dropdown>
+              <el-button type="primary">
+                更多操作<el-icon class="el-icon--right"><arrow-down /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item v-if="scope.row.storeType == 2">
+                    <el-text class="mx-1" type="primary">编辑</el-text>
+                  </el-dropdown-item>
+                  <el-dropdown-item>
+                    <el-text class="mx-1" type="primary" @click="rename(scope.row)">重命名</el-text>
+                  </el-dropdown-item>
+                  <el-dropdown-item v-if="scope.row.storeType == 2">
+                    <el-text class="mx-1" type="primary">导出</el-text>
+                  </el-dropdown-item>
+                  <el-dropdown-item v-if="scope.row.storeType == 2">
+                    <el-text class="mx-1" type="primary">移动</el-text>
+                  </el-dropdown-item>
+                  <el-dropdown-item>
+                    <el-text class="mx-1" type="danger" @click="deleteTableItem(scope.row)">删除</el-text>
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="p-5 flex justify-end">
+      <!-- <el-pagination v-model:current-pageIndex="paging.pageIndex" :pageIndex-sizes="[10, 20, 30, 50]" :pageSize="paging.pageSize"
+        layout="total, prev, pager, next, sizes" :total="pagingTotal" @pageSize-change="handleSizeChange"
+        @current-change="handleCurrentChange" /> -->
+    </div>
+
+    <!-- 新建分类 -->
+    <el-dialog v-model="allVisible.addEditingCategoryVisable" width="600" :show-close="false" top="10vh">
+      <template #header="{ close, titleId, titleClass }">
+        <div class="flex justify-between items-center border-b pb-3 dialog-header">
+          <h4 :id="titleId">新建分类</h4>
+          <div>
+            <el-button type="primary" v-loading="allLoading.addEditingCategoryLoading"
+              @click="saveClassification(classificationFormRef)">确定</el-button>
+            <el-button @click="allVisible.addEditingCategoryVisable = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="scroll-bar m-6">
+        <el-form ref="classificationFormRef" style="max-width: 600px" :model="classificationForm" label-width="auto"
+          status-icon>
+          <el-form-item label="分类名称" prop="storeName" :rules="[{ required: true, message: '请填写' }]">
+            <el-input v-model.trim="classificationForm.storeName" placeholder="请输入" />
+          </el-form-item>
+          <el-form-item label="描述">
+            <el-input v-model="classificationForm.description" :autosize="{ minRows: 2, maxRows: 6 }" type="textarea"
+              placeholder="请输入" />
+          </el-form-item>
+        </el-form>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+/* 样式代码 */
+</style>

+ 14 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/index.vue

@@ -0,0 +1,14 @@
+<script lang="ts" setup>
+import { ref, onMounted, watch } from "vue";
+
+onMounted(() => {})
+</script>
+
+<template>
+  <div>
+    111222333
+  </div>
+</template>
+
+<style lang="scss" scoped>
+</style>

+ 54 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/index.vue

@@ -0,0 +1,54 @@
+<script lang="ts" setup>
+import { ref, onMounted, watch } from "vue";
+import { useRoute, useRouter } from 'vue-router'
+
+const routeActivePath = ref<string>("/");
+const route = useRoute();
+const router = useRouter()
+const routerList = ref<any>([])
+
+watch(route, (newVal) => {
+  routeActivePath.value = newVal.path;
+}, { immediate: true })
+
+function toPath(path: string) {
+  router.push(path)
+}
+
+onMounted(() => {
+  const filterPath = route.meta?.parentPath
+  const list = router.getRoutes().filter((item) => item.path == filterPath)[0].children
+  routerList.value = list.filter(item => !(['/biReport/addEdit', '/biReport/dragEdit'].includes(item.path)))
+})
+</script>
+
+<template>
+  <div class="w-full h-full p-5">
+    <el-container class="flex flex-row h-full">
+      <el-aside width="200px" class="bg-white rounded-md">
+        <el-scrollbar height="100%">
+          <el-menu :default-active="routeActivePath">
+            <el-menu-item :index="item.path" v-for="(item, index) in routerList" :key="index" @click="toPath(item.path)">
+              <span>{{ item.name }}</span>
+            </el-menu-item>
+          </el-menu>
+        </el-scrollbar>
+      </el-aside>
+      <el-main>
+        <router-view v-slot="{ Component }">
+          <transition name="ranimate">
+            <component :is="Component" />
+          </transition>
+        </router-view>
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+/* 样式代码 */
+::v-deep(.el-main) {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+</style>

+ 3 - 5
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/header/header.vue

@@ -383,13 +383,11 @@ const toDetail = (row: any) => {
 onMounted(() => {
   getNewsDrawerTableData()
   const isThereAnyDataAnalysisAvailable = routers.filter((item: any) => item.path == '/analysis') || []
-  routerList.value = isThereAnyDataAnalysisAvailable.length > 0 ? routers : [
+  routerList.value = (isThereAnyDataAnalysisAvailable.length > 0 ? routers : [
     { name: '首页', id: 99999, path: '/analysis', isMenu: true, useState: false, orderitem: 1, checked: false, icon: null, functionList: [], children: [], parentId: null },
     ...routers
-  ];
-  setTimeout(() => {
-    console.log(routerList.value)
-  }, 3000)
+  ]).filter((item: any) => (item.path || '').split('/').length <= 2);
+
   activeRouter.value = routerList.value.find((item) => item.path === router.currentRoute.value.path);
 
   window.addEventListener('resize', updateVisibleItems);

+ 11 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/login.vue

@@ -135,6 +135,17 @@ const loginLogic = (data: any) => {
     alert('无权访问,请联系管理员为您分配权限')
     return
   }
+  // 将 BI 报表单独提取出来
+  data.moduleList = data.moduleList.map((item: any) => {
+    if(item.path === '/biReport') {
+      return {
+        ...item,
+        childrenList: item.children,
+        children: []
+      }
+    }
+    return item
+  })
   data.moduleList = data.moduleList.filter((item: any) => item.path != '/corpreport')
   sessionStorage.setItem('isExistBusiness', data.company.isExistBusiness || 0)
   sessionStorage.setItem('token', data.id)

+ 235 - 13
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/test/index.vue

@@ -1,20 +1,242 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import { VueFlow, Handle, Position, type Node, Edge } from '@vue-flow/core'
+import { Background } from '@vue-flow/background'
+
+import '@vue-flow/core/dist/style.css';
+import '@vue-flow/core/dist/theme-default.css';
+
+interface selectedNode {
+  id: string;
+  data: any
+}
+
+const FixedConfiguration = {
+  type: 'custom',
+  style: {
+    width: '120px', padding: '5px 20px', fontSize: '14px',
+    border: '1px solid #dcdfe6', borderRadius: '8px'
+  },
+  sourcePosition: Position.Right,
+  targetPosition: Position.Left,
+  hidden: false
+}
+
+const fixedLabel = {
+  selected: false, // 是否选中
+}
+
+const connectionType = 'step'
+
+const nodes = ref<Node[]>([
+  {
+    id: '1', data: { label: '客户', ...fixedLabel }, position: { x: 100, y: 30 },
+    ...FixedConfiguration
+  },
+])
+
+const edges = ref<Edge[]>([
+  { id: 'e1-1', source: '1', target: '2', type: connectionType },
+])
+
+const clickedNodes = ref<selectedNode[]>([]) // 记录点击的节点
+const selectNodes = ref<any[]>([]) // 记录选中的节点
+
+function onNodeClick(event: any) {
+  const { x, y } = event.node.position // 点击的位置
+  const nodeId = event.node.id
+
+  // 点击的节点Id
+  const clickedNodesStr = clickedNodes.value.map(node => node.id)
+  // 选中的节点Id
+  const selectNodeIdList = selectNodes.value.map(node => node.id)
+  // 点击长度相同的节点Id
+  const clickedNodesLengthStr = clickedNodesStr.filter(strId => strId.length == nodeId.length)
+
+  if (clickedNodesStr.includes(nodeId)) {
+    if (selectNodeIdList.includes(nodeId)) {
+      for (let i in selectNodeIdList) {
+        if (nodeId !== selectNodeIdList[i] && nodeId.length === selectNodeIdList[i].length) {
+          expandNode(selectNodeIdList[i], true)
+        } else if (nodeId == selectNodeIdList[i]) {
+          expandNode(nodeId, false)
+        }
+      }
+
+      const notSelectedToDelete = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item));
+      for (let i in notSelectedToDelete) {
+        deteleData(notSelectedToDelete[i])
+      }
+    }
+  } else {
+    clickedNodes.value.push({ id: nodeId, data: event.node.data })
+    clickedNodesStr.push(nodeId)
+
+    // 隐藏和删除其他节点
+    const hiddenNode = clickedNodesLengthStr.filter(item => selectNodeIdList.includes(item))
+    const deleteNode = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item))
+
+    hiddenNode.forEach(item => expandNode(item, true))
+    deleteNode.forEach(item => deteleData(item))
+
+    const strList = ['拜访', '拜访计划', '协议', '合同订单', '工单', '项目', '报销费用']
+    AddData(strList, x, y, nodeId)
+  }
+}
+
+
+
+// 处理级之间的联动勾选
+function processSelectedNodes(nodeId: string) {
+  const item = nodes.value.find(item => item.id === nodeId)
+  if (item?.data?.selected) { // 选中状态将之前的节点选中
+    const selectNodes = item.id.split('-').map((_: any, index: number, arr: any) => arr.slice(0, index + 1).join('-'));
+    nodes.value = nodes.value.map(node => {
+      if (selectNodes.includes(node.id)) {
+        return {
+          ...node,
+          data: { ...node.data, selected: true }
+        }
+      } else {
+        return node
+      }
+    })
+  }
+
+  if(!item?.data?.selected) {
+    const cancelSelectNodes = nodes.value.filter(node => (node.id + '').indexOf((item?.id + '')) !== -1).map(node => node.id)
+    nodes.value = nodes.value.map(node => {
+      if (cancelSelectNodes.includes(node.id)) {
+        return {
+          ...node,
+          data: { ...node.data, selected: false }
+        }
+      } else {
+        return node
+      }
+    })
+  }
+  switchColors()
+  changeEdgeStyle()
+}
+
+// 更改连线样式
+function changeEdgeStyle() {
+  const selectNodeIdList = selectNodes.value.map(node => node.id)
+  edges.value = edges.value.map(edge => {
+    const { source, target } = edge
+    const isSelected = selectNodeIdList.includes(source) && selectNodeIdList.includes(target)
+    return {
+      ...edge,
+      style: {
+        ...edge.style,
+        strokeWidth: isSelected ? 4 : 1,
+        stroke: isSelected ? '#01517f' : '',
+        class: isSelected ? 'high-z-index' : ''
+      },
+    }
+  })
+}
+
+// 设置选中背景色
+function switchColors() {
+  selectNodes.value = nodes.value.filter(node => node.data.selected)
+  nodes.value = nodes.value.map(node => ({
+    ...node,
+    style: {
+      ...node.style,
+      backgroundColor: node.data.selected ? '#01517f' : '#fff',
+    },
+  }))
+}
+
+// 添加节点和连线数据
+function AddData(list: any[], x: number, y: number, nodeId: string) {
+  const newNodes: Node[] = []
+  const newEdges = [...edges.value]
+
+  const spacing = 80
+  const startY = y - (list.length * spacing) / 2
+
+  list.forEach((label, index) => {
+    const newNodeId = `${nodeId}-${index + 1}` // 生成唯一 ID
+    const newY = startY + index * spacing
+
+    newNodes.push({
+      id: newNodeId,
+      data: { label, ...fixedLabel },
+      position: { x: x + 240, y: newY },
+      ...FixedConfiguration,
+    })
+
+    newEdges.push({
+      id: `e${nodeId}-${newNodeId}`,
+      source: nodeId,
+      target: newNodeId,
+      type: connectionType
+    })
+  })
+
+  nodes.value = [...nodes.value, ...newNodes]
+  edges.value = newEdges
+}
+
+// 删除节点和连线数据
+function deteleData(nodeId: string) {
+  clickedNodes.value = clickedNodes.value.filter(item => (item.id + '').indexOf(nodeId) === -1)
+  nodes.value = nodes.value.filter(node => (node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId)))
+  edges.value = edges.value.filter(edge => edge.source !== nodeId && edge.source.indexOf(nodeId) === -1)
+}
+
+// 展开收起节点
+function expandNode(nodeId: string, val: boolean) {
+  const filterNodes = nodes.value.filter(node => !(node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId))).map(item => item.id)
+  nodes.value = nodes.value.map(node => {
+    if (filterNodes.includes(node.id)) {
+      return {
+        ...node,
+        hidden: val
+      }
+    } else {
+      return node
+    }
+  })
+}
+
+
+</script>
+
 <template>
-  <div class="flex w-96 justify-around">
-    <div class="w-1/6 h-80 bg-cyan-200 line-through">1</div>
-    <div class="w-1/6 h-80 bg-cyan-200 leading-normal">2</div>
-    <div v-if="config[id].show" class="w-1/6 h-80 bg-cyan-200 hover:translate-y-10 duration-500">3</div>
-    <div class="myClass">4</div>
-    <div>{{ config[id].num }}</div>
+  <div style="width: 100%; height: 100%;">
+    <VueFlow :nodes="nodes" :edges="edges" @node-click="onNodeClick" fit-view-on-init :edge-updater-layer="true">
+      <template #node-custom="{ data, id }">
+        <div class="flex flex-row">
+          <Handle class="handle left-handle" type="target" :position="Position.Left" :id="`${id}`" />
+          <div class="flex items-center">
+            <el-checkbox v-model="data.selected" size="large" @change="processSelectedNodes(id)" @click.stop
+              class="pr-2 tops"></el-checkbox>
+            <span :class="{ 'text-white': data.selected }">{{ data.label }}</span>
+          </div>
+          <Handle class="handle right-handle" type="source" :position="Position.Right" :id="`${id}`" />
+        </div>
+      </template>
+      <Background />
+    </VueFlow>
   </div>
 </template>
 
-<script lang="ts" setup>
-import { config } from "./config"
-const id = 2;
-</script>
+<style lang='scss' scoped>
+.tops {
+  top: 2px
+}
+
+::deep(.vue-flow__node-custom) {
+  position: relative;
+}
 
-<style lang="scss" scoped>
-.myClass {
-  @apply w-1/6 bg-red-700 text-white hover:size-20;
+.high-z-index {
+  position: relative;
+  z-index: 9999 !important;
+  /* 提高连线层级 */
 }
 </style>

+ 18 - 4
fhKeeper/formulahousekeeper/customerBuler-crm/src/router/routerGuards.ts

@@ -12,6 +12,8 @@ export function createRouterGuards(router: Router) {
     const skipPath = ["/login", "/register", "/test", "/testEcharts"];
     if (skipPath.includes(to.path)) {
       next();
+    } else if(to.path === '/biReport') {
+      next(`/biReport/cusReportForm`);
     } else {
       if (token && routerList && routerList.length > 0) {
         if (asyncRoutesMark) {
@@ -24,11 +26,11 @@ export function createRouterGuards(router: Router) {
           );
 
           let modules = import.meta.glob("@/pages/**/*.vue");
-          console.log(modules);
-          routerList.forEach((item: any) => {
+          routerList.forEach((item: any, index: number) => {
             let filePath = item.path.replace("/", "")
-            if (item.children && item.children.length > 0) {
-              item.children.forEach((child: any) => {
+            const children = item.children;
+            if (children && children.length > 0) {
+              children.forEach((child: any) => {
                 let childFilePath = child.path.replace("/", "");
                 addNewRouter?.children.push({
                   path: child.path,
@@ -44,6 +46,17 @@ export function createRouterGuards(router: Router) {
                 meta: {},
                 component: modules[`/src/pages/${filePath}/index.vue`],
               });
+              if(item.childrenList && item.childrenList.length > 0) {
+                addNewRouter.children[index + 1].children = item.childrenList.map((child: any) => {
+                  let childFilePath = child.path.replace("/", "");
+                  return {
+                    path: child.path,
+                    name: child.name,
+                    meta: { parentPath: item.path },
+                    component: modules[`/src/pages/${childFilePath}/index.vue`]
+                  }
+                })
+              }
             }
           });
           router.addRoute(addNewRouter);
@@ -52,6 +65,7 @@ export function createRouterGuards(router: Router) {
             name: 'NotFound',
             component: () => import("../pages/404.vue"),
           })
+          console.log(router.getRoutes(), '<==== router.getRoutes()')
           next({ ...to, replace: true });
         }
       } else {

+ 4 - 3
fhKeeper/formulahousekeeper/customerBuler-crm/vite.config.ts

@@ -3,11 +3,12 @@ import vue from '@vitejs/plugin-vue';
 
 import { resolve } from 'path';
 
-// const target = 'http://192.168.2.40:10099';
-// const target = 'http://192.168.2.17:10010';
+// const target = 'http://192.168.2.10:10010';
+// const target = 'http://192.168.2.5:10010';
 // const target = "http://127.0.0.1:10010";
 // const target = "http://192.168.2.178:10010";
-const target = 'http://47.101.180.183:10014';
+// const target = 'http://47.101.180.183:10014';
+const target = 'http://1.94.62.58:10014';
 
 export default defineConfig({
   plugins: [vue({