Browse Source

ETA图库

jwyu 11 months ago
parent
commit
69410bb2e3

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 # 接口地址
-VITE_APP_API_URL="http://8.136.199.33:8610/v1"
+VITE_APP_API_URL="/v1"
 # 路由根地址
 VITE_APP_BASE_URL="/"
 # 打包输入文件名

+ 1 - 1
.env.production

@@ -1,5 +1,5 @@
 # 接口地址
-VITE_APP_API_URL="http://8.136.199.33:8610/v1"
+VITE_APP_API_URL="/v1"
 # 路由根地址
 VITE_APP_BASE_URL="/"
 # 打包输入文件名

+ 1 - 1
.env.test

@@ -1,5 +1,5 @@
 # 接口地址
-VITE_APP_API_URL="http://8.136.199.33:8610/v1"
+VITE_APP_API_URL="/v1"
 # 路由根地址
 VITE_APP_BASE_URL="/"
 # 打包输入文件名

+ 3 - 1
README.md

@@ -1 +1,3 @@
-# ETA社区项目
+# ETA社区项目
+1. 测试访问地址:http://8.136.199.33:8901/
+

+ 4 - 1
package.json

@@ -10,8 +10,11 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@antv/x6": "^2.18.1",
+    "@antv/x6-vue-shape": "^2.1.2",
     "@vueuse/core": "^10.9.0",
     "axios": "^1.6.8",
+    "dagre": "^0.8.5",
     "highcharts": "^11.4.1",
     "lodash": "^4.17.21",
     "moment": "^2.30.1",
@@ -19,7 +22,7 @@
     "tdesign-icons-vue-next": "^0.2.2",
     "tdesign-vue-next": "^1.9.3",
     "vue": "^3.4.21",
-    "vue-router": "4"
+    "vue-router": "^4.3.2"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.0.4",

+ 69 - 4
pnpm-lock.yaml

@@ -1,12 +1,21 @@
 lockfileVersion: '6.0'
 
 dependencies:
+  '@antv/x6':
+    specifier: ^2.18.1
+    version: 2.18.1
+  '@antv/x6-vue-shape':
+    specifier: ^2.1.2
+    version: 2.1.2(@antv/x6@2.18.1)(vue@3.4.21)
   '@vueuse/core':
     specifier: ^10.9.0
     version: 10.9.0(vue@3.4.21)
   axios:
     specifier: ^1.6.8
     version: 1.6.8
+  dagre:
+    specifier: ^0.8.5
+    version: 0.8.5
   highcharts:
     specifier: ^11.4.1
     version: 11.4.1
@@ -29,8 +38,8 @@ dependencies:
     specifier: ^3.4.21
     version: 3.4.21
   vue-router:
-    specifier: '4'
-    version: 4.3.0(vue@3.4.21)
+    specifier: ^4.3.2
+    version: 4.3.2(vue@3.4.21)
 
 devDependencies:
   '@vitejs/plugin-vue':
@@ -58,6 +67,40 @@ packages:
     resolution: {integrity: sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==}
     dev: true
 
+  /@antv/x6-common@2.0.17:
+    resolution: {integrity: sha512-37g7vmRkNdYzZPdwjaMSZEGv/MMH0S4r70/Jwoab1mioycmuIBN73iyziX8m56BvJSDucZ3J/6DU07otWqzS6A==}
+    dependencies:
+      lodash-es: 4.17.21
+      utility-types: 3.11.0
+    dev: false
+
+  /@antv/x6-geometry@2.0.5:
+    resolution: {integrity: sha512-MId6riEQkxphBpVeTcL4ZNXL4lScyvDEPLyIafvWMcWNTGK0jgkK7N20XSzqt8ltJb0mGUso5s56mrk8ysHu2A==}
+    dev: false
+
+  /@antv/x6-vue-shape@2.1.2(@antv/x6@2.18.1)(vue@3.4.21):
+    resolution: {integrity: sha512-lfLNJ2ztK8NP2JBAWTD6m5Wol0u6tOqj2KdOhWZoT8EtEw9rMmAdxsr8uTi9MRJO9pDMM0nbsR3cidnMh7VeDQ==}
+    peerDependencies:
+      '@antv/x6': ^2.x
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^2.0.0 || >=3.0.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+    dependencies:
+      '@antv/x6': 2.18.1
+      vue: 3.4.21
+      vue-demi: 0.14.7(vue@3.4.21)
+    dev: false
+
+  /@antv/x6@2.18.1:
+    resolution: {integrity: sha512-FkWdbLOpN9J7dfJ+kiBxzowSx2N6syBily13NMVdMs+wqC6Eo5sLXWCZjQHateTFWgFw7ZGi2y9o3Pmdov1sXw==}
+    dependencies:
+      '@antv/x6-common': 2.0.17
+      '@antv/x6-geometry': 2.0.5
+      utility-types: 3.11.0
+    dev: false
+
   /@babel/helper-string-parser@7.24.1:
     resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
     engines: {node: '>=6.9.0'}
@@ -905,6 +948,13 @@ packages:
   /csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  /dagre@0.8.5:
+    resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
+    dependencies:
+      graphlib: 2.1.8
+      lodash: 4.17.21
+    dev: false
+
   /data-view-buffer@1.0.1:
     resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
     engines: {node: '>= 0.4'}
@@ -1422,6 +1472,12 @@ packages:
     resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
     dev: true
 
+  /graphlib@2.1.8:
+    resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==}
+    dependencies:
+      lodash: 4.17.21
+    dev: false
+
   /has-ansi@2.0.0:
     resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
     engines: {node: '>=0.10.0'}
@@ -1826,6 +1882,10 @@ packages:
       pkg-types: 1.0.3
     dev: true
 
