Browse Source

Merge remote-tracking branch 'origin/master'

master
xueyinfei 16 hours ago
parent
commit
0c6ebd82c2
  1. 85
      vue-fastapi-backend/module_admin/dao/data_ast_content_dao.py
  2. 11
      vue-fastapi-backend/module_admin/service/data_ast_content_service.py
  3. 9
      vue-fastapi-frontend/src/api/meta/metaInfo.js
  4. 78
      vue-fastapi-frontend/src/components/codemirror/SQLCodeMirrorSqlFlow.vue
  5. 1
      vue-fastapi-frontend/src/views/dataAsset/assetDetail/index.vue
  6. 24
      vue-fastapi-frontend/src/views/dataAsset/directory/index.vue
  7. 744
      vue-fastapi-frontend/src/views/meta/metaInfo/bloodRelationSql.vue
  8. 240
      vue-fastapi-frontend/src/views/sqlFlow/index.vue

85
vue-fastapi-backend/module_admin/dao/data_ast_content_dao.py

@ -9,6 +9,7 @@ from module_admin.entity.do.user_do import SysUser
from module_admin.entity.vo.data_ast_content_vo import DataCatalogPageQueryModel, DeleteDataCatalogModel,DataCatalogChild,DataAstBookmarkRelaRequest,DataAstIndxRequest,DataAstIndxResponse
from utils.page_util import PageUtil
from utils.log_util import logger
from exceptions.exception import ServiceException
class DataCatalogDAO:
@ -176,7 +177,33 @@ class DataCatalogDAO:
)
return data_ast_list
@classmethod
async def check_duplicate_catalog(cls, db: AsyncSession, catalog1: dict, exclude_content_onum: int = None):
"""
校验同一父节点下是否已存在相同名称的目录可排除指定 content_onum
:param db: ORM 对象
:param catalog1: 主目录对象DataAstContent
:param exclude_content_onum: 可选编辑时排除自己
:return: 如果存在重复目录抛出 ServiceException
"""
query = select(DataAstContent).where(
and_(
DataAstContent.supr_content_onum == catalog1.get("supr_content_onum"),
DataAstContent.content_name == catalog1.get("content_name")
)
)
if exclude_content_onum:
query = query.where(DataAstContent.content_onum != exclude_content_onum)
result = await db.execute(query)
exist_catalog = result.scalars().first()
if exist_catalog:
raise ServiceException(
message=f"同一父节点下已存在名称为“{catalog1.get('content_name')}”的目录"
)
@classmethod
async def add_catalog_dao(cls, db: AsyncSession, catalog1: dict, catalog2: dict):
"""
@ -233,17 +260,19 @@ class DataCatalogDAO:
await db.execute(stmt)
@classmethod
async def edit_catalog_child_dao(cls, db: AsyncSession, catalog: dict):
"""
编辑目录数据库操作
编辑目录数据库操作子节点先删除再新增
:param db: orm对象
:param catalog: 需要更新的目录字典
:return:
:param catalog: 需要更新的目录字典包含 children 列表
"""
content_onum = catalog['content_onum']
# ---------- 更新主目录 ----------
stmt = (
update(DataAstContent)
.where(DataAstContent.content_onum == content_onum)
@ -256,35 +285,25 @@ class DataCatalogDAO:
leaf_node_flag=catalog['leaf_node_flag'],
upd_prsn=catalog['upd_prsn'],
upd_time=datetime.now()
) )
)
)
await db.execute(stmt)
# 处理子关系
# ---------- 删除原有子关系 ----------
delete_stmt = (
delete(DataAstContentRela)
.where(DataAstContentRela.content_onum == content_onum)
)
await db.execute(delete_stmt)
# ---------- 新增子关系 ----------
for child in catalog.get('children', []):
rela_onum = child.get('rela_onum')
if rela_onum:
st = (
update(DataAstContentRela)
.where(DataAstContentRela.rela_onum == rela_onum)
.values(
content_onum=child.get('content_onum'),
ast_onum=child.get('ast_onum'),
rela_type=child.get('rela_type'),
rela_eff_begn_date=child.get('rela_eff_begn_date'),
rela_eff_end_date=child.get('rela_eff_end_date'),
upd_prsn=child.get('upd_prsn'))
)
await db.execute(st)
await cls.update_leaf_node_flag(db)
else:
child['content_onum'] = content_onum
db_child = DataAstContentRela(**child)
db.add(db_child)
await db.flush()
await cls.update_leaf_node_flag(db)
child['content_onum'] = content_onum
db_child = DataAstContentRela(**child)
db.add(db_child)
# 更新叶子节点状态
await cls.update_leaf_node_flag(db)

11
vue-fastapi-backend/module_admin/service/data_ast_content_service.py

@ -156,10 +156,14 @@ class DataCatalogService:
child["rela_eff_end_date"] = datetime(year=2999, month=12, day=31, hour=0, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S"), # 设置默认值,2999-12-31
child["upd_prsn"] = request.upd_prsn,
child["rela_status"] = "1"
await DataCatalogDAO.check_duplicate_catalog(query_db, catalog_data1)
new_catalog = await DataCatalogDAO.add_catalog_dao(query_db, catalog_data1, catalog_data2)
await query_db.commit()
return CrudResponseModel(is_success=True, message='新增成功', data=new_catalog)
except ServiceException as e:
await query_db.rollback()
# 直接抛出,不再重新包装,保留 DAO 层信息
raise e
except Exception as e:
await query_db.rollback()
raise ServiceException(message=f"创建目录时发生错误: {str(e)}")
@ -224,9 +228,14 @@ class DataCatalogService:
child["upd_prsn"] = request.upd_prsn
child["rela_status"] = "1"
await DataCatalogDAO.check_duplicate_catalog(query_db, catalog_data, exclude_content_onum=request.content_onum)
await DataCatalogDAO.edit_catalog_child_dao(query_db, catalog_data)
await query_db.commit()
return CrudResponseModel(is_success=True, message='更新成功')
except ServiceException as e:
await query_db.rollback()
# 直接抛出,不再重新包装,保留 DAO 层信息
raise e
except Exception as e:
await query_db.rollback()
raise ServiceException(message=f"更新目录时发生错误: {str(e)}")

9
vue-fastapi-frontend/src/api/meta/metaInfo.js

@ -17,6 +17,15 @@ export function runBloodAnalysis(query) {
headers: {dashUserName:query.userName,dashPassword:query.password}
})
}
export function runBloodAnalysisBySql(query) {
return request({
url: '/blood-analysis-api/bloodAnalysis/sql/analysis',
method: 'post',
data: query,
headers: {dashUserName:query.userName,dashPassword:query.password}
})
}
// 查询参数列表
export function getMetaDataList(query) {
return request({

78
vue-fastapi-frontend/src/components/codemirror/SQLCodeMirrorSqlFlow.vue

@ -0,0 +1,78 @@
<template>
<codemirror v-model:value="value" :options="sqlOptions" :dbType="dbType" />
</template>
<script setup>
// sql
import * as sqlFormatter from "sql-formatter";
import Codemirror from 'codemirror-editor-vue3';
import 'codemirror/mode/sql/sql.js';
import "codemirror/mode/javascript/javascript.js";
// language
import 'codemirror/mode/javascript/javascript.js';
// theme
import 'codemirror/theme/monokai.css';
//
import 'codemirror/addon/fold/foldcode.js';
import 'codemirror/addon/fold/foldgutter.js';
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/fold/brace-fold.js';
//
import 'codemirror/addon/hint/show-hint.js';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/javascript-hint.js';
// lint
import 'codemirror/addon/lint/lint.js';
import 'codemirror/addon/lint/lint.css';
import 'codemirror/addon/lint/json-lint';
//
import 'codemirror/addon/edit/matchbrackets.js';
import 'codemirror/addon/edit/closebrackets.js';
import "codemirror/addon/lint/json-lint.js";
import {watch,ref} from "vue";
const props = defineProps({
data: String,
dbType: String,
})
const sqlOptions = {
autorefresh: true, //
smartIndent: true, //
tabSize: 4, // 4
mode: "text/x-sql", //
line: true, //
viewportMargin: Infinity, //
highlightDifferences: true,
autofocus: false,
indentUnit: 2,
readOnly: false, //
showCursorWhenSelecting: true,
firstLineNumber: 1,
matchBrackets: true,//
lineWrapping: true, //
gutters: [
"CodeMirror-linenumbers",
"CodeMirror-foldgutter",
"CodeMirror-lint-markers",
],
lineNumbers: true, //
lint: true, // json
}
const value = ref("")
watch(() => value.value,
(val) =>{
console.log(props.dbType)
value.value = sqlFormatter.format(val,{ language: props.dbType?.toLowerCase() || "sql" })
}
)
onMounted(()=>{
value.value = props.data
}
)
</script>
<style scoped lang="scss">
</style>

1
vue-fastapi-frontend/src/views/dataAsset/assetDetail/index.vue

@ -222,7 +222,6 @@ const treeData = ref([])
const treeDataChildren = ref([]);
const getSrcSysName = (id) => {
if (id === null || id === undefined) return '';
const getName = (val) => {
const match = dsSysList.find(item => item.id == String(val).trim());
return match ? match.name : val;

24
vue-fastapi-frontend/src/views/dataAsset/directory/index.vue

@ -33,7 +33,21 @@
><Folder
/></el-icon>
<el-icon v-else><Document /></el-icon>
<span>{{ data.contentName || data.dataAstCnName }}</span>
<el-tooltip
v-if="(data.contentName || data.dataAstCnName)?.length > 9"
:content="data.contentName || data.dataAstCnName"
placement="top"
>
<span class="ellipsis-text">
{{ (data.contentName || data.dataAstCnName).slice(0, 10) + '…' }}
</span>
</el-tooltip>
<span
v-else
class="ellipsis-text"
>
{{ data.contentName || data.dataAstCnName }}
</span>
</el-space>
<div class="tree-node__action">
<template v-if="isAsset(data)">
@ -761,6 +775,14 @@ const handleIframeLoad = () => {
overflow: auto;
}
}
.ellipsis-text {
display: inline-block;
max-width: 150px; /* 可根据实际宽度调整 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
iframe {
border: none;

744
vue-fastapi-frontend/src/views/meta/metaInfo/bloodRelationSql.vue

@ -0,0 +1,744 @@
<template>
<div id="bloodRelationG6" style="width: 100%;height: 100%;overflow: auto;position: relative"></div>
<div id="bloodmini-container"></div>
</template>
<script setup>
import G6 from '@antv/g6';
import {watch} from "vue";
const props = defineProps({
data: {
type: Object,
default: () => {}
},
currentTable: {
type:Object,
default:()=>{}
}
})
const g6data = ref([])
function initG6() {
const {
Util,
registerBehavior,
registerEdge,
registerNode
} = G6;
const isInBBox = (point, bbox) => {
const {
x,
y
} = point;
const {
minX,
minY,
maxX,
maxY
} = bbox;
return x < maxX && x > minX && y > minY && y < maxY;
};
const itemHeight = 20;
registerBehavior("dice-er-scroll", {
getDefaultCfg() {
return {
multiple: true,
};
},
getEvents() {
return {
itemHeight,
wheel: "scorll",
click: "click",
"node:mousemove": "move",
"node:mousedown": "mousedown",
"node:mouseup": "mouseup"
};
},
scorll(e) {
// e.preventDefault();
const {
graph
} = this;
const nodes = graph.getNodes().filter((n) => {
const bbox = n.getBBox();
return isInBBox(graph.getPointByClient(e.clientX, e.clientY), bbox);
});
const x = e.deltaX || e.movementX;
let y = e.deltaY || e.movementY;
if (!y && navigator.userAgent.indexOf('Firefox') > -1) y = (-e.wheelDelta * 125) / 3
if (nodes) {
const edgesToUpdate = new Set();
nodes.forEach((node) => {
return;
const model = node.getModel();
if (model.attrs.length < 2) {
return;
}
const idx = model.startIndex || 0;
let startX = model.startX || 0.5;
let startIndex = idx + y * 0.02;
startX -= x;
if (startIndex < 0) {
startIndex = 0;
}
if (startX > 0) {
startX = 0;
}
if (startIndex > model.attrs.length - 1) {
startIndex = model.attrs.length - 1;
}
graph.updateItem(node, {
startIndex,
startX,
});
node.getEdges().forEach(edge => edgesToUpdate.add(edge))
});
// G6 update the related edges when graph.updateItem with a node according to the new properties
// here you need to update the related edges manualy since the new properties { startIndex, startX } for the nodes are custom, and cannot be recognized by G6
edgesToUpdate.forEach(edge => edge.refresh())
}
},
click(e) {
const {
graph
} = this;
const item = e.item;
const shape = e.shape;
if (!item) {
return;
}
if (shape.get("name") === "collapse") {
graph.updateItem(item, {
collapsed: true,
size: [300, 50],
});
setTimeout(() => {
graph.layout();
}, 50);
} else if (shape.get("name") === "expand") {
graph.updateItem(item, {
collapsed: false,
size: [300, 120],
});
setTimeout(() => {
graph.layout();
}, 50);
}
else if (shape.get("name") && shape.get("name").startsWith("item")) {
let edges = graph.getEdges()
let columnName = ''
g6data.value.forEach(data => {
if (data.id === item.getModel().id) {
columnName = data.attrs[shape.get("name").split("-")[1]].key
}
})
edges.forEach(edg => {
if (edg.getModel().sourceKey === columnName && edg.getModel().source === item.getModel().id) {
edg.show()
} else if(edg.getModel().targetKey === columnName && edg.getModel().target === item.getModel().id){
edg.show()
} else{
edg.hide()
}
})
let nodes = graph.getNodes()
let index = Number(shape.get("name").split("-")[1])
nodes.forEach(node => {
let model = node.getModel()
model.selectedIndex = NaN
graph.updateItem(node, model)
})
this.graph.updateItem(item, {
selectedIndex: index,
});
} else {
graph.updateItem(item, {
selectedIndex: NaN,
});
let edges = graph.getEdges()
edges.forEach(edg => {
edg.show()
})
}
},
mousedown(e) {
this.isMousedown = true;
},
mouseup(e) {
this.isMousedown = false;
},
move(e) {
if (this.isMousedown) return;
// const name = e.shape.get("name");
// const item = e.item;
//
// if (name && name.startsWith("item")) {
// this.graph.updateItem(item, {
// selectedIndex: Number(name.split("-")[1]),
// });
// } else {
// this.graph.updateItem(item, {
// selectedIndex: NaN,
// });
// }
},
});
registerEdge("dice-er-edge", {
draw(cfg, group) {
const edge = group.cfg.item;
const sourceNode = edge.getSource().getModel();
const targetNode = edge.getTarget().getModel();
const sourceIndex = sourceNode.attrs.findIndex(
(e) => e.key === cfg.sourceKey
);
const sourceStartIndex = sourceNode.startIndex || 0;
let sourceY = 15;
if (!sourceNode.collapsed && sourceIndex > sourceStartIndex - 1) {
sourceY = 30 + (sourceIndex - sourceStartIndex + 0.5) * itemHeight;
}
const targetIndex = targetNode.attrs.findIndex(
(e) => e.key === cfg.targetKey
);
const targetStartIndex = targetNode.startIndex || 0;
let targetY = 15;
if (!targetNode.collapsed && targetIndex > targetStartIndex - 1) {
targetY = (targetIndex - targetStartIndex + 0.5) * itemHeight + 30;
}
const startPoint = {
...cfg.startPoint
};
const endPoint = {
...cfg.endPoint
};
startPoint.y = startPoint.y + sourceY;
endPoint.y = endPoint.y + targetY;
let shape;
if (sourceNode.id !== targetNode.id) {
shape = group.addShape("path", {
attrs: {
stroke: "#5B8FF9",
path: [
["M", startPoint.x, startPoint.y],
[
"C",
endPoint.x / 3 + (2 / 3) * startPoint.x,
startPoint.y,
endPoint.x / 3 + (2 / 3) * startPoint.x,
endPoint.y,
endPoint.x,
endPoint.y,
],
],
endArrow: true,
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: "path-shape",
});
} else {
let gap = Math.abs((startPoint.y - endPoint.y) / 3);
if (startPoint["index"] === 1) {
gap = -gap;
}
shape = group.addShape("path", {
attrs: {
stroke: "#5B8FF9",
path: [
["M", startPoint.x, startPoint.y],
[
"C",
startPoint.x - gap,
startPoint.y,
startPoint.x - gap,
endPoint.y,
startPoint.x,
endPoint.y,
],
],
endArrow: props.type === 'er'? {path: G6.Arrow.triangle(10,-10,6),fill:'#5B8FF9'} : false//cfg.endArrow,
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: "path-shape",
});
}
return shape;
},
afterDraw(cfg, group) {
const labelCfg = cfg.labelCfg || {};
const edge = group.cfg.item;
const sourceNode = edge.getSource().getModel();
const targetNode = edge.getTarget().getModel();
if (sourceNode.collapsed && targetNode.collapsed) {
return;
}
const path = group.find(
(element) => element.get("name") === "path-shape"
);
const labelStyle = Util.getLabelPosition(path, 0.5, 0, 0, true);
const label = group.addShape("text", {
attrs: {
...labelStyle,
text: cfg.label || '',
fill: "#000",
textAlign: "center",
stroke: "#fff",
lineWidth: 1,
},
});
label.rotateAtStart(labelStyle.rotate);
},
});
registerNode("dice-er-box", {
draw(cfg, group) {
const width = 300;
const {
attrs = [],
startIndex = 0,
selectedIndex,
collapsed,
icon,
} = cfg;
let currentTableLabel = props.currentTable.tabEngName
if (props.currentTable.tabCnName && props.currentTable.tabCnName.length>0){
currentTableLabel += "("+props.currentTable.tabCnName+")"
}else if (props.currentTable.tabCrrctName && props.currentTable.tabCnName.tabCrrctName>0){
currentTableLabel += "("+props.currentTable.tabCrrctName+")"
}
const list = attrs;
const itemCount = list.length;
const boxStyle = {
stroke: currentTableLabel === cfg.label?"#67C23A":"#096DD9",
radius: 4,
};
const afterList = list.slice(
Math.floor(startIndex),
Math.floor(startIndex + itemCount)
);
const offsetY = (0.5 - (startIndex % 1)) * itemHeight + 30;
//
group.addShape("rect", {
attrs: {
fill: boxStyle.stroke,
height: 30,
width,
radius: [boxStyle.radius, boxStyle.radius, 0, 0],
},
draggable: true,
});
let fontLeft = 12;
//
if (icon && icon.show !== false) {
group.addShape("image", {
attrs: {
x: 8,
y: 8,
height: 16,
width: 16,
...icon,
},
});
fontLeft += 18;
}
//
group.addShape("text", {
attrs: {
y: 22,
x: fontLeft,
fill: "#fff",
text: cfg.label,
fontSize: 12,
fontWeight: 500,
},
});
//div
group.addShape("rect", {
attrs: {
x: 0,
y: collapsed ? 30 : (list.length + 1) * itemHeight + 15,
height: 15,
width,
fill: "#eee",
radius: [0, 0, boxStyle.radius, boxStyle.radius],
cursor: "pointer",
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: collapsed ? "expand" : "collapse",
});
//
group.addShape("text", {
attrs: {
x: width / 2 - 6,
y: (collapsed ? 30 : (list.length + 1) * itemHeight + 15) + 12,
text: collapsed ? "+" : "-",
width,
fill: "#000",
radius: [0, 0, boxStyle.radius, boxStyle.radius],
cursor: "pointer",
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: collapsed ? "expand" : "collapse",
});
//
const keyshape = group.addShape("rect", {
attrs: {
x: 0,
y: 0,
width,
height: collapsed ? 45 : (list.length + 1) * itemHeight + 15 + 15,
...boxStyle,
},
draggable: true,
});
if (collapsed) {
return keyshape;
}
const listContainer = group.addGroup({});
//
listContainer.setClip({
type: "rect",
attrs: {
x: -8,
y: 30,
width: width + 16,
height: (list.length + 1) * itemHeight + 15 - 30,
},
});
//
listContainer.addShape({
type: "rect",
attrs: {
x: 1,
y: 30,
width: width - 2,
height: (list.length + 1) * itemHeight + 15 - 30,
fill: "#fff",
},
draggable: true,
});
if (list.length + 1 > itemCount) {
const barStyle = {
width: 4,
padding: 0,
boxStyle: {
stroke: "#00000022",
},
innerStyle: {
fill: "#00000022",
},
};
//
listContainer.addShape("rect", {
attrs: {
y: 30,
x: width - barStyle.padding - barStyle.width,
width: barStyle.width,
height: (list.length + 1) * itemHeight - 30,
...barStyle.boxStyle,
},
});
const indexHeight =
afterList.length > itemCount ?
(afterList.length / list.length + 1) * (list.length + 1) * itemHeight :
10;
//
listContainer.addShape("rect", {
attrs: {
y: 30 +
barStyle.padding +
(startIndex / list.length + 1) * ((list.length + 1) * itemHeight - 30),
x: width - barStyle.padding - barStyle.width,
width: barStyle.width,
height: Math.min((list.length + 1) * itemHeight, indexHeight),
...barStyle.innerStyle,
},
});
}
if (afterList) {
afterList.forEach((e, i) => {
const isSelected =
Math.floor(startIndex) + i === Number(selectedIndex);
let {
key = "", type
} = e;
if (type) {
key += " - " + type;
}
const label = key;
//
listContainer.addShape("rect", {
attrs: {
x: 1,
y: i * itemHeight - itemHeight / 2 + offsetY,
width: width - 4,
height: itemHeight,
radius: 2,
lineWidth: 1,
cursor: "pointer",
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: `item-${Math.floor(startIndex) + i}-content`,
draggable: true,
});
if (!cfg.hideDot) {
//
listContainer.addShape("circle", {
attrs: {
x: 0,
y: i * itemHeight + offsetY,
r: 3,
stroke: boxStyle.stroke,
fill: "white",
radius: 2,
lineWidth: 1,
cursor: "pointer",
},
});
//
listContainer.addShape("circle", {
attrs: {
x: width,
y: i * itemHeight + offsetY,
r: 3,
stroke: boxStyle.stroke,
fill: "white",
radius: 2,
lineWidth: 1,
cursor: "pointer",
},
});
}
//
listContainer.addShape("text", {
attrs: {
x: 12,
y: i * itemHeight + offsetY + 6,
text: label,
fontSize: 12,
fill: "#000",
fontFamily: "Avenir,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol",
full: e,
fontWeight: isSelected ? 500 : 100,
cursor: "pointer",
},
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
name: `item-${Math.floor(startIndex) + i}`,
});
});
}
return keyshape;
},
getAnchorPoints() {
return [
[0, 0],
[1, 0],
];
},
});
const dataTransform = (data) => {
const nodes = [];
let edges = [];
data.map((node) => {
nodes.push({
...node
});
if (node.attrs) {
node.attrs.forEach((attr) => {
if (attr.relation) {
attr.relation.forEach((relation) => {
edges.push({
source: node.id,
target: relation.nodeId,
sourceKey: attr.key,
endArrow: relation.endArrow,
targetKey: relation.key,
label: relation.label,
});
});
}
});
}
});
return {
nodes,
edges,
};
}
const container = document.getElementById("bloodRelationG6");
const mini_container = document.getElementById("bloodmini-container");
const width = container.scrollWidth;
const height = container.scrollHeight || container.offsetHeight || 500;
// minimap
const minimap = new G6.Minimap({
size: [200, 200],
container: mini_container,
type: 'delegate',
});
const graph = new G6.Graph({
container: container,
plugins: [minimap],
width,
height,
defaultNode: {
size: [350, 200],
type: 'dice-er-box',
color: '#5B8FF9',
style: {
fill: '#9EC9FF',
lineWidth: 3,
},
labelCfg: {
style: {
fill: 'black',
fontSize: 20,
},
},
},
defaultEdge: {
type: 'dice-er-edge',
style: {
stroke: '#e2e2e2',
lineWidth: 4,
endArrow: {path: G6.Arrow.triangle(10,-10,6),fill:'#5B8FF9'},
},
},
modes: {
default: ['dice-er-scroll', 'drag-node', 'drag-canvas', 'zoom-canvas'],
},
layout: {
type: 'dagre',
rankdir: 'LR',
align: 'UL',
controlPoints: true,
nodesep: 1,
ranksep: 1
},
animate: true,
// fitView: true
})
graph.data(dataTransform(g6data.value))
graph.render();
}
watch(
() => props.data,
(val) => {
g6data.value = []
if (props.data.tableList && props.data.tableList.length>0){
for (let i = 0; i < props.data.tableList.length; i++) {
let table = props.data.tableList[i]
let g6Tab = {
id: table.ssys_id+"-"+table.mdl_name+"-"+table.tab_eng_name,
label: table.tab_eng_name + ((table.tab_cn_name && table.tab_cn_name.length>0)?"("+table.tab_cn_name+")":""),
attrs:[],
collapsed:true
}
for (let j = 0; j < table.column.length; j++) {
g6Tab.attrs.push({
key: table.column[j].fldEngName,
type: table.column[j].fldType
})
}
g6data.value.push(g6Tab)
}
if (props.data.relation && props.data.relation.length>0){
for (let i = 0; i < props.data.relation.length; i++) {
let relation = props.data.relation[i]
let key = relation.targetColName.toLowerCase()
let tableKey = relation.sourceSysId+"-"+relation.sourceMdlName.toLowerCase()+"-"+relation.sourceTableName.toLowerCase()
let nodeId = relation.targetSysId+"-"+relation.targetMdlName.toLowerCase()+"-"+relation.targetTableName.toLowerCase()
if (g6data.value.length > 0){
for (let j = 0; j < g6data.value.length; j++) {
if (g6data.value[j].id === tableKey){
for (let k = 0; k < g6data.value[j].attrs.length; k++) {
if (g6data.value[j].attrs[k].key === relation.sourceColName.toLowerCase()){
if (g6data.value[j].attrs[k].relation && g6data.value[j].attrs[k].relation.length>0){
let hasRelation = false
for (let l = 0; l < g6data.value[j].attrs[k].relation.length; l++) {
if (g6data.value[j].attrs[k].relation[l].key === key && g6data.value[j].attrs[k].relation[l].nodeId === nodeId){
hasRelation = true
}
}
if (!hasRelation){
g6data.value[j].attrs[k].relation.push({
key: key,
nodeId: nodeId,
endArrow: true
})
}
}else {
g6data.value[j].attrs[k].relation = [{
key: key,
nodeId: nodeId,
endArrow: true
}]
}
}
}
}
}
}
}
}
let g6 = document.getElementById("bloodRelationG6")
const mini_container = document.getElementById("bloodmini-container");
if (mini_container){
mini_container.innerHTML=''
}
if (g6){
g6.innerHTML=''
initG6()
}
}
},
{ deep: true,immediate: true }
)
</script>
<style scoped lang="scss">
#bloodmini-container{
position: absolute !important;
right: 20px;
bottom: 20px;
border: 1px solid #e2e2e2;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.9);
z-index: 10;
}
</style>

240
vue-fastapi-frontend/src/views/sqlFlow/index.vue

@ -1,22 +1,250 @@
<template>
<div class="sql-container">
<SQLCodeMirror v-if="activeColumnTab === 'proc'" :data="procStr" :dbType="dbType" />
<div class="app-container" ref="containerRef">
<!-- 左侧 SQL 编辑区 -->
<div class="sql-container" :style="{ width: leftWidth + 'px' }">
<!-- 工具栏 -->
<div class="toolbar">
<!-- 数据库类型 -->
<el-select
v-model="dbType"
placeholder="选择数据库类型"
size="small"
style="width: 140px; margin-left: 10px;"
>
<el-option label="MySQL" value="MYSQL" />
<el-option label="PostgreSQL" value="PG" />
<el-option label="SQL Server" value="MSSQL" />
<el-option label="Oracle" value="ORACLE" />
<el-option label="DB2" value="DB2" />
</el-select>
<!-- 系统选择 -->
<el-select
v-model="selectedSystem"
placeholder="选择系统"
size="small"
style="width: 160px"
>
<el-option
v-for="sys in dsSysList"
:key="sys.id"
:label="sys.name"
:value="sys.id"
/>
</el-select>
<!-- 模式 -->
<el-input
v-model="defaultModel"
placeholder="输入模式名"
size="small"
style="width: 140px; margin-left: 10px;"
clearable
/>
<!-- 执行按钮 -->
<el-button
type="primary"
size="small"
style="margin-left: 10px"
@click="executeSql"
>
执行
</el-button>
</div>
<!-- SQL 编辑器 -->
<SQLCodeMirror
v-if="activeColumnTab === 'proc'"
v-model:data="procStr"
:dbType="dbType"
/>
</div>
<!-- 分隔条 -->
<div class="divider" @mousedown="startDragging"></div>
<!-- 右侧 血缘关系图 -->
<div class="relation-container">
<BloodRelation :currentTable="currentMetaData" :data="bloodRelation" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SQLCodeMirror from '@/components/codemirror/SQLCodeMirror.vue'
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { getMetaDataBloodRelship, runBloodAnalysisBySql } from '@/api/meta/metaInfo'
import BloodRelation from '@/views/meta/metaInfo/bloodRelationSql.vue'
import SQLCodeMirror from '@/components/codemirror/SQLCodeMirrorSqlFlow.vue'
import useUserStore from '@/store/modules/user'
import cache from "@/plugins/cache";
const userStore = useUserStore()
const dsSysList = userStore.dsSysList
// ========================= =========================
const activeColumnTab = ref('proc')
const procStr = ref('SELECT * FROM users LIMIT 100;')
const dbType = ref('MYSQL')
const containerRef = ref(null)
//
const selectedSystem = ref(dsSysList?.[0]?.id || null)
//
const defaultModel = ref('')
//
const currentMetaData = reactive({
tabEngName: 't_dim_comp',
tabCnName: '公司维度表',
ssysCd: 'PG_CONN',
ssysId: 1,
mdlName: 'public',
tabCrrctName: '',
tabDesc: '',
govFlag: null,
pic: '',
tags: []
})
//
const bloodRelation = ref([])
// ========================= =========================
const leftWidth = ref(600)
const isDragging = ref(false)
let startX = 0
let startWidth = 0
const startDragging = (e) => {
isDragging.value = true
startX = e.clientX
startWidth = leftWidth.value
document.body.style.userSelect = 'none'
document.addEventListener('mousemove', handleDragging)
document.addEventListener('mouseup', stopDragging)
}
const handleDragging = (e) => {
if (!isDragging.value) return
const delta = e.clientX - startX
const newWidth = startWidth + delta
const minWidth = 300
const maxWidth = window.innerWidth - 400
leftWidth.value = Math.min(Math.max(newWidth, minWidth), maxWidth)
}
const stopDragging = () => {
isDragging.value = false
document.body.style.userSelect = ''
document.removeEventListener('mousemove', handleDragging)
document.removeEventListener('mouseup', stopDragging)
}
// ========================= =========================
const changeBloodOption = () => {
getMetaDataBloodRelship(currentMetaData.ssysId).then((res) => {
bloodRelation.value = res.data
})
}
/**
* 执行 SQL 并生成血缘分析图
*/
const executeSql = async () => {
if (!selectedSystem.value) {
ElMessage.warning('请选择系统')
return
}
if (!procStr.value.trim()) {
ElMessage.warning('请输入 SQL 语句')
return
}
const params = {
sqlType: dbType.value,
defaultSystem: selectedSystem.value,
defaultModel: defaultModel.value || '',
sql: procStr.value,
userName: cache.local.get("username"),
password: cache.local.get("password")
}
console.log(params)
try {
ElMessage.info('正在执行血缘分析,请稍候...')
const res = await runBloodAnalysisBySql(params)
if (res?.data) {
bloodRelation.value = res.data
ElMessage.success('血缘分析执行成功')
} else {
ElMessage.warning('未返回血缘数据')
}
} catch (err) {
console.error(err)
ElMessage.error('执行血缘分析失败')
}
}
// ========================= =========================
onMounted(() => {
changeBloodOption() // SQL
})
onBeforeUnmount(() => {
stopDragging()
})
</script>
<style scoped>
.app-container {
display: flex;
flex-direction: row;
height: 100vh;
width: 100%;
overflow: hidden;
}
/* 左侧 SQL 编辑区 */
.sql-container {
padding: 16px;
height: 100%;
background: #fafafa;
height: 100vh;
border-right: 1px solid #e0e0e0;
overflow: auto;
transition: width 0.1s ease-out;
display: flex;
flex-direction: column;
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
background: #f5f7fa;
border-bottom: 1px solid #e0e0e0;
padding: 8px 12px;
height: 45px;
}
/* 分隔条 */
.divider {
width: 6px;
cursor: col-resize;
background-color: #dcdcdc;
transition: background-color 0.2s;
flex-shrink: 0;
}
.divider:hover {
background-color: #aaa;
}
/* 右侧 血缘关系图 */
.relation-container {
flex: 1;
height: 100%;
overflow: hidden;
padding: 8px;
background: #fff;
}
</style>

Loading…
Cancel
Save