a4f209d4 by lihua

添加关系网和桑基图

1 parent be22dd47
......@@ -28,6 +28,7 @@ declare module '@vue/runtime-core' {
FileUpload: typeof import('./src/components/FileUpload/index.vue')['default']
FixedActionBar: typeof import('./src/components/FixedActionBar/index.vue')['default']
Form: typeof import('./src/components/Form/index.vue')['default']
GraphTopbar: typeof import('./src/components/RelationNetwork/graphTopbar.vue')['default']
Hour: typeof import('./src/components/Schedule/component/hour.vue')['default']
ImagePreview: typeof import('./src/components/ImagePreview/index.vue')['default']
ImagesUpload: typeof import('./src/components/ImagesUpload/index.vue')['default']
......@@ -42,6 +43,7 @@ declare module '@vue/runtime-core' {
PageNav: typeof import('./src/components/PageNav/index.vue')['default']
PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default']
Popover: typeof import('./src/components/Popover/index.vue')['default']
RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Schedule: typeof import('./src/components/Schedule/index.vue')['default']
......
<script lang="ts" setup name="topbar">
import { ref, watch } from 'vue';
const props = defineProps({
isGraphDisplay: {
type: Boolean,
default: true
},
});
const isGraph = ref(false);
watch(() => props.isGraphDisplay, (val) => {
isGraph.value = val;
}, {
immediate: true
})
const emits = defineEmits(["displaySwitchChange"]);
const switchChange = (val) => {
isGraph.value = val
emits('displaySwitchChange', val);
}
</script>
<template>
<div className='g6-component-topbar-content'>
<div :class="isGraph ? 'selected g6-component-topbar-item' : 'g6-component-topbar-item'" @click="switchChange(true)">
关系网
</div>
<div :class="!isGraph ? 'selected g6-component-topbar-item' : 'g6-component-topbar-item'" @click="switchChange(false)">
桑基图
</div>
</div>
</template>
<style scoped lang="scss">
.g6-component-topbar-content {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: #fff;
border: 1px solid var(--el-color-primary);
border-radius: 32px;
padding: 4px;
width: 138px;
height: 32px;
}
.g6-component-topbar-item {
width: 50%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999999;
cursor: pointer;
&.selected {
background: #4FA1A4;
border-radius: 32px;
color: #fff;
}
}
</style>
\ No newline at end of file
......@@ -28,6 +28,7 @@ declare module '@vue/runtime-core' {
FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
FixedActionBar: typeof import('./../components/FixedActionBar/index.vue')['default']
Form: typeof import('./../components/Form/index.vue')['default']
GraphTopbar: typeof import('./../components/RelationNetwork/graphTopbar.vue')['default']
Hour: typeof import('./../components/Schedule/component/hour.vue')['default']
ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
......@@ -42,6 +43,7 @@ declare module '@vue/runtime-core' {
PageNav: typeof import('./../components/PageNav/index.vue')['default']
PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default']
Popover: typeof import('./../components/Popover/index.vue')['default']
RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Schedule: typeof import('./../components/Schedule/index.vue')['default']
......
<template>
<div ref="containerRef" class='canvas-wrapper'>
</div>
</template>
<script lang="ts" setup name="topbar">
import { ref, watch } from 'vue';
import * as echarts from "echarts";
const props = defineProps({
treeData: {
type: Array<any>,
default: [
{
source: 'a',
target: 'a1',
value: 5
},
{
source: 'a',
target: 'a2',
value: 3
},
{
source: 'b',
target: 'b1',
value: 8
},
{
source: 'a',
target: 'b1',
value: 3
},
{
source: 'b1',
target: 'a1',
value: 1
},
{
source: 'b1',
target: 'c',
value: 2
}
]
},
names: {
type: Array<any>,
default: [
{
name: 'a'
},
{
name: 'b'
},
{
name: 'a1'
},
{
name: 'a2'
},
{
name: 'b1'
},
{
name: 'c'
}
]
}
})
watch(() => props.treeData, (val) => {
setChartsOption();
})
const sankeyInstance: any = ref();
const containerRef = ref();
const setChartsOption = () => {
let option = {
tooltip: {
trigger: 'item'
},
series: [
{
type: 'sankey',
top: 80,
bottom: 40,
left: 50,
right: 50,
data: props.names,
links: props.treeData,
lineStyle: {
color: 'source',
curveness: 0.5
},
}
]
}
sankeyInstance.value.setOption(option);
sankeyInstance.value.resize();
}
const resizeObserver = ref();
const observeResize = () => {
resizeObserver.value = new ResizeObserver(() => {
sankeyInstance.value?.resize();
});
resizeObserver.value.observe(containerRef.value);
}
onMounted(() => {
nextTick(() => {
sankeyInstance.value = echarts.init(containerRef.value);
setChartsOption();
observeResize();
})
})
</script>
<style lang="scss" scoped>
.canvas-wrapper {
width: 100%;
height: 100%;
position: relative;
}
</style>
\ No newline at end of file
<route lang="yaml">
name: metadataStandardQuery
</route>
<script lang="ts" setup name="metadataStandardQuery">
import { ref } from 'vue';
import { ElMessage, ElMessageBox } from "element-plus";
import Sankey from './components/Sankey.vue';
import Tree from '@/components/Tree/index.vue';
import RelationNetwork from '@/components/RelationNetwork/index.vue';
import {
} from '@/api/modules/dataMetaService';
import { useRouter, useRoute } from "vue-router";
import useDataMetaStore from "@/store/modules/dataMeta"
const router = useRouter();
const route = useRoute()
const switchGraphDisplay = ref(true); //true表示关系网,false表示桑基图
const { proxy } = getCurrentInstance() as any;
const relationNetworkRef = ref();
const treeInfo = ref({
id: "data-meta-standard-tree",
filter: true,
queryValue: "",
queryPlaceholder: "输入库/表名称搜索",
props: {
label: "name",
value: "guid",
isLeaf: "isLeaf",
},
nodeKey: 'guid',
expandedKey: ['1'],
currentNodeKey: '',
expandOnNodeClick: false,
data: <any>[],
loading: false
});
/** 获取左侧树数据. */
const getTreeData = async () => {
treeInfo.value.loading = true
// let params = {}
// const res: any = await getMetaTreeData(params)
// if (res.code == proxy.$passCode) {
// const data = res.data || [];
// let treeData: any = Object.keys(data).length ? [data] : [];
// treeInfo.value.data = treeData;
// allTreeData.value = treeData;
// if (treeData.length) {
// treeInfo.value.currentNodeKey = treeData[0].guid;
// treeInfo.value.expandedKey = <any>[treeData[0].guid];
// }
// } else {
// ElMessage.error(res.msg);
// }
treeInfo.value.data = [{
guid: '1',
name: '第一级1',
children: [{
guid: '1-1',
name: '第二级1-1',
children: [{
guid: '1-1-1',
name: '第三级1-1-1'
}]
}, {
guid: '1-2',
name: '第二级1-2'
}, {
guid: '1-3',
name: '第二级1-3'
}]
}, {
guid: '2',
name: '第一级2',
children: [{
guid: '2-1',
name: '第二级1-1'
}, {
guid: '2-2',
name: '第二级1-2'
}]
}];
treeInfo.value.loading = false
treeInfo.value.currentNodeKey = '1';
nodeClick(treeInfo.value.data[0])
}
/** 左侧树的的组件引用. */
const treeInfoRef = ref();
/** 当前选中的树节点数据data */
const lastClickNode: any = ref({});
const treeDataLoading = ref(false);
/** 数据血缘关系图组件 */
const lineageGraph: any = ref();
/** 点击左侧树节点,更新对应的血缘关系图. */
const nodeClick = (data) => {
console.log(data);
const ele = <HTMLElement>document.querySelector(".g6-component-contextmenu")
if (ele) {
ele.style.display = "none"
}
nextTick(() => {
lineageGraph.value?.tooltip1.hide()
})
treeInfo.value.currentNodeKey = data.guid;
treeInfo.value.expandedKey = <any>[data.guid];
lastClickNode.value = data;
}
/** 选中树节点后自动滚动到可视范围内. */
const scrollToNode = (nodeId) => {
nextTick(() => {
const nodeElement = treeInfoRef.value.treeRef.$el.querySelector(`[data-key="${nodeId}"]`);
if (nodeElement) {
nodeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}
/** 处理从详情处跳转而来的默认展示. */
const processRouter = () => {
const { guid, databaseName, tableName, databaseChName, databaseGuid, fieldGuid, fieldEnName, set } = useDataMetaStore()
let isFL = useDataMetaStore().isFieldLineage;
if (fieldGuid) {//查看字段血缘的
nextTick(() => {
treeInfo.value.expandedKey = <any>[databaseGuid, guid];
treeInfo.value.currentNodeKey = fieldGuid as string;
scrollToNode(fieldGuid);
treeInfoRef.value.setCurrentKey(fieldGuid);
})
lastClickNode.value = { guid: fieldGuid, tableGuid: guid, databaseGuid: databaseGuid, enName: fieldEnName, tableName, databaseName, type: 4, isLeaf: true, databaseChName };
// getTableFieldLineageMap();
set()
} else if (guid) {
treeInfo.value.currentNodeKey = guid as string;
treeInfo.value.expandedKey = <any>[databaseGuid, guid];
lastClickNode.value = { guid: guid, tableName, databaseName, type: 3, databaseChName };
scrollToNode(guid);
switchGraphDisplay.value = isFL;
// if (isFL) {
// getAllTableFieldLineageMap();
// } else {
// getTableLineageMap()
// }
set()
}
}
onActivated(() => {
processRouter();
});
onBeforeMount(async () => {
await getTreeData()
processRouter();
})
onMounted(() => { })
const isGraphDisplay = ref(true);
const displaySwitchChange = (val) => {
isGraphDisplay.value = val;
}
const handleNodeItemClick = (graph, nodeItem) => {
const nodeId = nodeItem.get('id');
const parentData = graph.findDataById(nodeId);
if (!parentData.children) {
parentData.children = [];
}
treeDataLoading.value = true;
let childData = [{
guid: '33',
isField: true,
name: '字段1'
}, {
guid: '44',
isField: true,
name: '字段2字段2字段2字段2字段2字段2字段2字段2字段2xx2字段22字段22字段22字段22字段22字段2'
}, {
guid: '55',
isField: true,
name: '字段3'
}]
// parentData.collapsed = true;
// nodeItem.getModel().collapsed = true;
parentData.children = childData;
setTimeout(() => {
treeDataLoading.value = false;
graph.changeData();
// parentData.collapsed = true;
// graph.updateChildren(childData, parentData.id);
// graph.changeData(lastClickNode.value);
//graph.data(lastClickNode.value);
}, 2000)
}
const handleContextMenu = (nodeData) => {
//TODO,新建引用数据集
}
onBeforeUnmount(() => {
relationNetworkRef.value.destroy();
})
</script>
<template>
<div>元数据标准查询</div>
<div class="container_wrap full flex">
<div class="aside_wrap">
<div class="aside_title">数据库目录列表</div>
<Tree ref="treeInfoRef" :treeInfo="treeInfo" @nodeClick="nodeClick" />
</div>
<div class="main_wrap">
<div className='g6-component-topbar'>
<graphTopbar ref="topBarRef" @displaySwitchChange="displaySwitchChange" :isGraphDisplay="isGraphDisplay" />
</div>
<RelationNetwork v-show="isGraphDisplay" ref="relationNetworkRef" :tree-data="lastClickNode"
v-loading="treeDataLoading" @nodeItemClick="handleNodeItemClick" @contextMenu="handleContextMenu">
</RelationNetwork>
<Sankey v-show="!isGraphDisplay"></Sankey>
<!-- <div v-show="lastClickNode && lastClickNode?.type !== 3 && lastClickNode?.type !== 4" class="main-placeholder">
<img src="../../assets/images/no-data.png" :style="{ width: '96px', height: '96px' }" />
<div class="empty-text">暂无标准数据</div>
</div> -->
</div>
</div>
</template>
<script>
export default {
<style scoped lang="scss">
.container_wrap {
.aside_wrap {
width: 200px;
margin-right: 1px;
}
.main_wrap {
position: relative;
:deep(.canvas-wrapper) {
background-color: #f7f7f9;
}
.main-placeholder {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.empty-text {
font-size: 14px;
color: #b2b2b2;
}
}
}
}
</script>
<style>
</style>
\ No newline at end of file
.g6-component-topbar {
position: absolute;
left: 24px;
bottom: unset;
top: 14px;
padding: 0;
text-align: center;
z-index: 999;
}
.container_wrap.flex .main_wrap {
padding: 0px;
}
.tree_panel {
height: calc(100% - 36px);
padding-top: 0;
:deep(.el-tree) {
margin: 0;
overflow: hidden auto;
}
}
.card-noData {
height: 100%;
width: 100%;
background: #fafafa;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
font-size: 14px;
}
:deep(.el-form .el-form-item) {
width: calc(100%);
// margin-right: 8px;
}
:deep(.el-message) {
position: fixed;
/* 使用fixed或absolute定位 */
z-index: 10000;
/* 设置一个较高的z-index值确保在最上层显示 */
}
</style>
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!