+  /lodash-es@4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+    dev: false
+
   /lodash@4.17.21:
     resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
     dev: false
@@ -2804,6 +2864,11 @@ packages:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
+  /utility-types@3.11.0:
+    resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
+    engines: {node: '>= 4'}
+    dev: false
+
   /validator@13.11.0:
     resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==}
     engines: {node: '>= 0.10'}
@@ -2882,8 +2947,8 @@ packages:
     dependencies:
       vue: 3.4.21
 
-  /vue-router@4.3.0(vue@3.4.21):
-    resolution: {integrity: sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==}
+  /vue-router@4.3.2(vue@3.4.21):
+    resolution: {integrity: sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==}
     peerDependencies:
       vue: ^3.2.0
     dependencies:

+ 28 - 0
src/api/etaChart/etaChart.js

@@ -0,0 +1,28 @@
+import {get,post} from '@/api/index'
+
+export default{
+    /* 图表分类
+     * ParentId:0 表示查询未分类目录下的内容,-1表示查询所有一级目录
+    */
+    classifyList:params=>{
+        return get('/chart/classify/simple',params)
+    },
+    // 图列表
+    chartList:params=>{
+        return get('/chart/classify/chart_list',params)
+    },
+    // 图详情
+    chartDetail:params=>{
+        return get('/chart/detail',params)
+    },
+    // 图搜索
+    chartSearch:params=>{
+        return get('/chart/search_by_es',params)
+    },
+    // 指标溯源
+    getEdbSource:params=>{
+        return get('/edb/trace',params)
+    }
+
+
+}

+ 5 - 0
src/api/etaChart/index.js

@@ -0,0 +1,5 @@
+import apiETAChart from './etaChart'
+
+export {
+    apiETAChart,
+}

+ 1 - 2
src/api/index.js

@@ -34,7 +34,7 @@ _axios.interceptors.request.use(
       LOADINGCOUNT++;
     }
     
