feat(Data Source): add Table relationship management

This commit is contained in:
dataeaseShu
2025-09-24 16:29:42 +08:00
committed by dataeaseShu
parent 9db73d3d2f
commit 24e681e921
7 changed files with 543 additions and 7 deletions
+2
View File
@@ -3,6 +3,8 @@ import { request } from '@/utils/request'
export const datasourceApi = {
check: (data: any) => request.post('/datasource/check', data),
check_by_id: (id: any) => request.get(`/datasource/check/${id}`),
relationGet: (id: any) => request.post(`/table_relation/get/${id}`),
relationSave: (dsId: any, data: any) => request.post(`/table_relation/save/${dsId}`, data),
add: (data: any) => request.post('/datasource/add', data),
list: () => request.get('/datasource/list'),
update: (data: any) => request.post('/datasource/update', data),
@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M3.64022 7.5556C4.16283 6.83993 4.62541 5.57259 5.08687 3.73093C5.35032 2.6795 5.67072 2.22226 5.94868 2.22226C6.19414 2.22226 6.39312 2.02328 6.39312 1.77782C6.39312 1.53236 6.19414 1.33337 5.94868 1.33337C5.11915 1.33337 4.5776 2.10619 4.22463 3.51488C3.82889 5.09429 3.42651 6.20034 3.07384 6.81136C2.92269 6.71956 2.74527 6.66671 2.55551 6.66671H1.88884C1.33655 6.66671 0.88884 7.11442 0.88884 7.66671V8.33337C0.88884 8.88566 1.33655 9.33337 1.88884 9.33337H2.55551C2.72066 9.33337 2.87646 9.29334 3.01373 9.22244C3.33856 9.88614 3.69874 10.9577 4.05281 12.4189C4.40497 13.8723 4.94329 14.6667 5.77769 14.6667C6.02314 14.6667 6.22213 14.4677 6.22213 14.2223C6.22213 13.9768 6.02314 13.7778 5.77769 13.7778C5.5046 13.7778 5.18096 13.3002 4.9167 12.2096C4.49157 10.4551 4.06637 9.20697 3.59353 8.44449H6.4666C6.57706 8.44449 6.6666 8.35494 6.6666 8.24449V7.7556C6.6666 7.64514 6.57706 7.5556 6.4666 7.5556H3.64022Z"/>
<path d="M7.99997 2.11115C7.99997 1.92706 8.14921 1.77782 8.3333 1.77782H14.7777C14.9618 1.77782 15.1111 1.92706 15.1111 2.11115V3.22226C15.1111 3.40636 14.9618 3.5556 14.7777 3.5556H8.3333C8.14921 3.5556 7.99997 3.40636 7.99997 3.22226V2.11115Z"/>
<path d="M8.3333 7.11115C8.14921 7.11115 7.99997 7.26039 7.99997 7.44449V8.5556C7.99997 8.73969 8.14921 8.88893 8.3333 8.88893H14.7777C14.9618 8.88893 15.1111 8.73969 15.1111 8.5556V7.44449C15.1111 7.26039 14.9618 7.11115 14.7777 7.11115H8.3333Z"/>
<path d="M7.99997 12.7778C7.99997 12.5937 8.14921 12.4445 8.3333 12.4445H14.7777C14.9618 12.4445 15.1111 12.5937 15.1111 12.7778V13.8889C15.1111 14.073 14.9618 14.2223 14.7777 14.2223H8.3333C8.14921 14.2223 7.99997 14.073 7.99997 13.8889V12.7778Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+3 -1
View File
@@ -7,6 +7,8 @@
"AI Model Configuration": "AI Model Configuration"
},
"training": {
"add_it_here": "Drag the table name on the left to add it here",
"table_relationship_management": "Table relationship management",
"system_anagement": "System Management",
"data_training": "SQL Sample Lib",
"problem_description": "Problem Description",
@@ -669,4 +671,4 @@
"setting_successfully": "Setting Successfully",
"customize_theme_color": "Customize theme color"
}
}
}
+3 -1
View File
@@ -7,6 +7,8 @@
"AI Model Configuration": "模型配置"
},
"training": {
"add_it_here": "拖拽左侧表名,添加到这里",
"table_relationship_management": "表关系管理",
"system_anagement": "系统管理",
"data_training": "SQL 示例库",
"problem_description": "问题描述",
@@ -669,4 +671,4 @@
"setting_successfully": "设置成功",
"customize_theme_color": "自定义主题色"
}
}
}
+4
View File
@@ -514,6 +514,10 @@ const scrollBottom = () => {
if (!isTyping.value && !getRecommendQuestionsLoading.value) {
clearInterval(scrollTime)
}
if (!chatListRef.value) {
clearInterval(scrollTime)
return
}
chatListRef.value!.setScrollTop(innerRef.value!.clientHeight)
}
+121 -5
View File
@@ -8,7 +8,8 @@ import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import edit from '@/assets/svg/icon_edit_outlined.svg'
import { useI18n } from 'vue-i18n'
import ParamsForm from './ParamsForm.vue'
import TableRelationship from '@/views/ds/TableRelationship.vue'
import icon_mindnote_outlined from '@/assets/svg/icon_mindnote_outlined.svg'
interface Table {
name: string
host: string
@@ -54,6 +55,7 @@ const paramsFormRef = ref()
const tableList = ref([] as any[])
const loading = ref(false)
const initLoading = ref(false)
const activeRelationship = ref(false)
const keywords = ref('')
const tableListWithSearch = computed(() => {
if (!keywords.value) return tableList.value
@@ -66,13 +68,29 @@ const showNum = ref(100)
const currentTable = ref<any>({})
const ds = ref<any>({})
const btnSelect = ref('d')
const isDrag = ref(false)
const tableName = ref<any[]>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const handleRelationship = () => {
activeRelationship.value = !activeRelationship.value
currentTable.value = {}
}
const singleDragStartD = (e: DragEvent, ele: any) => {
isDrag.value = true
e.dataTransfer!.setData('table', JSON.stringify(ele))
}
const getTableName = (val: any) => {
tableName.value = val
}
const singleDragEnd = () => {
isDrag.value = false
}
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
@@ -119,6 +137,7 @@ const handleSelectTableList = () => {
}
const clickTable = (table: any) => {
if (activeRelationship.value) return
loading.value = true
currentTable.value = table
fieldList.value = []
@@ -290,8 +309,14 @@ const btnSelectClick = (val: any) => {
<div
v-for="ele in tableListWithSearch"
:key="ele.table_name"
:draggable="activeRelationship && !tableName.includes(ele.id)"
class="model"
:class="currentTable.table_name === ele.table_name && 'isActive'"
@dragstart="($event: any) => singleDragStartD($event, ele)"
@dragend="singleDragEnd"
:class="[
currentTable.table_name === ele.table_name && 'isActive',
tableName.includes(ele.id) && activeRelationship && 'disabled-table',
]"
:title="ele.table_name"
@click="clickTable(ele)"
>
@@ -318,9 +343,32 @@ const btnSelectClick = (val: any) => {
</div>
</div>
</div>
<div class="table-relationship">
<div @click="handleRelationship" :class="activeRelationship && 'active'" class="btn">
<el-icon size="16">
<icon_mindnote_outlined></icon_mindnote_outlined>
</el-icon>
{{ t('training.table_relationship_management') }}
</div>
</div>
</div>
<div v-if="currentTable.table_name" v-loading="loading" class="info-table">
<div v-if="activeRelationship" class="relationship-content">
<div class="title">{{ t('training.table_relationship_management') }}</div>
<div class="content">
<TableRelationship
@getTableName="getTableName"
:dragging="isDrag"
:id="info.id"
></TableRelationship>
</div>
</div>
<div
v-if="currentTable.table_name && !activeRelationship"
v-loading="loading"
class="info-table"
>
<div class="table-name">
<div class="name">{{ currentTable.table_name }}</div>
<div class="notes">
@@ -336,6 +384,7 @@ const btnSelectClick = (val: any) => {
</el-tooltip>
</div>
</div>
<div class="table-content">
<div class="btn-select">
<el-button
@@ -532,6 +581,46 @@ const btnSelectClick = (val: any) => {
padding: 8px 16px;
height: 100%;
border-right: 1px solid #1f232926;
.table-relationship {
height: 56px;
width: 100%;
display: flex;
align-items: center;
margin-top: 20px;
position: relative;
&::after {
content: '';
width: calc(100% + 32px);
position: absolute;
left: -16px;
background-color: #1f232926;
top: 0;
height: 1px;
}
.btn {
width: 248px;
height: 32px;
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
padding-left: 8px;
.ed-icon {
color: #646a73;
margin-right: 8px;
}
&.active {
color: var(--ed-color-primary);
.ed-icon {
color: var(--ed-color-primary);
}
background-color: var(--ed-color-primary-1a);
}
}
}
.select-table_top {
height: 40px;
display: flex;
@@ -551,7 +640,7 @@ const btnSelectClick = (val: any) => {
}
.list-content {
height: calc(100% - 100px);
height: calc(100% - 156px);
.no-result {
margin-top: 72px;
font-weight: 400;
@@ -569,6 +658,12 @@ const btnSelectClick = (val: any) => {
border-radius: 4px;
cursor: pointer;
&.disabled-table {
background: #dee0e3 !important;
color: #646a73;
cursor: not-allowed;
}
.name {
margin-left: 8px;
font-weight: 500;
@@ -604,6 +699,27 @@ const btnSelectClick = (val: any) => {
}
}
}
.relationship-content {
position: absolute;
right: 0;
top: 0;
width: calc(100% - 280px);
height: 100%;
.content {
height: calc(100% - 56px);
width: 100%;
}
.title {
height: 56px;
padding-left: 24px;
line-height: 56px;
font-weight: 500;
font-size: 16px;
border-bottom: 1px solid #1f232926;
}
}
.info-table {
position: absolute;
right: 0;
+404
View File
@@ -0,0 +1,404 @@
<script lang="ts" setup>
import { onMounted, onBeforeUnmount, ref, nextTick } from 'vue'
import { datasourceApi } from '@/api/datasource'
import { useI18n } from 'vue-i18n'
import { Graph, Cell, Shape } from '@antv/x6'
import type { AnyColumn } from 'element-plus-secondary/es/components/table-v2/src/common.mjs'
const LINE_HEIGHT = 36
const NODE_WIDTH = 180
const props = withDefaults(
defineProps<{
id: string
dragging: boolean
}>(),
{
id: '',
dragging: false,
}
)
const emits = defineEmits(['getTableName'])
const { t } = useI18n()
const loading = ref(false)
const nodeIds = ref<any[]>([])
const cells = ref<Cell[]>([])
const edgeOPtion = {
tools: [
{
name: 'button-remove', // 工具名称
args: { x: 20, y: 20 }, // 工具对应的参数
},
],
attrs: {
line: {
stroke: '#DEE0E3',
strokeWidth: 1,
},
},
}
let graph: any
const initGraph = () => {
Graph.registerPortLayout(
'erPortPosition',
(portsPositionArgs) => {
return portsPositionArgs.map((_, index) => {
return {
position: {
x: 0,
y: (index + 1) * LINE_HEIGHT,
},
angle: 0,
}
})
},
true
)
Graph.registerNode(
'er-rect',
{
inherit: 'rect',
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
rect: {
strokeWidth: 1,
stroke: '#F5F6F7',
fill: '#F5F6F7',
},
label: {
fill: '#1F2329',
fontSize: 14,
},
},
ports: {
groups: {
list: {
markup: [
{
tagName: 'rect',
selector: 'portBody',
},
{
tagName: 'text',
selector: 'portNameLabel',
},
],
attrs: {
portBody: {
width: NODE_WIDTH,
height: LINE_HEIGHT,
strokeWidth: 1,
stroke: '#1F232926',
fill: '#ffffff',
magnet: true,
},
portNameLabel: {
ref: 'portBody',
refX: 12,
refY: 8,
fontSize: 14,
textAnchor: 'left',
textWrap: {
width: 150,
height: 24,
ellipsis: true,
},
},
},
position: 'erPortPosition',
},
},
},
},
true
)
graph = new Graph({
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
},
container: document.getElementById('container')!,
autoResize: true,
connecting: {
allowBlank: false,
router: {
name: 'er',
args: {
offset: 25,
direction: 'H',
},
},
validateEdge({ edge }: any) {
const obj = edge.store.data
if (!obj.target.port || obj.target.cell === obj.source.cell) return false
return true
},
createEdge() {
return new Shape.Edge(edgeOPtion)
},
},
})
graph.on('edge:mouseenter', ({ e }: any) => {
Array.from(document.querySelectorAll('.x6-edge-tool')).forEach((ele: any) => {
if (ele.dataset.cellId === e.target.parentNode.dataset.cellId) {
ele.style.display = 'block'
}
})
})
graph.on('edge:mouseleave', ({ e }: any) => {
Array.from(document.querySelectorAll('.x6-edge-tool')).forEach((ele: any) => {
if (ele.dataset.cellId === e.target.parentNode.dataset.cellId) {
ele.style.display = 'none'
}
})
})
graph.on('node:mouseenter', ({ node }: any) => {
node.addTools({
name: 'button',
args: {
markup: [
{
tagName: 'circle',
selector: 'button',
attrs: {
r: 7,
cursor: 'pointer',
},
},
{
tagName: 'path',
selector: 'icon',
attrs: {
d: 'M -3 -3 3 3 M -3 3 3 -3',
stroke: 'white',
'stroke-width': 2,
cursor: 'pointer',
},
},
],
x: 0,
y: 0,
offset: { x: 165, y: 18 },
onClick({ view }: any) {
graph.removeNode(view.cell.id)
nodeIds.value = nodeIds.value.filter((ele) => ele !== view.cell.id)
if (!nodeIds.value.length) {
graph.dispose()
graph = null
}
emits('getTableName', [...nodeIds.value])
},
},
})
})
// 鼠标移开时删除按钮
graph.on('node:mouseleave', ({ node }: any) => {
node.removeTools() // 删除所有的工具
})
}
const getTableData = () => {
loading.value = true
datasourceApi
.relationGet(props.id)
.then((data: any) => {
nodeIds.value = data.filter((ele: any) => ele.shape === 'er-rect').map((ele: any) => ele.id)
nextTick(() => {
if (!graph) {
initGraph()
}
data.forEach((item: any) => {
if (item.shape === 'edge') {
cells.value.push(graph.createEdge({ ...item, ...edgeOPtion }))
} else {
console.log(item)
cells.value.push(
graph.createNode({
...item,
attrs: {
label: {
text: item.attrs.text.text,
textAnchor: 'left',
refX: 10,
textWrap: {
width: 140,
height: 24,
ellipsis: true,
},
},
},
height: LINE_HEIGHT,
width: NODE_WIDTH,
})
)
}
})
graph.resetCells(cells.value)
graph.zoomToFit({ padding: 10, maxScale: 1 })
emits('getTableName', [...nodeIds.value])
})
})
.finally(() => {
loading.value = false
})
}
onMounted(() => {
getTableData()
})
onBeforeUnmount(() => {
graph = null
})
const dragover = () => {
// do
}
const addNode = (node: any) => {
if (!graph) {
initGraph()
}
graph.addNode(graph.createNode({ ...node, height: LINE_HEIGHT, width: NODE_WIDTH }))
}
const clickTable = (table: any) => {
loading.value = true
datasourceApi
.fieldList(table.id)
.then((res: AnyColumn) => {
const node = {
id: table.id,
shape: 'er-rect',
label: table.table_name,
width: 150,
height: 24,
position: {
x: table.x,
y: table.y,
},
ports: res.map((ele: any) => {
return {
id: ele.id,
group: 'list',
attrs: {
portNameLabel: {
text: ele.field_name,
},
portTypeLabel: {
text: ele.field_type,
},
},
}
}),
}
nodeIds.value = [...nodeIds.value, table.id]
nextTick(() => {
addNode(node)
})
emits('getTableName', [...nodeIds.value])
})
.finally(() => {
loading.value = false
})
}
const drop = (e: any) => {
const obj = JSON.parse(e.dataTransfer.getData('table') || '{}')
if (!obj.id) return
clickTable({ ...obj, x: e.layerX, y: e.layerY })
}
const save = () => {
datasourceApi.relationSave(props.id, graph.toJSON().cells).then(() => {
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
</script>
<template>
<div v-loading="loading" v-if="!nodeIds.length" class="relationship-empty">
{{ t('training.add_it_here') }}
</div>
<div v-loading="loading" v-else id="container"></div>
<div
@dragover.prevent.stop="dragover"
@drop.prevent.stop="drop"
v-show="dragging"
class="drag-mask"
></div>
<div class="save-btn">
<el-button type="primary" v-if="nodeIds.length" @click="save">
{{ t('common.save') }}
</el-button>
</div>
</template>
<style lang="less" scoped>
.save-btn {
position: absolute;
right: 16px;
bottom: 16px;
}
.drag-mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 56px;
z-index: 10;
}
.relationship-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 16px;
}
#container {
font-size: 14px;
user-select: text;
overflow: hidden;
outline: none;
touch-action: none;
box-sizing: border-box;
position: relative;
min-width: 400px;
min-height: 600px;
width: 100%;
height: 100%;
:deep(.x6-edge-tool) {
display: none;
circle {
fill: var(--ed-color-primary) !important;
}
}
:deep(.x6-node-tool) {
circle {
fill: var(--ed-color-primary) !important;
}
}
}
</style>