<script lang="ts" setup>
|
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
|
import icon_rename_outlined from '@/assets/svg/icon_rename_outlined.svg'
|
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
|
import icon_textBox_outlined from '@/assets/svg/icon_text-box_outlined.svg'
|
import icon_fullAssociation from '@/assets/svg/icon_full-association.svg'
|
import icon_intersect from '@/assets/svg/icon_intersect.svg'
|
import icon_leftAssociation from '@/assets/svg/icon_left-association.svg'
|
import icon_rightAssociation from '@/assets/svg/icon_right-association.svg'
|
import icon_sql_outlined from '@/assets/svg/icon_sql_outlined.svg'
|
import { getCSSVariable } from '@/utils/color'
|
import referenceTable from '@/assets/svg/reference-table.svg'
|
import icon_moreVertical_outlined from '@/assets/svg/icon_more-vertical_outlined.svg'
|
import { reactive, computed, ref, nextTick, inject, type Ref, watch, unref } from 'vue'
|
import AddSql from './AddSql.vue'
|
import { useI18n } from '@/hooks/web/useI18n'
|
import zeroNodeImg from '@/assets/img/drag.png'
|
import { ElMessageBox, type Action } from 'element-plus-secondary'
|
import { guid } from './util'
|
import { HandleMore } from '@/components/handle-more'
|
import { propTypes } from '@/utils/propTypes'
|
import UnionFieldList from './UnionFieldList.vue'
|
import { type Node, type Field, num } from './util'
|
import { getTableField } from '@/api/dataset'
|
import type { SqlNode } from './AddSql.vue'
|
import { cloneDeep } from 'lodash-es'
|
import type { Table } from '@/api/dataset'
|
import { useAppearanceStoreWithOut } from '@/store/modules/appearance'
|
const appearanceStore = useAppearanceStoreWithOut()
|
const state = reactive({
|
nodeList: [],
|
visualNode: null,
|
visualNodeParent: null,
|
visualPath: null
|
})
|
const props = defineProps({
|
maskShow: propTypes.bool.def(false),
|
offsetX: propTypes.number.def(0),
|
offsetY: propTypes.number.def(0),
|
dragHeight: propTypes.number.def(260),
|
getDsName: propTypes.func
|
})
|
|
const primaryColor = computed(() => {
|
return appearanceStore.themeColor === 'custom' ? appearanceStore.customColor : getCSSVariable()
|
})
|
const isCross = inject<Ref>('isCross')
|
|
const iconName = {
|
left: icon_leftAssociation,
|
right: icon_rightAssociation,
|
inner: icon_intersect,
|
full: icon_fullAssociation
|
}
|
const { t } = useI18n()
|
const editSqlField = ref(false)
|
|
const nodeField = ref<Field[]>([])
|
|
const currentNode = ref<Node>()
|
|
const sqlNode = ref<SqlNode>()
|
|
const allfields = inject('allfields') as Ref
|
|
const getNodeField = ({ datasourceId, id, info, tableName, type, currentDsFields }) => {
|
return getTableField({ datasourceId, id, info, tableName, type, isCross: isCross.value })
|
.then(res => {
|
const idOriginNameMap = allfields.value.reduce((pre, next) => {
|
pre[`${next.datasetTableId}${next.originName}`] = next.id
|
return pre
|
}, {})
|
nodeField.value = res as unknown as Field[]
|
nodeField.value.forEach(ele => {
|
ele.id = idOriginNameMap[`${id}${ele.originName}`]
|
ele.checked = currentDsFields.map(ele => ele.originName).includes(ele.originName)
|
})
|
})
|
.finally(() => {
|
editUnion.value = true
|
})
|
}
|
|
const nodeNameList = computed(() => {
|
const arr = []
|
dfsNodeNameList(state.nodeList, arr)
|
return arr
|
})
|
|
const dragMask = ref()
|
|
const handleClickOutside = ev => {
|
if (ev.target === dragMask.value) {
|
activeNodeId.value = ''
|
}
|
}
|
|
const activeNodeId = ref('')
|
|
let shadowWidth = 0
|
let shadowHeight = 0
|
|
const setLayOut = () => {
|
svgWidth.value = `${shadowWidth * 100 + (shadowWidth + 1) * 200 + 48}px`
|
svgHeight.value = `${shadowHeight * 24 + (shadowHeight + 1) * 32 + 48}px`
|
}
|
const dfsNodeList = computed(() => {
|
let nodeListLocation = []
|
shadowWidth = 0
|
shadowHeight = 0
|
dfsNode(state.nodeList, nodeListLocation)
|
setLayOut()
|
return nodeListLocation
|
})
|
|
const dfsNodeNameList = (list, arr) => {
|
list.forEach(ele => {
|
arr.push(`${ele.tableName}${ele.datasourceId}`)
|
if (ele.children?.length) {
|
dfsNodeNameList(ele.children, arr)
|
}
|
})
|
}
|
|
const dfsForDsId = (arr, datasourceId) => {
|
return arr.every(ele => {
|
if (ele.children?.length) {
|
return dfsForDsId(ele.children, datasourceId)
|
}
|
if (!ele.datasourceId) {
|
return true
|
}
|
return ele.datasourceId === datasourceId
|
})
|
}
|
|
const crossDatasources = computed(() => {
|
const { datasourceId, children = [] } = state.nodeList[0] || {}
|
if (datasourceId && !!children.length) {
|
return !dfsForDsId(children, datasourceId)
|
}
|
return false
|
})
|
|
let isUpdate = false
|
|
watch(
|
() => state.nodeList,
|
() => {
|
if (isUpdate) {
|
emits('changeUpdate')
|
}
|
},
|
{ deep: true }
|
)
|
|
const initState = nodeList => {
|
Object.assign(state.nodeList, nodeList)
|
nextTick(() => {
|
isUpdate = true
|
emits('addComplete')
|
})
|
}
|
|
const editUnion = ref(false)
|
|
const svgWidth = ref('200px')
|
const svgHeight = ref('260px')
|
|
const delNode = (id, arr) => {
|
arr.some((ele, index) => {
|
if (id === ele.id) {
|
arr.splice(index, 1)
|
return true
|
}
|
if (ele.children?.length) {
|
delNode(id, ele.children)
|
}
|
return false
|
})
|
}
|
let fakeDelId = []
|
|
const collectId = arr => {
|
arr.forEach(ele => {
|
fakeDelId = [...fakeDelId, ...ele.currentDsFields.map(itx => itx.id)]
|
if (ele.children?.length) {
|
collectId(ele.children)
|
}
|
})
|
}
|
|
const delNodeFake = (id, arr) => {
|
arr.forEach(ele => {
|
if (id === ele.id) {
|
fakeDelId = [...ele.currentDsFields.map(itx => itx.id)]
|
if (ele.children?.length) {
|
collectId(ele.children)
|
}
|
} else if (ele.children?.length) {
|
delNodeFake(id, ele.children)
|
}
|
})
|
}
|
|
const changeSqlId = ref([])
|
const changedNodeId = ref([])
|
const saveSqlNode = (val: SqlNode, cb) => {
|
const { tableName, id, sql, datasourceId, sqlVariableDetails = null, changeFlag = false } = val
|
if (changeFlag) {
|
changedNodeId.value = changedNodeId.value.filter(itx => itx.from !== id && id !== itx.to)
|
!changeSqlId.value.includes(id) && changeSqlId.value.push(id)
|
}
|
if (state.visualNode) {
|
Object.assign(state.visualNode, {
|
info: JSON.stringify({ table: tableName, sql }),
|
sqlVariableDetails,
|
unionType: 'left',
|
type: 'sql',
|
id,
|
datasourceId,
|
unionFields: [],
|
currentDsFields: []
|
})
|
if (!state.nodeList.length) {
|
state.visualNode.tableName = tableName
|
getTableField({
|
datasourceId,
|
id: id,
|
info: state.visualNode.info,
|
tableName,
|
type: 'sql',
|
isCross: isCross.value
|
}).then(res => {
|
state.visualNode.confirm = true
|
state.nodeList.push(state.visualNode)
|
currentNode.value = state.nodeList[0]
|
nodeField.value = res as unknown as Field[]
|
nodeField.value.forEach(ele => {
|
ele.checked = true
|
})
|
state.nodeList[0].currentDsFields = cloneDeep(res)
|
cb?.()
|
confirmEditUnion()
|
confirm()
|
})
|
} else {
|
getTableField({
|
datasourceId,
|
id: id,
|
info: state.visualNode.info,
|
tableName,
|
type: 'sql',
|
isCross: isCross.value
|
}).then(() => {
|
state.visualNode.confirm = true
|
cb?.()
|
})
|
}
|
return
|
}
|
const obj = {
|
info: JSON.stringify({ table: tableName, sql }),
|
id,
|
datasourceId,
|
tableName,
|
sqlVariableDetails
|
}
|
dfsNodeBack([obj], [id], state.nodeList)
|
cb?.()
|
emits('reGetName')
|
}
|
|
const setChangeStatus = (to, from) => {
|
if (changedNodeId.value.some(ele => ele.from === from && ele.to === to)) return
|
changedNodeId.value.push({ from, to })
|
}
|
|
const closeSqlNode = () => {
|
if (
|
state.nodeList.length === 1 &&
|
!state.nodeList[0].children?.length &&
|
changeSqlId.value.length === 1
|
) {
|
currentNode.value = state.nodeList[0]
|
const { datasourceId, id, info, tableName, sqlVariableDetails } = currentNode.value
|
getTableField({
|
datasourceId,
|
id,
|
info,
|
tableName,
|
type: 'sql',
|
isCross: isCross.value,
|
sqlVariableDetails: sqlVariableDetails
|
}).then(res => {
|
const idOriginNameMap = allfields.value.reduce((pre, next) => {
|
pre[`${next.datasetTableId}${next.originName}`] = next.id
|
return pre
|
}, {})
|
nodeField.value = res as unknown as Field[]
|
nodeField.value.forEach(ele => {
|
ele.id = idOriginNameMap[`${id}${ele.originName}`]
|
ele.checked = true
|
})
|
state.nodeList[0].currentDsFields = cloneDeep(res)
|
editUnion.value = true
|
})
|
changeSqlId.value = []
|
}
|
if (state.visualNode?.confirm) {
|
nextTick(() => {
|
emits('joinEditor', [
|
{
|
...state.visualNode,
|
tableName: (JSON.parse(state.visualNode.info) as { table: string }).table
|
},
|
state.visualNodeParent
|
])
|
})
|
editSqlField.value = false
|
return
|
}
|
editSqlField.value = false
|
if (!state.visualNodeParent) {
|
state.visualNode = null
|
return
|
}
|
state.visualNodeParent.children = state.visualNodeParent.children.filter(ele => !ele.isShadow)
|
confirm()
|
}
|
|
const changeNodeFields = val => {
|
currentNode.value.currentDsFields = val
|
}
|
|
const closeEditUnion = () => {
|
nodeField.value = []
|
currentNode.value = null
|
const [fir] = state.nodeList
|
if (fir.isShadow) {
|
delete fir.isShadow
|
state.nodeList = []
|
emits('addComplete')
|
}
|
editUnion.value = false
|
}
|
|
const setGuid = (arr, id, datasourceId) => {
|
arr.forEach(ele => {
|
if (!ele.id) {
|
ele.id = `${++num.value}`
|
ele.datasetTableId = id
|
ele.datasourceId = datasourceId
|
}
|
})
|
}
|
|
const delUpdateDsFields = (id, arr: Node[]) => {
|
arr.some(ele => {
|
if (id === ele.id) {
|
setGuid(currentNode.value.currentDsFields, ele.id, ele.datasourceId)
|
ele.currentDsFields = currentNode.value.currentDsFields
|
return true
|
}
|
if (ele.children?.length) {
|
delUpdateDsFields(id, ele.children)
|
}
|
return false
|
})
|
}
|
const delUpdateDsFieldsFake = (id, arr: Node[]) => {
|
arr.forEach(ele => {
|
if (id === ele.id) {
|
fakeDelId = [...ele.currentDsFields.map(itx => itx.id)]
|
}
|
if (ele.children?.length) {
|
delUpdateDsFieldsFake(id, ele.children)
|
}
|
})
|
}
|
|
const confirmEditUnion = () => {
|
delUpdateDsFieldsFake(currentNode.value.id, state.nodeList)
|
const currentIds = currentNode.value.currentDsFields.map(ele => ele.id)
|
let ids = fakeDelId.filter(ele => !currentIds.includes(ele))
|
fakeDelId = []
|
if (!!ids.length) {
|
const idArr = allfields.value.reduce((pre, next) => {
|
if (next.extField === 2) {
|
let idMap = next.originName.match(/\[(.+?)\]/g) || []
|
idMap = idMap.filter(
|
itx => !next.params?.map(element => element.id).includes(itx.slice(1, -1))
|
)
|
const result = idMap.map(itm => {
|
return itm.slice(1, -1)
|
})
|
result.forEach(ele => {
|
if (ids.includes(ele)) {
|
pre.push(ele)
|
}
|
})
|
}
|
return pre
|
}, [])
|
|
if (!!idArr.length) {
|
ElMessageBox.confirm(t('data_set.field_selection'), {
|
confirmButtonText: t('dataset.confirm'),
|
cancelButtonText: t('common.cancel'),
|
showCancelButton: true,
|
tip: `${t('data_set.field')}: ${allfields.value
|
.filter(ele => [...new Set(idArr)].includes(ele.id) && ele.extField !== 2)
|
.map(ele => ele.name)
|
.join(',')}, ${t('data_set.confirm_the_deletion')}`,
|
confirmButtonType: 'danger',
|
type: 'warning',
|
autofocus: false,
|
showClose: false,
|
callback: (action: Action) => {
|
if (action === 'confirm') {
|
delUpdateDsFields(currentNode.value.id, state.nodeList)
|
const [fir] = state.nodeList
|
if (fir.isShadow) {
|
delete fir.isShadow
|
}
|
closeEditUnion()
|
nextTick(() => {
|
emits('updateAllfields')
|
})
|
}
|
}
|
})
|
return
|
}
|
}
|
|
delUpdateDsFields(currentNode.value.id, state.nodeList)
|
const [fir] = state.nodeList
|
if (fir.isShadow) {
|
delete fir.isShadow
|
}
|
closeEditUnion()
|
nextTick(() => {
|
emits('updateAllfields')
|
})
|
}
|
|
const handleCommand = (ele, command) => {
|
if (command === 'editorField') {
|
getNodeField(ele)
|
currentNode.value = cloneDeep(ele)
|
}
|
|
if (command === 'rename') {
|
tableRename({ name: ele.tableName, id: ele.id })
|
}
|
|
if (command === 'editorSql') {
|
const { tableName, datasourceId, info, id, sqlVariableDetails } = ele
|
if (ele.type === 'sql') {
|
sqlNode.value = {
|
sql: ((JSON.parse(info) as { sql: string }) || {}).sql,
|
tableName,
|
id,
|
variables: JSON.parse(sqlVariableDetails),
|
datasourceId
|
}
|
editSqlField.value = true
|
return
|
}
|
}
|
|
if (command === 'del') {
|
delNodeFake(ele.id, state.nodeList)
|
if (!!fakeDelId.length) {
|
const idArr = allfields.value.reduce((pre, next) => {
|
if (next.extField === 2) {
|
const idMap = next.originName.match(/\[(.+?)\]/g) || []
|
const result = idMap.map(itm => {
|
return itm.slice(1, -1)
|
})
|
result.forEach(ele => {
|
if (fakeDelId.includes(ele)) {
|
pre.push(ele)
|
}
|
})
|
}
|
return pre
|
}, [])
|
fakeDelId = []
|
if (!!idArr.length) {
|
ElMessageBox.confirm(t('data_set.confirm_to_delete', { a: ele.tableName }), {
|
confirmButtonText: t('dataset.confirm'),
|
cancelButtonText: t('common.cancel'),
|
showCancelButton: true,
|
tip: t('data_set.also_be_deleted'),
|
confirmButtonType: 'danger',
|
type: 'warning',
|
autofocus: false,
|
showClose: false,
|
callback: (action: Action) => {
|
if (action === 'confirm') {
|
delNode(ele.id, state.nodeList)
|
nextTick(() => {
|
emits('addComplete')
|
emits('updateAllfields')
|
})
|
}
|
}
|
})
|
return
|
}
|
}
|
|
delNode(ele.id, state.nodeList)
|
nextTick(() => {
|
emits('addComplete')
|
emits('updateAllfields')
|
})
|
}
|
}
|
|
const handlePathClick = ele => {
|
const { from, to } = ele
|
const arr = []
|
const idArr = [from.id, to.id]
|
dfsPath(arr, idArr, state.nodeList)
|
emits('joinEditor', arr)
|
}
|
|
const dfsPath = (arr, idArr, list) => {
|
list.forEach(ele => {
|
if (idArr.includes(ele.id)) {
|
arr.unshift(ele)
|
}
|
if (ele.children?.length) {
|
dfsPath(arr, idArr, ele.children)
|
}
|
})
|
}
|
|
const dfsNodeBack = (arr, idArr, list) => {
|
list.forEach(ele => {
|
if (idArr.includes(ele.id)) {
|
idArr.shift()
|
const node = arr.shift()
|
Object.assign(ele, node)
|
}
|
if (ele.children?.length) {
|
dfsNodeBack(arr, idArr, ele.children)
|
}
|
})
|
}
|
|
const dfsNodeFieldBack = (list, { originName, datasetTableId }) => {
|
list.forEach(ele => {
|
if (datasetTableId === ele.id) {
|
const currentDsFields = ele.currentDsFields.filter(ele => ele.originName !== originName)
|
ele.currentDsFields = currentDsFields
|
}
|
if (ele.children?.length) {
|
dfsNodeFieldBack(ele.children, { originName, datasetTableId })
|
}
|
})
|
}
|
|
const dfsNodeFieldBackReal = ele => {
|
dfsNodeFieldBack(state.nodeList, ele)
|
}
|
|
const menuList = [
|
{
|
svgName: icon_textBox_outlined,
|
label: t('data_set.field_selection'),
|
command: 'editorField'
|
},
|
{
|
svgName: icon_deleteTrash_outlined,
|
label: t('data_set.delete'),
|
command: 'del'
|
}
|
]
|
|
const sqlMenu = [
|
{
|
svgName: icon_edit_outlined,
|
label: t('data_set.edit_sql'),
|
command: 'editorSql'
|
},
|
{
|
svgName: icon_rename_outlined,
|
label: t('datasource.field_rename'),
|
command: 'rename'
|
}
|
]
|
|
const dragOffsetX = ref(0)
|
const dragOffsetY = ref(0)
|
|
function elementInteractArea(pos1, pos2) {
|
const pos1Width = pos1.right - pos1.left
|
const pos1Height = pos1.bottom - pos1.top
|
const pos2Width = pos2.right - pos2.left
|
const pos2Height = pos2.bottom - pos2.top
|
|
const axisOverlap =
|
pos1Width + pos2Width - (Math.max(pos1.right, pos2.right) - Math.min(pos1.left, pos2.left))
|
const crossOverlap =
|
pos1Height + pos2Height - (Math.max(pos1.bottom, pos2.bottom) - Math.min(pos1.top, pos2.top))
|
if (axisOverlap <= 0 || crossOverlap <= 0) {
|
return 0
|
}
|
return axisOverlap * crossOverlap
|
}
|
|
const possibleNodeAreaList = computed(() => {
|
let flatArr = []
|
leafNode(dfsNodeList.value, flatArr)
|
return flatArr.filter(ele => !ele.isShadow)
|
})
|
|
const leafNode = (arr, leafList) => {
|
arr.forEach((ele, index) => {
|
const fromX = ele.x * 300 + 24
|
const fromY = ele.y * 56 + 24
|
let toX = fromX + 200
|
let toY = fromY + 56
|
const next = arr[index + 1]
|
if (next) {
|
toY = next.y * 56 + 24
|
}
|
if (ele.children?.length) {
|
leafNode(ele.children, leafList)
|
}
|
if (!!ele.x || (!!ele.y && !!ele.x)) {
|
leafList.push({
|
...ele,
|
isShadow: ele.isShadow,
|
isLeaf: !ele.children?.length,
|
fromX,
|
fromY,
|
toX,
|
toY
|
})
|
}
|
})
|
}
|
|
const flatNodeList = computed(() => {
|
let flatArr = []
|
flatNode(dfsNodeList.value, flatArr)
|
return flatArr
|
})
|
|
const flatNode = (arr, flatNodeList) => {
|
arr.forEach(ele => {
|
flatNodeList.push(ele)
|
if (ele.children?.length) {
|
flatNode(ele.children, flatNodeList)
|
}
|
})
|
}
|
|
const flatPathList = computed(() => {
|
let flatArr = []
|
const [root = {}] = dfsNodeList.value
|
flatLine(root, flatArr)
|
return flatArr
|
})
|
|
const dfsNode = (arr = [], nodeListLocation, x = 0, y = 0) => {
|
arr.map((ele, index) => {
|
const pre = nodeListLocation[index - 1]
|
if (!ele.children?.length) {
|
let idxChild = index + y
|
let maxY = 0
|
if (pre) {
|
const last = pre.children?.length
|
? pre.children[pre.children.length - 1]
|
: { y: 0, maxY: 0 }
|
idxChild = Math.max(last.y, pre.y) + 1
|
maxY += idxChild
|
idxChild = Math.max(idxChild, last.maxY)
|
}
|
nodeListLocation.push({
|
...ele,
|
x,
|
y: idxChild,
|
maxY,
|
isShadow: !!ele.isShadow
|
})
|
shadowHeight = Math.max(idxChild, shadowHeight)
|
shadowWidth = Math.max(x, shadowWidth)
|
} else {
|
const children = []
|
const pre = nodeListLocation[index - 1]
|
let idx = y
|
let maxY = 0
|
if (pre) {
|
const last = pre.children?.length
|
? pre.children[pre.children.length - 1]
|
: { y: 0, maxY: 0 }
|
idx = Math.max(last.y, pre.y) + 1
|
maxY = last.maxY
|
}
|
dfsNode(ele.children, children, x + 1, idx ? Math.max(idx, maxY) : idx)
|
maxY = Math.max(children[children.length - 1].maxY + 1, maxY)
|
|
nodeListLocation.push({
|
...ele,
|
x,
|
y: children[0].y,
|
maxY,
|
isShadow: !!ele.isShadow,
|
children
|
})
|
shadowHeight = Math.max(children[0].y, shadowHeight)
|
shadowWidth = Math.max(x, shadowWidth)
|
}
|
})
|
}
|
|
const dfsNodeShadow = (arr, { tableName, id }, position, parent) => {
|
return arr.some((ele, index) => {
|
if (ele.tableName === tableName && id === ele.id) {
|
const flag = tableName + '_&&' + position
|
if (ele.isShadow && state.visualNode.flag === flag) return true
|
state.visualNode = {
|
tableName: '',
|
isShadow: true,
|
flag
|
}
|
|
if (position === 'b') {
|
state.visualNodeParent = parent
|
arr.splice(index + 1, 0, state.visualNode)
|
} else {
|
state.visualNodeParent = ele
|
ele.children = [state.visualNode]
|
}
|
return true
|
}
|
if (ele.children?.length) {
|
return dfsNodeShadow(ele.children, { tableName, id }, position, ele)
|
}
|
return false
|
})
|
}
|
|
const flatLine = (item, flatNodeList) => {
|
let sqlChangeFlag = changeSqlId.value.includes(item.id)
|
if (item.children?.length) {
|
sqlChangeFlag = item.children.some(itx => changeSqlId.value.includes(itx.id)) || sqlChangeFlag
|
}
|
const from = { ...item, d: '' }
|
;(item.children || []).forEach(ele => {
|
let localSqlChangeFlag = true
|
changedNodeId.value.some(element => {
|
if (
|
(element.from === item.id && ele.id === element.to) ||
|
(element.from === ele.id && item.id === element.to)
|
) {
|
localSqlChangeFlag = false
|
return true
|
}
|
return false
|
})
|
flatNodeList.push({
|
from,
|
sqlChangeFlag: localSqlChangeFlag && sqlChangeFlag,
|
isShadow: ele.isShadow || item.isShadow,
|
to: {
|
...ele
|
},
|
d:
|
ele.y === from.y
|
? `M ${item.x * 300 + 224} ${ele.y * 56 + 40} l 100 0`
|
: `M ${item.x * 300 + 240} ${from.y * 56 + 40} l 0 ${(ele.y - from.y) * 56} l 84 0`
|
})
|
if (ele.children?.length) {
|
flatLine(ele, flatNodeList)
|
}
|
})
|
}
|
|
const dragleave_handler = ev => {
|
ev.preventDefault()
|
notConfirm()
|
}
|
|
const dragover_handler = ev => {
|
ev.preventDefault()
|
|
dragOffsetX.value = ev.offsetX - props.offsetX
|
dragOffsetY.value = ev.offsetY - props.offsetY
|
|
const lg = state.nodeList.length
|
const [fir] = state.nodeList
|
|
if (!lg) return
|
|
const [obj] = fir.children || []
|
|
if (!fir.children?.length || obj?.isShadow) {
|
if (obj?.isShadow) return
|
state.visualNode = {
|
tableName: '',
|
isShadow: true,
|
flag: '_&&'
|
}
|
|
state.nodeList[0].children = [state.visualNode]
|
state.visualNodeParent = state.nodeList[0]
|
return
|
}
|
|
let resultList = possibleNodeAreaList.value.map(ele => {
|
const { fromX, fromY, toX, toY, isLeaf = false, tableName } = ele
|
return [
|
elementInteractArea(
|
{
|
left: dragOffsetX.value,
|
right: dragOffsetX.value + 200,
|
top: dragOffsetY.value,
|
bottom: dragOffsetY.value + 32
|
},
|
{
|
left: fromX,
|
right: toX,
|
top: fromY,
|
bottom: toY
|
}
|
),
|
isLeaf || state.visualNode?.flag === tableName + '_&&r'
|
? elementInteractArea(
|
{
|
left: dragOffsetX.value,
|
right: dragOffsetX.value + 200,
|
top: dragOffsetY.value,
|
bottom: dragOffsetY.value + 32
|
},
|
{
|
left: fromX + 200,
|
right: toX + 100,
|
top: fromY,
|
bottom: fromY + 32
|
}
|
)
|
: 0
|
]
|
})
|
|
let maxIndex = 0
|
|
resultList.reduce((pre, next, idx) => {
|
const max = Math.max(...pre) > Math.max(...next)
|
maxIndex = max ? maxIndex : idx
|
return max ? pre : next
|
})
|
|
let maxArr = resultList[maxIndex]
|
|
if (Array.isArray(state.visualNodeParent?.children)) {
|
const shadowIndex = state.visualNodeParent.children.findIndex(ele => ele.isShadow)
|
if (shadowIndex > -1) {
|
state.visualNodeParent.children.splice(shadowIndex, 1)
|
}
|
}
|
state.visualNode = null
|
|
if (Math.max(...maxArr)) {
|
const { tableName, isShadow = false, id } = possibleNodeAreaList.value[maxIndex]
|
const [b, r] = maxArr
|
if (!isShadow) {
|
dfsNodeShadow(state.nodeList, { tableName, id }, b >= r ? 'b' : 'r', state.nodeList[0])
|
}
|
}
|
}
|
|
const dragenter_handler = ev => {
|
ev.preventDefault()
|
}
|
|
const drop_handler = ev => {
|
ev.preventDefault()
|
let data = ev.dataTransfer.getData('text')
|
const { tableName, type, datasourceId, name: noteName } = JSON.parse(data) as Table
|
const extraData = {
|
info: JSON.stringify({
|
table: tableName,
|
sql: ''
|
}),
|
noteName,
|
unionType: 'left',
|
unionFields: [],
|
currentDsFields: [],
|
sqlVariableDetails: null
|
}
|
|
if (!state.nodeList.length) {
|
if (type === 'sql') {
|
state.visualNode = {
|
tableName,
|
type,
|
datasourceId,
|
id: guid(),
|
...extraData
|
}
|
sqlNode.value = {
|
sql: '',
|
tableName,
|
id: state.visualNode.id,
|
datasourceId
|
}
|
editSqlField.value = true
|
return
|
}
|
state.nodeList.push({
|
tableName,
|
type,
|
isShadow: true,
|
datasourceId,
|
id: guid(),
|
...extraData
|
})
|
|
currentNode.value = state.nodeList[0]
|
|
getTableField({
|
datasourceId,
|
id: currentNode.value.id,
|
info: currentNode.value.info,
|
tableName,
|
isCross: isCross.value,
|
type
|
})
|
.then(res => {
|
nodeField.value = res as unknown as Field[]
|
nodeField.value.forEach(ele => {
|
ele.checked = true
|
})
|
state.nodeList[0].currentDsFields = cloneDeep(res)
|
})
|
.finally(() => {
|
editUnion.value = true
|
})
|
nextTick(() => {
|
emits('addComplete')
|
})
|
return
|
}
|
|
nextTick(() => {
|
emits('addComplete')
|
})
|
|
if (!state.visualNode) return
|
if (type === 'sql') {
|
sqlNode.value = {
|
sql: '',
|
tableName,
|
id: guid(),
|
datasourceId
|
}
|
editSqlField.value = true
|
return
|
}
|
|
nextTick(() => {
|
Object.assign(state.visualNode, {
|
tableName,
|
type,
|
datasourceId,
|
id: guid(),
|
...extraData
|
})
|
emits('joinEditor', [
|
{
|
tableName,
|
type,
|
datasourceId,
|
id: state.visualNode.id,
|
...extraData
|
},
|
state.visualNodeParent
|
])
|
})
|
}
|
|
const setStateBack = (node, parent) => {
|
delete parent.children
|
delete node.children
|
dfsNodeBack([parent, node], [parent.id, node.id], state.nodeList)
|
if (state.visualNode) {
|
confirm()
|
}
|
}
|
|
const dialogRename = ref(false)
|
const renameForm = ref()
|
const defaultParam = {
|
name: '',
|
id: ''
|
}
|
const dfsNodeListRename = arr => {
|
arr.some(ele => {
|
if (ele.id === renameParam.id && ele.tableName !== renameParam.name) {
|
ele.tableName = renameParam.name
|
return true
|
}
|
|
if (!!ele.children?.length) {
|
dfsNodeListRename(ele.children)
|
}
|
})
|
}
|
const renameParam = reactive(cloneDeep(defaultParam))
|
const confirmRename = () => {
|
renameForm.value.validate(val => {
|
if (val) {
|
dfsNodeListRename(state.nodeList)
|
renameParam.name = ''
|
renameParam.id = ''
|
dialogRename.value = false
|
emits('reGetName')
|
}
|
})
|
}
|
const tableRename = ({ name, id }) => {
|
renameParam.name = name
|
renameParam.id = id
|
dialogRename.value = true
|
}
|
const confirm = () => {
|
state.visualNode.isShadow = false
|
delete state.visualNode.flag
|
state.visualNode = null
|
state.visualNodeParent = null
|
}
|
|
const notConfirm = () => {
|
if (!state.visualNodeParent) return
|
state.visualNodeParent.children = state.visualNodeParent.children.filter(ele => !ele.isShadow)
|
confirm()
|
}
|
|
const getNodeList = () => {
|
return cloneDeep(unref(state.nodeList))
|
}
|
|
defineExpose({
|
nodeNameList,
|
getNodeList,
|
setStateBack,
|
notConfirm,
|
dfsNodeFieldBackReal,
|
initState,
|
setChangeStatus,
|
crossDatasources
|
})
|
|
const handleActiveNode = ele => {
|
activeNodeId.value = ele.id
|
handleCommand(ele, 'editorField')
|
}
|
|
const emits = defineEmits([
|
'addComplete',
|
'joinEditor',
|
'updateAllfields',
|
'changeUpdate',
|
'reGetName'
|
])
|
</script>
|
|
<template>
|
<div
|
@drop="$event => drop_handler($event)"
|
@dragenter="$event => dragenter_handler($event)"
|
@dragover="$event => dragover_handler($event)"
|
@dragleave="$event => dragleave_handler($event)"
|
class="drag-mask_dataset"
|
ref="dragMask"
|
@click="handleClickOutside"
|
:style="{ height: dragHeight + 'px' }"
|
>
|
<svg
|
version="1.1"
|
baseProfile="full"
|
:width="svgWidth"
|
:height="svgHeight"
|
xmlns="http://www.w3.org/2000/svg"
|
>
|
<path
|
:key="ele.d"
|
class="path-point"
|
v-for="ele in flatPathList"
|
:d="ele.d"
|
:stroke-dasharray="ele.isShadow ? '4,4' : '0'"
|
:stroke="ele.isShadow ? primaryColor : '#BBBFC4'"
|
stroke-width="1"
|
fill="none"
|
/>
|
<foreignObject
|
:key="ele.tableName"
|
v-for="ele in flatNodeList"
|
:x="ele.x * 300 + 24"
|
:y="ele.y * 56 + 24"
|
width="200"
|
height="32"
|
>
|
<div
|
@click="handleActiveNode(ele)"
|
class="node-union"
|
ref="activeNode"
|
:class="[
|
{
|
'shadow-node': ele.isShadow,
|
'active-node': activeNodeId === ele.id
|
}
|
]"
|
>
|
<el-icon>
|
<Icon
|
><component
|
class="svg-icon"
|
:is="ele.type !== 'sql' ? referenceTable : icon_sql_outlined"
|
></component
|
></Icon>
|
</el-icon>
|
<span class="tableName">{{ ele.tableName }}</span>
|
<span class="placeholder">{{ t('data_set.custom_sql_here') }}</span>
|
<handle-more
|
style="margin-left: auto"
|
:iconName="icon_moreVertical_outlined"
|
:menuList="ele.type === 'sql' ? [...sqlMenu, ...menuList] : menuList"
|
@handle-command="command => handleCommand(ele, command)"
|
></handle-more>
|
</div>
|
</foreignObject>
|
|
<foreignObject
|
:key="ele.d"
|
v-for="ele in flatPathList"
|
:x="ele.from.x * 300 + 272"
|
:y="ele.to.y * 56 + 24"
|
width="32"
|
height="32"
|
>
|
<div
|
v-if="!ele.isShadow"
|
@click="handlePathClick(ele)"
|
class="path-union"
|
:style="{ borderColor: ele.sqlChangeFlag ? '#F54A45' : '' }"
|
>
|
<el-icon>
|
<Icon><component class="svg-icon" :is="iconName[ele.to.unionType]"></component></Icon>
|
</el-icon>
|
</div>
|
</foreignObject>
|
</svg>
|
<div
|
class="mask-dataset"
|
:class="[
|
{
|
'mask-dataset-none': !state.nodeList.length
|
}
|
]"
|
v-if="maskShow"
|
></div>
|
<div class="zero" v-if="!state.nodeList.length">
|
<img :src="zeroNodeImg" alt="" />
|
<p>{{ t('data_set.on_the_left') }}</p>
|
<p>{{ t('data_set.a_data_set') }}</p>
|
</div>
|
</div>
|
<el-dialog
|
v-model="dialogRename"
|
:close-on-press-escape="false"
|
:close-on-click-modal="false"
|
:title="t('data_set.rename_table')"
|
width="420px"
|
>
|
<el-form
|
ref="renameForm"
|
require-asterisk-position="right"
|
:model="renameParam"
|
label-position="top"
|
>
|
<el-form-item
|
prop="name"
|
:label="t('data_set.table_name')"
|
:rules="[
|
{
|
required: true,
|
message: t('commons.cannot_be_null')
|
}
|
]"
|
>
|
<el-input :placeholder="t('common.inputText')" v-model="renameParam.name"></el-input>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button secondary @click="dialogRename = false">
|
{{ t('common.cancel') }}
|
</el-button>
|
<el-button type="primary" @click="confirmRename">
|
{{ t('common.sure') }}
|
</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
<el-drawer
|
:before-close="closeEditUnion"
|
v-model="editUnion"
|
modal-class="union-item-drawer"
|
size="600px"
|
direction="rtl"
|
>
|
<template #header v-if="currentNode">
|
<div style="width: 100%">
|
<div class="info">
|
<span :title="currentNode.tableName" class="label ellipsis">{{
|
currentNode.tableName
|
}}</span>
|
</div>
|
<div class="info" style="margin-top: 4px">
|
<span
|
:title="getDsName(currentNode.datasourceId)"
|
style="max-width: 550px"
|
class="name ellipsis"
|
>{{ t('auth.datasource') }}:{{ getDsName(currentNode.datasourceId) }}</span
|
>
|
</div>
|
<div class="info" style="margin-top: 4px">
|
<span :title="currentNode.noteName" style="max-width: 500px" class="name ellipsis"
|
>{{ t('data_set.table_remarks') }}:{{ currentNode.noteName || '-' }}</span
|
>
|
</div>
|
</div>
|
</template>
|
<union-field-list
|
:field-list="nodeField"
|
:node="currentNode"
|
v-if="nodeField.length"
|
@checkedFields="changeNodeFields"
|
/>
|
<template #footer>
|
<el-button secondary @click="closeEditUnion">{{ t('dataset.cancel') }} </el-button>
|
<el-button type="primary" @click="confirmEditUnion">{{ t('dataset.confirm') }} </el-button>
|
</template>
|
</el-drawer>
|
<el-drawer
|
direction="btt"
|
:close-on-click-modal="false"
|
size="calc(100% - 100px)"
|
:with-header="false"
|
:close-on-press-escape="false"
|
modal-class="sql-drawer-fullscreen"
|
v-model="editSqlField"
|
>
|
<add-sql @save="saveSqlNode" @close="closeSqlNode" :sqlNode="sqlNode"></add-sql>
|
</el-drawer>
|
</template>
|
|
<style lang="less">
|
.path-point {
|
cursor: pointer;
|
}
|
|
.sql-drawer-fullscreen {
|
.ed-drawer.btt > .ed-drawer__body {
|
padding: 0;
|
}
|
}
|
|
.union-item-drawer {
|
.ed-drawer__header {
|
height: auto;
|
font-family: var(--de-custom_font, 'PingFang');
|
|
.ed-drawer__close-btn {
|
top: 40.5px;
|
}
|
|
.info {
|
width: 100%;
|
display: flex;
|
.label {
|
font-weight: 500;
|
font-size: 16px;
|
color: #1f2329;
|
max-width: 500px;
|
}
|
.name {
|
font-weight: 400;
|
font-size: 14px;
|
color: #646a73;
|
line-height: 22px;
|
}
|
}
|
}
|
.field-block-body {
|
height: calc(100% - 70px) !important;
|
}
|
}
|
|
.node-union {
|
height: 100%;
|
width: 100%;
|
border: 1px solid #dee0e3;
|
border-radius: 4px;
|
font-family: var(--de-custom_font, 'PingFang');
|
font-size: 14px;
|
font-weight: 400;
|
color: #1f2329;
|
padding-left: 9px;
|
display: flex;
|
align-items: center;
|
background: #fff;
|
position: relative;
|
cursor: pointer;
|
padding-right: 12px;
|
|
&:hover {
|
border-color: var(--ed-color-primary);
|
}
|
|
.placeholder {
|
display: none;
|
}
|
|
& > .ed-icon {
|
font-size: 13.3px;
|
margin-right: 9.33px;
|
}
|
.tableName {
|
max-width: 125px;
|
text-overflow: ellipsis;
|
overflow: hidden;
|
white-space: nowrap;
|
}
|
|
&:not(.shadow-node)::before {
|
content: '';
|
position: absolute;
|
width: 3px;
|
height: 32px;
|
left: -1px;
|
top: -1px;
|
background: var(--ed-color-primary);
|
border-radius: 4px 0px 0px 4px;
|
}
|
}
|
|
.path-union {
|
width: 100%;
|
height: 100%;
|
border: 1px solid #dee0e3;
|
border-radius: 50%;
|
font-size: 22px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: #fff;
|
color: var(--ed-color-primary);
|
cursor: pointer;
|
&:hover {
|
border-color: var(--ed-color-primary);
|
}
|
}
|
|
.shadow-node {
|
border: 1px dashed;
|
border-color: var(--ed-color-primary);
|
background-color: rgba(51, 112, 255, 0.08);
|
.ed-icon,
|
.tableName {
|
display: none;
|
}
|
|
.placeholder {
|
display: inline-block;
|
}
|
}
|
|
.active-node {
|
border-color: var(--ed-color-primary);
|
}
|
|
.drag-mask_dataset {
|
background: #f5f6f7;
|
overflow: auto;
|
position: relative;
|
width: 100%;
|
border: none !important;
|
}
|
|
.mask-dataset {
|
position: absolute;
|
left: 16px;
|
top: 16px;
|
width: calc(100% - 32px);
|
height: calc(100% - 32px);
|
z-index: 5;
|
user-select: none;
|
}
|
|
.mask-dataset-none {
|
background-color: #e5ebf8;
|
border: 1px dashed;
|
border-color: var(--ed-color-primary);
|
}
|
|
.zero {
|
position: absolute;
|
left: 16px;
|
top: 16px;
|
width: calc(100% - 32px);
|
height: calc(100% - 32px);
|
z-index: 6;
|
user-select: none;
|
display: flex;
|
align-items: center;
|
flex-direction: column;
|
padding-top: 42px;
|
img {
|
width: 125px;
|
height: 125px;
|
margin-bottom: 8px;
|
-webkit-user-drag: none;
|
}
|
|
p {
|
font-family: var(--de-custom_font, 'PingFang');
|
font-style: normal;
|
font-weight: 400;
|
font-size: 14px;
|
line-height: 22px;
|
text-align: center;
|
color: #646a73;
|
}
|
}
|
</style>
|