-    config.headers.Authorization=localStorage.getItem('token')||''
+    // config.headers.Authorization=localStorage.getItem('token')||''
     return config;
   },
   function (error) {
@@ -71,7 +71,6 @@ _axios.interceptors.response.use(
     }
     if(data.Ret===408){//token失效
       MessagePlugin.warning(data.Msg)
-      router.replace('/login')
     }
     if(data.Ret===403){
       setTimeout(() => {

+ 8 - 0
src/api/system/common.js

@@ -0,0 +1,8 @@
+import {get,post} from '@/api/index'
+
+export default{
+    //机构用户
+    companyUserList:()=>{
+        return get('/admin/business/admin',{})
+    }
+}

+ 6 - 0
src/api/system/index.js

@@ -0,0 +1,6 @@
+import apiSystemCommon from './common'
+
+export {
+    apiSystemCommon
+}
+

BIN
src/assets/imgs/nodata.png


BIN
src/assets/imgs/tree_closed.png


BIN
src/assets/imgs/tree_opend.png


+ 3 - 0
src/assets/svg/edb_source.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.79165 3.33337C13.3586 3.33337 16.25 6.22472 16.25 9.7917C16.25 10.2693 16.198 10.7403 16.0963 11.1986L16.0556 11.3699L15.0918 11.1281C15.2011 10.6949 15.2564 10.2471 15.2564 9.7917C15.2564 6.77351 12.8098 4.32696 9.79165 4.32696C6.77345 4.32696 4.3269 6.77351 4.3269 9.7917C4.3269 12.8099 6.77345 15.2564 9.79165 15.2564C11.0708 15.2585 12.3098 14.8099 13.2911 13.9893L13.3268 13.9585L9.44422 10.1464L10.1391 9.43666L14.1528 13.3772L14.7477 13.9625L14.3999 14.3168C13.7994 14.9299 13.0823 15.4167 12.291 15.7487C11.4996 16.0807 10.6498 16.2512 9.79165 16.25C6.22466 16.25 3.33331 13.3587 3.33331 9.7917C3.33331 6.22472 6.22466 3.33337 9.79165 3.33337ZM9.79165 6.64534C10.2582 6.64525 10.719 6.74894 11.1406 6.94889C11.5621 7.14884 11.934 7.44004 12.2291 7.80141C12.5242 8.16279 12.7353 8.58528 12.8471 9.03828C12.9588 9.49129 12.9684 9.96348 12.8751 10.4206L12.8476 10.5432L11.8825 10.3067C11.9938 9.85448 11.9555 9.37834 11.7732 8.94974C11.5909 8.52115 11.2746 8.16322 10.8717 7.92966C10.4687 7.69611 10.0009 7.59952 9.53843 7.65441C9.07593 7.70929 8.64371 7.91267 8.30662 8.23405C7.96953 8.55542 7.74575 8.97744 7.66887 9.43678C7.59198 9.89613 7.66613 10.368 7.88019 10.7816C8.09426 11.1953 8.43668 11.5283 8.85609 11.7308C9.2755 11.9333 9.74927 11.9943 10.2063 11.9047L10.3073 11.8825L10.5441 12.8473C10.112 12.9534 9.66223 12.9667 9.2246 12.8861C8.78696 12.8056 8.37137 12.6332 8.00528 12.3802C7.6392 12.1273 7.33093 11.7995 7.10083 11.4186C6.87074 11.0378 6.72405 10.6124 6.67046 10.1707C6.61686 9.72892 6.65757 9.28084 6.78991 8.85598C6.92225 8.43113 7.1432 8.03919 7.43817 7.70602C7.73314 7.37285 8.09543 7.10604 8.50112 6.9232C8.90681 6.74037 9.34666 6.64566 9.79165 6.64534Z" fill="#0052D9"/>
+</svg>

+ 150 - 0
src/components/EdbSourceToolTip.vue

@@ -0,0 +1,150 @@
+<script setup>
+import { inject, nextTick, onMounted, ref } from 'vue'
+import { Node } from '@antv/x6'
+
+const getNode = inject('getNode')
+const getGraph = inject('getGraph')
+
+const data = ref({
+  fold: false,
+  routeQuery: {}
+})
+const show = ref(false)
+let node = {}, graph = {};
+
+function initNode() {
+  node = getNode();
+  graph = getGraph()
+  node.data = node.data ? { ...data.value, ...node.data } : data.value
+  data.value = node.data
+
+  let { style } = data.value
+  initStyle(style)
+}
+
+const container = ref(null)
+const antvTooltip = ref(null)
+const containerText = ref(null)
+function initStyle(style) {
+  // !important 是有的节点 :hover不变色
+  if (style) {
+    // 背景色
+    if (style.backgroundColor && style.backgroundColor.indexOf("!important") != -1) {
+      let value = style.backgroundColor.split("!")[0]
+      containerText.value.style.setProperty('background-color', value, 'important')
+    } else {
+      container.value.style.backgroundColor = style.backgroundColor ? style.backgroundColor :
+        data.value.isRoot ? '#0052d9' : "#f2f6fa"
+    }
+    // 字体颜色
+    if (style.color && style.color.indexOf("!important") != -1) {
+      let value = style.color.split("!")[0]
+      containerText.value.style.setProperty('color', value, 'important')
+    } else {
+      container.value.style.color = style.color ? style.color :
+        data.value.isRoot ? '#ffffff' : "#000000"
+    }
+    nextTick(()=>{
+        containerText.value.style.color = style.color
+    })
+    
+  }
+}
+
+function flodApi(event) {
+  const succ = graph.getSuccessors(node)
+  if (succ) {
+    succ.forEach((node, index) => {
+      node.setVisible(data.value.fold)
+      if (data.value.fold) {
+        node.toBack()
+        requestAnimationFrame(() => {
+          let edge = graph.getIncomingEdges(node) || []
+          edge[0].toBack()
+        })
+      }
+      node.data.fold = !data.value.fold
+    })
+  }
+  data.value.fold = !data.value.fold
+}
+
+function nodeMouseEnter() {
+  show.value = true
+}
+function nodeMouseOut() {
+  show.value = false
+}
+
+onMounted(() => {
+  initNode()
+})
+
+
+
+
+</script>
+
+<template>
+  <div
+    class="container"
+    ref="container"
+    @mouseover="nodeMouseEnter"
+    @mouseout="nodeMouseOut"
+  >
+    <t-tooltip
+      :content="data.RuleTitle"
+      class="container-input"
+      ref="antvTooltip"
+    >
+      <span ref="containerText">{{ data.EdbName }}</span>
+    </t-tooltip>
+    <t-icon
+      :name="data.fold ? 'add-circle' : 'minus-circle'"
+      v-if="!data.isLeaf"
+      @click="flodApi"
+      v-show="show"
+      class="fold-icon"
+    />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
+  background-color: #0052d9;
+  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
+  display: flex;
+  cursor: pointer;
+  position: relative;
+  border-radius: 4px;
+  .container-input {
+    text-align: center;
+    font-size: 16px;
+    color: #ffffff;
+    font-weight: 400;
+    word-break: break-all;
+  }
+  //   &:hover {
+  //     background-color: #ecf5ff;
+  //     .container-input {
+  //       color: #0052d9 !important;
+  //       text-decoration: underline;
+  //     }
+  //   }
+  .fold-icon {
+    position: absolute;
+    font-size: 24px;
+    color: #666666;
+    bottom: -24px;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+}
+</style>

+ 21 - 0
src/components/Empty.vue

@@ -0,0 +1,21 @@
+<script setup>
+
+</script>
+
+<template>
+    <div class="empty-wrap">
+        <img src="@/assets/imgs/nodata.png" alt="">
+        <p>暂无数据</p>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.empty-wrap{
+    text-align: center;
+    color: #666;
+    img{
+        width: 200px;
+        margin-bottom: 20px;
+    }
+}
+</style>

+ 3 - 2
src/main.js

@@ -12,11 +12,12 @@ import svgIcon from "@/components/SvgIcon.vue";
 //引入注册脚本
 import 'virtual:svg-icons-register'
 
+import EmptyWrap from '@/components/Empty.vue'
 
-const app= createApp(App)
 
-// reportErr(app)//设置全局错误上报
+const app= createApp(App)
 
 app.component('svg-icon', svgIcon)
+app.component('empty-wrap', EmptyWrap)
 app.use(router)
 app.mount('#app')

+ 9 - 1
src/router/index.js

@@ -18,7 +18,15 @@ const routes = [
         meta:{
           title:'ETA图库'
         },
-      }
+      },
+      {
+        path:'EDBSource',
+        name:'EDBSource',
+        component:()=>import('@/views/EDBSource.vue'),
+        meta:{
+          title:'指标溯源'
+        },
+      },
     ]
   },
   {

+ 39 - 0
src/views/AutoLogin.vue

@@ -0,0 +1,39 @@
+<script setup>
+import {apiSystemSet} from '@/api/system'
+import { useRoute, useRouter } from 'vue-router'
+
+const route=useRoute()
+const router=useRouter()
+
+function init(){
+    const code=route.query.code
+    if(!code) return
+    apiSystemSet.loginWithCode({
+        AuthCode:code
+    }).then(res=>{
+        if(res.Ret===200){
+            sessionStorage.setItem('token',res.Data.Authorization)
+            sessionStorage.setItem('userInfo',JSON.stringify(res.Data))
+            router.replace('/etaChart/index')
+        }
+    })
+}
+init()
+
+</script>
+
+<template>
+    <div class="auto-login-page">
+        登录中...
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.auto-login-page{
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+</style>

+ 376 - 0
src/views/EDBSource.vue

@@ -0,0 +1,376 @@
+<script setup>
+import { nextTick, ref } from 'vue'
+import { apiETAChart } from '@/api/etaChart'
+import { useRoute, useRouter } from 'vue-router'
+import { Graph } from '@antv/x6';
+import { register, getTeleport } from '@antv/x6-vue-shape'
+import dagre from "dagre"
+import EdbSourceToolTip from '@/components/EdbSourceToolTip.vue';
+
+
+const route = useRoute()
+const router = useRouter()
+
+const treeData = ref({})
+async function getDetail() {
+  if (!route.query.code) return
+  const res = await apiETAChart.getEdbSource({
+    UniqueCode: route.query.code
+  })
+  if (res.Ret === 200) {
+    treeData.value = res.Data;
+    nextTick(() => {
+      init()
+    })
+  }
+}
+getDetail()
+
+let graph = null, mould = ref(null), mouldText = ref(null);
+let params = {
+  stopNodeClick: false
+}
+
+
+const TeleportContainer = getTeleport()
+
+function init() {
+  graph = new Graph({
+    container: document.getElementById('sand-chart-body'),
+    autoResize: false,
+    async: true,//不异步时,显示隐藏会卡死
+    panning: true,
+    background: {
+      color: '#fff',
+    },
+    scroller: {
+      enabled: true,
+      pannable: true,
+      minVisibleWidth: 50,
+      minVisibleHeight: 50,
+    },
+    interacting: {
+      nodeMovable: false,
+      magnetConnectable: false,
+      edgeMovable: false,
+      edgeLabelMovable: false,
+      arrowheadMovable: false,
+      vertexMovable: false,
+      vertexMovable: false,
+      vertexDeletable: false
+    },
+    mousewheel: {
+      enabled: true,
+      modifiers: ['ctrl', 'meta']
+    }, //滚轮缩放
+    scaling: {
+      min: 0.5,
+      max: 2
+    },
+    //小地图
+    // minimap: {
+    //   enabled: true,
+    //   container: document.getElementById("minimap"),
+    // }
+  })
+
+  graph.on('node:mousemove', () => {
+    // 拖动画布的时候 阻止节点的点击操作
+    params.stopNodeClick = true
+  })
+
+  graph.on('node:mouseup', () => {
+    // 延迟释放
+    requestAnimationFrame(() => {
+      params.stopNodeClick = false
+    })
+  })
+
+  register({
+    shape: 'vue-shape',
+    component: EdbSourceToolTip,
+  })
+
+
+  Graph.registerEdge(
+    'org-edge',
+    {
+      attrs: {
+        line: {
+          strokeWidth: 1,
+          stroke: '#dddddd',
+          sourceMarker: null,
+          targetMarker: null,
+        },
+      },
+      zIndex: 0
+    },
+    true,
+  )
+
+
+  mouldText.value = document.getElementById('mould-text')
+  let edbName = treeData.value.EdbName + (treeData.value.IsStop ? '(暂停更新)' : '')
+  mouldText.value.innerText = edbName
+  mould.value = document.getElementById('mould')
+
+  let node = graph.createNode({
+    shape: 'vue-shape',
+    component: 'custom-rect',
+    width: mould.value.offsetWidth + 1,
+    height: mould.value.offsetHeight,
+    attrs: {
+      body: {
+        rx: 4,
+        ry: 4,
+        strokeWidth: 1,
+        class: 'body-class'
+      },
+    },
+    data: {
+      EdbName: edbName,
+      RuleTitle: treeData.value.RuleTitle,
+      routeQuery: {
+        ClassifyId: treeData.value.ClassifyId,
+        UniqueCode: treeData.value.UniqueCode,
+        EdbInfoId: treeData.value.EdbInfoId,
+        EdbInfoType: treeData.value.EdbInfoType,
+        HaveOperaAuth: treeData.value.HaveOperaAuth
+      },
+      isRoot: true,
+      isLeaf: (treeData.value.Child && treeData.value.Child.length > 0) ? false : true,
+      style: {
+        color: treeData.value.IsStop ? "red!important" : '#ffffff!important',
+      },
+    }
+  })
+  let cells = [node, ...createCells(treeData.value.Child, node)]
+  graph.resetCells(cells)
+  layout()
+  graph.positionCell(node, 'top', { padding: { top: 20 } })
+}
+
+function createCells(list, parentNode) {
+  if (!(list && list.length > 0)) {
+    return []
+  }
+  let dataList = []
+  list.forEach(element => {
+    let edbName = element.EdbName + (element.IsStop ? '(暂停更新)' : '')
+
+    mouldText.value.innerText = edbName
+    let node = graph.createNode({
+      shape: 'vue-shape',
+      component: 'custom-rect',
+      width: mould.value.offsetWidth + 1,
+      height: mould.value.offsetHeight,
+      attrs: {
+        body: {
+          rx: 4,
+          ry: 4,
+          strokeWidth: 1,
+          class: 'body-class'
+        },
+      },
+      data: {
+        style: {
+          backgroundColor: '#f2f6fa',
+          color: element.IsStop ? "red!important" : '#000000',
+        },
+        EdbName: edbName,
+        RuleTitle: element.RuleTitle,
+        routeQuery: {
+          ClassifyId: element.ClassifyId,
+          UniqueCode: element.UniqueCode,
+          EdbInfoId: element.EdbInfoId,
+          EdbInfoType: element.EdbInfoType,
+          HaveOperaAuth: treeData.value.HaveOperaAuth
+        },
+        isRoot: false,
+        isLeaf: (element.Child && element.Child.length > 0) ? false : true
+      }
+    })
+    let side = graph.createEdge({
+      shape: 'org-edge',
+      source: { cell: parentNode.id },
+      target: { cell: node.id },
+    })
+    dataList.push(node)
+    dataList.push(side)
+    dataList = [...dataList, ...createCells(element.Child, node)]
+  });
+  return dataList
+}
+
+function layout() {
+  const dir = 'TB'
+  const nodes = graph.getNodes()
+  const edges = graph.getEdges()
+  const g = new dagre.graphlib.Graph()
+
+  g.setDefaultEdgeLabel(() => ({}))
+  nodes.forEach((node) => {
+    g.setNode(node.id, { width: node.size().width, height: node.size().height })
+  })
+
+  let heights = nodes.map(item => {
+    // 为了防止重叠,不知道他内部怎么排的,先调整这样。
+    if (item.data.isLeaf) {
+      return item.size().height * 1.5 - 100
+    } else {
+      return item.size().height / 2
+    }
+  })
+  let maxHeight = Math.max(...heights)
+  // console.log(maxHeight,'maxHeight');
+  g.setGraph({ rankdir: dir, nodesep: 50, ranksep: maxHeight })
+  edges.forEach((edge) => {
+    const source = edge.getSource()
+    const target = edge.getTarget()
+    g.setEdge(source.cell, target.cell)
+  })
+
+  dagre.layout(g)
+
+  g.nodes().forEach((id) => {
+    const node = graph.getCellById(id)
+    if (node) {
+      const pos = g.node(id)
+      node.position(pos.x, pos.y)
+    }
+  })
+  edges.forEach((edge) => {
+    const source = edge.getSourceNode()
+    const target = edge.getTargetNode()
+    const sourceBBox = source.getBBox()
+    const targetBBox = target.getBBox()
+
+    if ((dir === 'LR' || dir === 'RL') && sourceBBox.y !== targetBBox.y) {
+      const gap =
+        dir === 'LR'
+          ? targetBBox.x - sourceBBox.x - sourceBBox.width
+          : -sourceBBox.x + targetBBox.x + targetBBox.width
+      const fix = dir === 'LR' ? sourceBBox.width : 0
+      const x = sourceBBox.x + fix + gap / 2
+      edge.setVertices([
+        { x, y: sourceBBox.center.y },
+        { x, y: targetBBox.center.y },
+      ])
+    } else if (
+      (dir === 'TB' || dir === 'BT')) {
+      const gap =
+        dir === 'TB'
+          ? targetBBox.y - sourceBBox.y - sourceBBox.height
+          : -sourceBBox.y + targetBBox.y + targetBBox.height
+      const fix = dir === 'TB' ? sourceBBox.height : 0
+      const y = sourceBBox.y + fix + gap / 2
+
+      edge.setVertices([
+        { x: sourceBBox.center.x, y },
+        { x: targetBBox.center.x, y },
+      ])
+    } else {
+      edge.setVertices([])
+    }
+  })
+}
+
+
+</script>
+
+<template>
+  <div class="edb-source-page">
+    <div class="edb-title">{{ treeData.EdbName || "" }}</div>
+    <div class="edb-source-wrap">
+      <div class="sandbox-body">
+        <div class="sand-chart-body" id="sand-chart-body"></div>
+        <TeleportContainer />
+        <div id="minimap" class="minimap"></div>
+      </div>
+    </div>
+    <!-- 确定每个Node节点大小用 -->
+    <div id="mould"><span id="mould-text"></span></div>
+  </div>
+</template>
+
+
+<style lang="scss">
+.sandbox-body {
+  .x6-graph-scroller {
+    flex: 1;
+  }
+
+  .x6-port-body {
+    display: none;
+  }
+
+  /* reseize 框样式 */
+  .x6-widget-transform {
+    .x6-widget-transform-resize {
+      border-radius: 0;
+    }
+  }
+  .x6-widget-minimap-viewport {
+    border-color: red;
+    .x6-widget-minimap-viewport-zoom {
+      border-color: red;
+    }
+  }
+  .x6-widget-minimap {
+    width: auto !important;
+    height: auto !important;
+  }
+}
+</style>
+<style scoped lang="scss">
+.edb-source-page {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 120px);
+  overflow: hidden;
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #c8cdd9;
+  box-sizing: border-box;
+  // padding:30px;
+  .edb-title {
+    margin: 0 -30px;
+    text-align: center;
+    padding: 30px;
+    border-bottom: 1px solid #c8cdd9;
+    font-size: 16px;
+  }
+  .edb-source-wrap {
+    text-align: center;
+    flex: 1;
+    overflow: auto;
+    .sandbox-body {
+      height: 100%;
+      display: flex;
+      position: relative;
+      .minimap {
+        position: absolute;
+        right: 6px;
+        bottom: 6px;
+        box-sizing: border-box;
+      }
+      #sand-chart-body {
+        flex: 1;
+      }
+    }
+  }
+  #mould {
+    position: absolute;
+    max-width: 100px;
+    padding: 20px;
+    background-color: red;
+    font-size: 16px;
+    text-align: center;
+    border-radius: 4px;
+    top: -10000px;
+    opacity: 0;
+    word-break: break-all;
+    box-sizing: content-box;
+  }
+}
+</style>

+ 57 - 30
src/views/etaChart/Index.vue

@@ -1,10 +1,15 @@
 <script setup>
 import { ref } from 'vue'
 import ClassifyWrapVue from "./components/ClassifyWrap.vue";
+import ChartWrap from './components/ChartWrap.vue'
+import { apiETAChart } from '@/api/etaChart'
+import {useClassify} from './hooks/useClassify'
+
+const {classifyActived,userVal} =useClassify()
 
 const tableColOpts = [
   {
-    colKey: 'ChartName',
+    colKey: 'ChartClassifyName',
     title: '图表名称',
     width: '450px'
   },
@@ -14,45 +19,63 @@ const tableColOpts = [
     width: '100px'
   },
   {
-    colKey: 'AdminName',
+    colKey: 'SysUserRealName',
     title: '创建人',
     width: '80px'
   }
 ]
+let page = 1
+const pageSize = 20
+let finished = false
 const tableData = ref([])
 const tableLoading = ref(false)
-function getTableData() {
+async function getTableData() {
   tableLoading.value = true
-  setTimeout(() => {
-    for (let i = 0; i < 30; i++) {
-      tableData.value.push({
-        index: i + 1,
-        ChartName: '图表名称很长的附近的拉进来放到手机里尽快费德勒就看撒就进口法',
-        Company: '弘则研究研究',
-        AdminName: '管理眼研',
-      });
-    }
-    tableLoading.value = false
-  }, 1500);
+  const res = await apiETAChart.chartList({
+    PageSize: pageSize,
+    CurrentIndex: page,
+    ChartClassifyId:classifyActived.value[0]?classifyActived.value[0]:0,
+    SysUserIds:userVal.value?.join(',')||''
+  })
+  tableLoading.value = false
+  if (res.Ret === 200) {
+    const arr = res.Data.AllNodes || []
+    tableData.value = [...tableData.value, ...arr]
+    finished = res.Data.Paging.IsEnd
+  }
 }
 getTableData()
 function tableScroll(opt) {
-  if (tableLoading.value) return
+  if (tableLoading.value || finished) return
   if (opt.scrollBottom < 100) {
+    page++
     getTableData()
   }
 }
 
+function handleFilterList() {
+  activeChartId.value=''
+  tableData.value=[]
+  finished=false
+  page=1
+  getTableData()
+  // 取消掉右侧图表
+  activeChartId.value=''
+}
+
+
+const activeChartId = ref('')
+function handleSelectChart(item) {
+  activeChartId.value = item.ChartInfoId
+}
+
 </script>
 
 <template>
   <div class="eta-chart-page">
-    <ClassifyWrapVue />
-    <div class="table-wrap" v-loading='tableLoading'>
-      <t-list
-        style="height: 100%"
-        :onScroll="tableScroll"
-      >
+    <ClassifyWrapVue @filter="handleFilterList" @change="handleSelectChart" />
+    <div class="table-wrap" v-loading="tableLoading">
+      <t-list style="height: 100%" :onScroll="tableScroll">
         <table class="table-box" cellpadding="0" cellspacing="0">
           <thead class="table-head thead-sticky">
             <tr>
@@ -66,7 +89,12 @@ function tableScroll(opt) {
             </tr>
           </thead>
           <tbody>
-            <tr v-for="row in tableData" :key="row.index">
+            <tr
+              :class="row.ChartInfoId === activeChartId ? 'active_row' : ''"
+              v-for="row in tableData"
+              :key="row.index"
+              @click="handleSelectChart(row)"
+            >
               <td
                 v-for="opt in tableColOpts"
                 :key="opt.colKey"
@@ -77,9 +105,10 @@ function tableScroll(opt) {
             </tr>
           </tbody>
         </table>
+        <empty-wrap v-if="tableData.length===0"/>
       </t-list>
     </div>
-    <div class="bg-white chart-wrap"></div>
+    <ChartWrap :chartInfoId="activeChartId"/>
   </div>
 </template>
 
@@ -111,18 +140,16 @@ function tableScroll(opt) {
         top: 0;
       }
       .table-head {
-        background-color: #EBEEF5;
+        background-color: #ebeef5;
         font-weight: bold;
       }
-      tbody{
+      tbody {
         color: #666;
+        .active_row{
+          background-color: #ECF5FF;
+        }
       }
     }
   }
-  .chart-wrap{
-    flex: 1;
-    min-width: 600px;
-    border: 1px solid #DCDFE6;
-  }
 }
 </style>

+ 179 - 0
src/views/etaChart/components/ChartWrap.vue

@@ -0,0 +1,179 @@
+<script setup>
+import { watch, nextTick, ref } from "vue"
+import { apiETAChart } from '@/api/etaChart'
+import { useChartRender } from '@/hooks/chart/render'
+import { useRouter } from "vue-router"
+
+const router = useRouter()
+
+const { options, axisLimitState, chartRender, setLimitData, isUseSelfLimit } = useChartRender()
+
+const props = defineProps({
+  chartInfoId: {
+    type: [Number, String],
+    default: ''
+  }
+})
+
+watch(
+  () => props.chartInfoId,
+  (n) => {
+    if(!n){
+      tableData.value=[]
+      intro.value=''
+      chartInfo.value=null
+      return
+    }
+    getChartDetail()
+  }
+)
+
+const columns = [
+  {
+    colKey: 'EdbName',
+    title: '指标名称',
+    align: 'center'
+  },
+  {
+    colKey: 'Frequency',
+    title: '更新频度',
+    width: '100',
+    align: 'center'
+  },
+  {
+    colKey: 'LatestDate',
+    title: '最新日期',
+    width: '120',
+    align: 'center'
+  },
+  {
+    colKey: 'LatestValue',
+    title: '最新值',
+    width: '100',
+    align: 'center'
+  },
+  {
+    colKey: 'SourceName',
+    title: '输据来源',
+    width: '150',
+    align: 'center'
+  }
+]
+const tableData = ref([])
+const intro = ref('')
+const chartInfo = ref(null)
+async function getChartDetail() {
+
+  const res = await apiETAChart.chartDetail({
+    ChartInfoId: props.chartInfoId
+  })
+  if (res.Ret === 200) {
+    tableData.value = res.Data.EdbInfoList || []
+    intro.value = res.Data.ChartInfo.Description
+    chartInfo.value = res.Data.ChartInfo
+    nextTick(() => {
+      chartRender({
+        data: {
+          ...res.Data,
+          ChartInfo: {
+            ...res.Data.ChartInfo,
+            Calendar: '公历'
+          },
+        },
+        renderId: 'chart-box',
+        // lang:currentLang.value,
+        changeLangIsCheck: false,
+        showChartTitle: true
+      })
+    })
+  }
+}
+
+
+// 跳转指标溯源
+function handleGoEdbSource(data) {
+  const href = router.resolve({
+    path: '/EDBSource',
+    query: {
+      code: data.UniqueCode
+    }
+  }).href
+  window.open(href, "_blank")
+}
+
+
+
+</script>
+
+<template>
+  <div class="bg-white chart-wrap">
+    <template v-if="props.chartInfoId">
+      <div class="chart-render-wrap">
+        <div class="chart-box" id="chart-box"></div>
+        <div class="chart-source" v-if="chartInfo">
+          <span
+            v-if="
+              chartInfo.SourcesFrom && JSON.parse(chartInfo.SourcesFrom).isShow
+            "
+            :style="`color: ${
+              JSON.parse(chartInfo.SourcesFrom).color
+            };fontSize: ${JSON.parse(chartInfo.SourcesFrom).fontSize}px;
+                          `"
+            >来源:{{ JSON.parse(chartInfo.SourcesFrom).text }}</span
+          >
+        </div>
+      </div>
+      <div class="table-wrap">
+        <t-table
+          row-key="index"
+          :data="tableData"
+          :columns="columns"
+          bordered
+          hover
+          max-height="300"
+          cell-empty-content="-"
+          resizable
+        >
+          <template #SourceName="{ row }">
+            <span>{{ row.SourceName }}</span>
+            <!-- 指标溯源 -->
+            <svg-icon
+              v-if="row.EdbType === 2"
+              style="
+                font-size: 20px;
+                cursor: pointer;
+                position: relative;
+                top: 5px;
+              "
+              name="edb_source"
+              @click="handleGoEdbSource(row)"
+            ></svg-icon>
+          </template>
+        </t-table>
+      </div>
+      <div class="instructions-wrap" v-if="intro">
+        <p>逻辑简述:</p>
+        <p>{{ intro }}</p>
+      </div>
+    </template>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.chart-wrap {
+  flex: 1;
+  min-width: 600px;
+  border: 1px solid #dcdfe6;
+  padding: 30px;
+  .chart-render-wrap {
+    margin-bottom: 20px;
+    .chart-box {
+      height: 600px;
+    }
+  }
+  .instructions-wrap {
+    margin-top: 20px;
+    line-height: 1.7;
+  }
+}
+</style>

+ 166 - 73
src/views/etaChart/components/ClassifyWrap.vue

@@ -1,60 +1,129 @@
 <script setup>
-import { ref } from 'vue'
-import { SearchIcon,Icon  } from 'tdesign-icons-vue-next';
+import { reactive, ref, watch } from 'vue'
+import { SearchIcon, Icon } from 'tdesign-icons-vue-next';
+import { apiETAChart } from '@/api/etaChart'
+import { apiSystemCommon } from '@/api/system'
+import { useClassify } from '../hooks/useClassify'
 
-const companyOpts = [{ label: '弘则研究', value: 1 }]
-const companyVal = ref('')
+const emits = defineEmits(['change', 'filter'])
 
-const userOpts = [{ label: '弘则研究', value: 1 }]
-const userVal = ref('')
+const userProps = {
+  label: 'RealName',
+  value: 'AdminId',
+  children: 'ChildrenList'
+}
+const userOpts = ref([])
+const { userVal } = useClassify()
+async function getCompanyUserData() {
+  const res = await apiSystemCommon.companyUserList()
+  if (res.Ret === 200) {
+    userOpts.value = res.Data.List || []
+  }
+}
+getCompanyUserData()
+function handleUserChange() {
+  emits('filter')
+  getClassify()
+}
 
+const searchSelectKeys = {
+  value: 'ChartInfoId',
+  label: 'ChartName'
+}
 const searchVal = ref('')
+const searchOpts = ref([])
+const searchLoading = ref(false)
+let searchPage = 1
+const searchPageSize = 20
+let finished = false
+function handleSearchChart() {
+  finished = false
+  searchPage = 1
+  searchOpts.value = []
+  handleGetSearchChartList()
+}
+async function handleGetSearchChartList() {
+  searchLoading.value = true
+  const res = await apiETAChart.chartSearch({
+    PageSize: searchPageSize,
+    CurrentIndex: searchPage,
+    SysUserIds: userVal.value?.join(','),
+    Keyword: searchVal.value
+  })
+  searchLoading.value = false
+  if (res.Ret === 200) {
+    const arr = res.Data.List || []
+    searchOpts.value = [...searchOpts.value, ...arr]
+    finished = res.Data.Paging.IsEnd
+  }
+}
+async function handleLoadMoreChart() {
+  if (finished || searchLoading.value) return
+  handleGetSearchChartList()
+}
+function handleSelectChart(value, context) {
+  if (value) {
+    emits('change', context.option)
+  }
+}
+
+
 
 
-const classifyList = ref([{
-  label: '第一段',
-  children: [
-    {
-      label: '第二段',
-    },
-    {
-      label: '第二段',
-    },
-  ],
-},
-{
-  label: '第一段',
-  children: [
-    {
-      label: '第二段',
-    },
-    {
-      label: '第二段',
-    },
-  ],
-},
-{
-  label: '第一段',
-  children: [
-    {
-      label: '第二段',
-    },
-    {
-      label: '第二段',
-    },
-  ],
-},
-{
-  label: '第一段',
-  children: [
-    {
-      label: '第二段',
-    },
-    {
-      label: '第二段',
-    },
-  ],
-},])
+//分类列表
+const classifyTreeKeys = {
+  label: 'ChartClassifyName',
+  children: 'Children',
+  value: 'ChartClassifyId'
+}
+const classifyList = ref([])
+const { classifyActived } = useClassify()
+async function getClassify() {
+  const res = await apiETAChart.classifyList({
+    ParentId: -1,
+    SysUserIds: userVal.value?.join(',')
+  })
+  if (res.Ret === 200) {
+    const arr = res.Data.AllNodes || []
+    classifyList.value = arr.map(item => {
+      return {
+        ...item,
+        Children: true
+      }
+    })
+  }
+}
+getClassify()
+// 懒加载分类
+async function classifyLoad(node) {
+  return new Promise(async (resolve) => {
+    let nodes = []
+    const res = await apiETAChart.classifyList({
+      ParentId: node.data.ChartClassifyId
+    })
+    if (res.Ret === 200) {
+      const arr = res.Data.AllNodes || []
+      nodes = arr.map(item => {
+        return {
+          ...item,
+          Children: item.ChartInfoId ? [] : true,
+          ChartClassifyId: item.ChartInfoId ? item.UniqueCode : item.ChartClassifyId,//如果是指标则将分类id设置为图表UniqueCode
+        }
+      })
+    }
+    resolve(nodes);
+  });
+}
+//点击目录树
+function handleClassifyActiveChange({ node }) {
+  console.log(node.data);
+  classifyActived.value = [node.data.ChartClassifyId]
+  if (node.data.ChartInfoId) {//选择的是图表
+    emits('change', node.data)
+  } else {
+    emits('filter')
+  }
+}
 
 
 </script>
@@ -62,33 +131,57 @@ const classifyList = ref([{
 <template>
   <div class="bg-white classify-wrap">
     <div class="select-wrap">
-      <t-select placeholder="机构" v-model="companyVal">
-        <t-option
-          v-for="opt in companyOpts"
-          :key="opt.value"
-          :label="opt.label"
-          :value="opt.value"
-        ></t-option>
-      </t-select>
-      <t-select placeholder="创建人" v-model="userVal">
-        <t-option
-          v-for="opt in userOpts"
-          :key="opt.value"
-          :label="opt.label"
-          :value="opt.value"
-        ></t-option>
-      </t-select>
+      <t-cascader
+        v-model="userVal"
+        :options="userOpts"
+        :keys="userProps"
+        multiple
+        :minCollapsedNum="1"
+        clearable
+        filterable
+        :showAllLevels="false"
+        placeholder="创建人"
+        @change="handleUserChange"
+      />
     </div>
-    <t-input v-model="searchVal" clearable placeholder="请输入图表名称">
+    <t-select
+      v-model="searchVal"
+      placeholder="请输入图表名称"
+      clearable
+      filterable
+      :keys="searchSelectKeys"
+      :options="searchOpts"
+      :loading="searchLoading"
+      @search="handleSearchChart"
+      @change="handleSelectChart"
+      :popup-props="{ 'on-scroll-to-bottom': handleLoadMoreChart }"
+    >
       <template #prefixIcon>
         <search-icon />
       </template>
-    </t-input>
+    </t-select>
     <div class="classify-list-box">
-      <t-tree :data="classifyList" activable transition>
+      <t-tree
+        :actived="classifyActived"
+        :data="classifyList"
+        activable
+        transition
+        lazy
+        :load="classifyLoad"
+        value-mode="all"
+        :keys="classifyTreeKeys"
+        check-strictly
+        :onClick="handleClassifyActiveChange"
+      >
         <template #icon="{ node }">
-          <svg-icon style="font-size:16px" v-if="node.getChildren() && node.expanded" name="tree_opend" />
-          <svg-icon style="font-size:16px" v-if="node.getChildren() && !node.expanded" name="tree_close" />
+          <t-icon
+            name="add-rectangle"
+            v-if="node.getChildren() && !node.expanded"
+          />
+          <t-icon
+            name="minus-rectangle"
+            v-if="node.getChildren() && node.expanded"
+          />
         </template>
       </t-tree>
     </div>
@@ -100,7 +193,7 @@ const classifyList = ref([{
   width: 300px;
   flex-shrink: 0;
   padding: 20px;
-  
+
   .select-wrap {
     display: flex;
     gap: 0 10px;
@@ -108,7 +201,7 @@ const classifyList = ref([{
   }
   .classify-list-box {
     padding-top: 10px;
-    height: calc(100vh - 240px);
+    height: calc(100vh - 260px);
     overflow-y: auto;
   }
 }

+ 12 - 0
src/views/etaChart/hooks/useClassify.js

@@ -0,0 +1,12 @@
+import {ref} from 'vue'
+
+const classifyActived = ref([])//当前选中的分类
+const userVal = ref([])//当前选中的创建人
+
+export function useClassify(){
+
+    return {
+        classifyActived,
+        userVal
+    }
+}

+ 10 - 0
vite.config.js

@@ -80,5 +80,15 @@ export default defineConfig(({ mode }) => {
     build: {
       outDir: ENV.VITE_APP_OUTDIR,
     },
+    server:{
+      port:8901,
+      proxy:{
+        '/v1': {
+          target: 'http://8.136.199.33:8901/v1',
+          changeOrigin: true,
+          rewrite: (path) => path.replace(/^\/v1/, ''),
+        }
+      }
+    }
   };
 });