This commit is contained in:
clay
2024-03-04 19:13:43 +08:00
commit e44edd71c0
350 changed files with 52288 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
/**
* @author: clay
* @data: 2021/5/14 23:20
* @email: clay@hchyun.com
* @description: node er图点击事件
*/
let vm = null;
const sendThis = (_this) => {
vm = _this;
};
export default {
sendThis,
name: "click-er-edge",
options: {
getEvents() {
return {
"edge:click": "onEdgeClick",
"edge:contextmenu": "onEdgeRightClick"
};
},
/**
* 点击连线函数
* @param event
*/
onEdgeClick(event) {
let clickEdge = event.item;
clickEdge.setState("selected", !clickEdge.hasState("selected"));
this.singleClickEdge(event)
// vm.editCurrentFocus("edge")
// this.updateVmData(event);
},
/**
* 右键点击线
* @param event
*/
onEdgeRightClick(event) {
this.singleClickEdge(event)
},
singleClickEdge(event) {
let graph = vm.getGraph();
let clickEdge = event.item;
let clickEdgeModel = toRaw(clickEdge.getModel());
let selectedEdges = graph.findAllByState("edge", "selected");
// 如果当前点击节点不是之前选中的单个节点,才进行下面的处理
if (!(selectedEdges.length === 1 && clickEdgeModel.id === selectedEdges[0].getModel().id)) {
// 先取消所有节点的选中状态
graph.findAllByState("edge", "selected").forEach(edge => {
edge.setState("selected", false);
});
// 再添加该节点的选中状态
clickEdge.setState("selected", true);
vm.editCurrentFocus("edge")
this.updateVmData(event);
}else {
vm.editCurrentFocus("edge")
this.updateVmData(event);
}
},
updateVmData(event) {
// 更新vm的data: selectedEdge 和 selectedEdgeParams
let clickEdge = event.item;
if (clickEdge.hasState("selected")) {
let clickEdgeModel = toRaw(clickEdge.getModel());
vm.editSelectedEdge(clickEdge)
let edgeAppConfig = { ...vm.getEdgeAppConfig() };
Object.keys(edgeAppConfig).forEach(function(key) {
edgeAppConfig[key] = "";
});
vm.editSelectedEdgeParams({
label: clickEdgeModel.label || "",
relationalItem: clickEdgeModel.relationalItem,
sourceColumn: clickEdgeModel.sourceColumn,
targetColumn: clickEdgeModel.targetColumn,
appConfig: { ...edgeAppConfig, ...clickEdgeModel.appConfig }
})
}
},
}
};

View File

@@ -0,0 +1,157 @@
/**
* @author: clay
* @data: 2021/5/14 23:20
* @email: clay@hchyun.com
* @description: node er图点击事件
*/
const isInBBox = (point, bbox) => {
const {
x,
y
} = point;
const {
minX,
minY,
maxX,
maxY
} = bbox;
return x < maxX && x > minX && y > minY && y < maxY;
};
let vm = null;
const sendThis = (_this) => {
vm = _this;
};
export default {
sendThis,
name: "click-er-node",
options: {
getEvents() {
return {
wheel: "scroll",
"node:click": "onNodeClick",
"node:contextmenu": "onNodeRightClick",
"canvas:click": "onCanvasClick"
};
},
//滚动事件监听
scroll(event) {
//禁止滚动的默认事件
event.preventDefault();
let graph = vm.getGraph()
// ↓|| vm.getClickCtrlValue() === true
if (vm.getClickCtrlValue()) {
if (graph && !graph.destroyed) {
if (event.deltaY > 0) {
graph.zoom(0.8, {x: event.x, y: event.y})
} else {
graph.zoom(1.2, {x: event.x, y: event.y})
}
}
} else {
//graph.getNodes()获取 Combo 中所有子节点
const nodes = graph.getNodes().filter((n) => {
const bbox = n.getBBox();//获取元素的包围盒。
//将 clientX/clientY 坐标系的坐标值转换为 pointX/pointY 的坐标值。
return isInBBox(graph.getPointByClient(event.clientX, event.clientY), bbox);
});
if (nodes) {
nodes.forEach((node) => {
const model = node.getModel();
if (model.columns.length < 9) {
return;
}
const idx = model.startIndex || 0; // 获取model的startIndex属性如果没有则使用0赋值给变量idx
// this.start = idx; // 这里将idx赋值给变量this.start
let startX = model.startX || 0.5; // 获取model的startX属性如果没有则使用0.5赋值给变量startX
//正值向下滚动,负值向上滚动
let startIndex = idx + event.deltaY * 0.018; // 计算startIndex值将idx与event的deltaY属性值乘以0.018相加赋值给变量startIndex
if ((model.columns.length - idx) < 10 && startIndex > idx) { // 判断条件判断model的columns数组的长度减去idx的值是否小于10并且startIndex的值是否大于idx
return; // 如果条件满足,则结束函数的执行
}
startX -= event.deltaX;//deltaX 属性在向右滚动时返回正值,向左滚动时返回负值,否则为 0。
//对startIndex进行边界检查和调整
if (startIndex < 0) {
startIndex = 0;
}
if (startX > 0) {
startX = 0;
}
if (startIndex > model.columns.length - 1) {
startIndex = model.columns.length - 1;
}
graph.update(node, {
startIndex,
startX
});
});
}
}
},
//节点左键点击监听
onNodeClick(event) {
if (event) {
vm.editCurrentFocus("node")
vm.editRightMenuShow(false)
this.shrinkage(event);
this.updateVmData(event);
}
},
/**
* todo 右键打开设置面板
* @param event
*/
onNodeRightClick(event) {
this.updateVmData(event);
vm.editTableColumnEditOpen(true)
},
onCanvasClick() {
vm.editCurrentFocus("canvas")
vm.editRightMenuShow(false)
},
updateVmData(event) {
let clickNode = event.item;
clickNode.setState("selected", true);
vm.editSelectedNode(clickNode);
let clickNodeModel = toRaw(clickNode.getModel());
let nodeAppConfig = {...vm.getNodeAppConfig()};
Object.keys(nodeAppConfig).forEach(function (key) {
nodeAppConfig[key] = "";
});
vm.editSelectedNodeParams({
label: clickNodeModel.label || "",
columns: clickNodeModel.columns,
appConfig: {...nodeAppConfig, ...clickNodeModel.appConfig}
})
},
shrinkage(e) {
if (!e.item) {
return;
}
const graph = vm.getGraph();
const name=e.shape.get("name")
if (name === "collapse") {
graph.updateItem(e.item, {
collapsed: true,
size: [300, 50],
height: 44
});
setTimeout(() => graph.layout(), 100);
} else if (name === "expand") {
graph.updateItem(e.item, {
collapsed: false,
size: [300, 500],
height: 316
});
setTimeout(() => graph.layout(), 100);
}
// else {
// const model = e.item.getModel();
// }
},
}
};

View File

@@ -0,0 +1,232 @@
/**
* @author: clay
* @data: 2019/07/16
* @description: edit mode: 通过拖拽节点上的锚点添加连线
*/
import utils from "../../utils";
import theme from "../theme";
import {ElMessage} from "element-plus";
// 用来获取调用此js的vue组件实例this
let vm = null;
const sendThis = (_this) => {
vm = _this;
};
export default {
sendThis, // 暴露函数
name: "drag-add-edge",
options: {
getEvents() {
return {
"node:mousedown": "onNodeMousedown",
"node:mouseup": "onNodeMouseup",
"edge:mouseup": "onEdgeMouseup",
"mousemove": "onMousemove"
};
},
onNodeMousedown(event) {
let self = this;
// 交互过程中的信息
self.evtInfo = {
action: null,
node: event.item,
target: event.target,
};
if (self.evtInfo.target && self.evtInfo.target.attrs.name) {
// todo...未来可能针对锚点增加其它功能(例如拖拽调整大小)
switch (self.evtInfo.target.attrs.name) {
case "anchor"://点击锚点中间
self.evtInfo.action = "drawEdge";
break;
case "anchorBg"://点击锚点
self.evtInfo.action = "drawEdge";
break;
}
}
if (self.evtInfo && self.evtInfo.action) {
self[self.evtInfo.action].start.call(self, event);
}
},
onNodeMouseup(event) {
let self = this;
if (self.evtInfo && self.evtInfo.action) {
self[self.evtInfo.action].stop.call(self, event);
}
},
onEdgeMouseup(event) {
let self = this;
if (self.evtInfo && self.evtInfo.action === "drawEdge") {
self[self.evtInfo.action].stop.call(self, event);
}
},
onMousemove(event) {
let self = this;
if (self.evtInfo && self.evtInfo.action) {
self[self.evtInfo.action].move.call(self, event);
}
},
drawEdge: {
isMoving: false,
currentLine: null,
start: function (event) {
let self = this;
let themeStyle = theme.defaultStyle; // todo...先使用默认主题,后期可能增加其它风格的主体
// ************** 暂存【连线】前的数据状态 start **************
let graph = vm.getGraph();
self.historyData = JSON.stringify(graph.save());
// ************** 暂存【连线】前的数据状态 end **************
let sourceAnchor = self.evtInfo.node.getAnchorPoints();
let sourceNodeModel = toRaw(self.evtInfo.node.getModel());
// 锚点数据
let anchorPoints = self.evtInfo.node.getAnchorPoints();
// 处理线条目标点
if (anchorPoints && anchorPoints.length) {
// 获取距离指定坐标最近的一个锚点
sourceAnchor = self.evtInfo.node.getLinkPoint({
x: event.x,
y: event.y
})
}
// let relational = vm.getRelationalMap().get(sourceNodeModel.tableId)
let relational = vm.getRelationalMap().map(item => {
if (item.mainId === sourceNodeModel.tableId) {
return item
}
})
//item.columnName+':'+item.columnComment
let columns = []
sourceNodeModel.columns.forEach(item => {
let column = {
columnName: item.columnName,
columnComment: item.columnComment
}
columns.push(column)
})
self.drawEdge.currentLine = self.graph.addItem("edge", {
// id: G6.Util.uniqueId(), // 这种生成id的方式有bug会重复
id: utils.generateUUID(),
// 起始节点
source: sourceNodeModel.id,
sourceColumn: columns,
sourceAnchor: sourceAnchor ? sourceAnchor.anchorIndex : "",
// 终止节点/位置
relational: relational,
target: {
x: event.x,
y: event.y
},
type: self.graph.$C.edge.type || "top-cubic",
style: themeStyle.edgeStyle.default || self.graph.$C.edge.style
});
self.drawEdge.isMoving = true;
},
move(event) {
let self = this;
if (self.drawEdge.isMoving && self.drawEdge.currentLine) {
self.graph.updateItem(self.drawEdge.currentLine, {
target: {
x: event.x,
y: event.y
}
});
}
},
stop(event) {
let self = this;
if (self.drawEdge.isMoving) {
if (self.drawEdge.currentLine === event.item) {
// 画线过程中点击则移除当前画线
self.graph.removeItem(event.item);
} else {
let targetNode = event.item;
let targetNodeModel = toRaw(targetNode.getModel());//节点的数据模型
let targetAnchor = null;
// 锚点数据
let anchorPoints = targetNode.getAnchorPoints();
// 处理线条目标点
let relational = self.drawEdge.currentLine.getModel().relational//边的数据模型start函数中定义的包含该节点的关联关系
let starts = false;
let relationalItem = null;
if (relational) {
relational.map(item => {
if (item) {
if (item.childId === targetNodeModel.tableId) {
starts = true
relationalItem = item
} else {
starts = false
}
}
})
}
// 去掉关系验证
// starts = true
if (starts) {
if (anchorPoints && anchorPoints.length > 0) {
// 获取距离指定坐标最近的一个锚点
targetAnchor = targetNode.getLinkPoint({
x: event.x,
y: event.y
});
}
let columns = []
targetNodeModel.columns.forEach(item => {
let column = {
columnName: item.columnName,
columnComment: item.columnComment
}
columns.push(column)
})
self.graph.updateItem(self.drawEdge.currentLine, {
target: targetNodeModel.id,
relationalItem: relationalItem,
targetAnchor: targetAnchor ? targetAnchor.anchorIndex : "",
targetColumn: columns,
});
// ************** 记录historyData的逻辑 start **************
if (self.historyData) {
let graph = self.graph;
// 如果当前点过【撤销】了,拖拽节点后没有【重做】功能
// 重置undoCount拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录
if (vm.getUndoCount() > 0) {
vm.editHistoryIndex(vm.getHistoryIndex() - vm.getUndoCount()); // 此时的historyIndex应当更新为【撤销】后所在的索引位置
for (let i = 1; i <= vm.getUndoCount(); i++) {
let key = `graph_history_${vm.getHistoryIndex() + i}`;
vm.removeHistoryData(key);
}
vm.editUndoCount(0)
} else {
// 正常顺序执行的情况,记录拖拽线 前的数据状态
let key = `graph_history_${vm.getHistoryIndex()}`;
vm.addHistoryData(key, self.historyData);
}
// 记录拖线后的数据状态
const index = vm.getHistoryIndex() + 1;
vm.editHistoryIndex(index);
let key = `graph_history_${index}`;
let currentData = JSON.stringify(graph.save());
vm.addHistoryData(key, currentData);
}
} else {
if (self.evtInfo.node.getModel().tableId === targetNodeModel.tableId) {
ElMessage.warning("不可连接自身")
} else {
ElMessage.warning("两表之间无关联关系")
}
self.graph.removeItem(self.drawEdge.currentLine);
}
// ************** 记录historyData的逻辑 end **************
}
}
self.drawEdge.currentLine = null;
self.drawEdge.isMoving = false;
self.evtInfo = null;
}
}
}
};

View File

@@ -0,0 +1,57 @@
/**
* @author: clay
* @data: 2019/07/16
* @description: edit mode: 鼠标拖动节点的交互(记录拖拽前后的数据,用于【撤销】和【重做】)
*/
// 用来获取调用此js的vue组件实例this
let vm = null
let historyData = null
const sendThis = (_this) => {
vm = _this
}
export default {
sendThis, // 暴露函数
name: 'drag-event-edit',
options: {
getEvents() {
return {
'node:dragstart': 'onNodeDragstart',
'node:dragend': 'onNodeDragend'
}
},
onNodeDragstart() {
let graph = vm.getGraph()
if(graph.cfg){
historyData = JSON.stringify(graph.save())
}
},
onNodeDragend() {
if (historyData) {
let graph = vm.getGraph()
// 如果当前点过【撤销】了,拖拽节点后没有【重做】功能
// 重置undoCount拖拽后的数据给(当前所在historyIndex + 1),且清空这个时间点之后的记录
if (vm.getUndoCount() > 0) {
vm.editHistoryIndex( vm.getHistoryIndex() - vm.getUndoCount()) // 此时的historyIndex应当更新为【撤销】后所在的索引位置
for (let i = 1; i <= vm.getUndoCount(); i++) {
let key = `graph_history_${vm.getHistoryIndex() + i}`
vm.removeHistoryData(key)
}
vm.editUndoCount(0);
} else {
// 正常顺序执行的情况,记录拖拽前的数据状态
let key = `graph_history_${vm.getHistoryIndex()}`
vm.addHistoryData(key, historyData)
}
// 记录拖拽后的数据状态
const index=vm.getHistoryIndex()+ 1;
vm.editHistoryIndex(index);
let key = `graph_history_${index}`
let currentData = JSON.stringify(graph.save())
vm.addHistoryData(key, currentData)
}
}
}
}

View File

@@ -0,0 +1,66 @@
/**
* @author: clay
* @data: 2019/07/16
* @description: edit mode: 悬浮交互
*/
// 用来获取调用此js的vue组件实例this
let vm = null;
let hourItem = null;
const sendThis = (topo) => {
vm = topo;
};
export default {
sendThis, // 暴露函数
name: "hover-event-edit",
options: {
getEvents() {
return {
"node:mouseover": "onNodeHover",
// "node:mouseout": "onNodeOut",
// "mouseleave":"onNodeLeave",
};
},
onNodeHover(event) {
let graph = vm.getGraph();
let hoverNode = event.item;
const name = event.shape.get("name");//todo ??
const item = event.item;
if (name && name.startsWith("item")) {
graph.updateItem(item, {//更新元素,包括更新数据、样式等
selectedIndex: Number(name.split("-")[1])
});
} else {
graph.updateItem(item, {
selectedIndex: NaN
});
}
if (name && name.startsWith("marker")) {
hoverNode.setState("hover", true, graph);
hourItem=hoverNode;
}else {
if (hourItem!=null){
hourItem.setState("hover", false)
}
}
},
// onNodeOut(event) {
// console.log('移出节点')
// const name = event.shape.get("name");
// let hoverNode = event.item;
// console.log('name',name)
// if (name && name.startsWith("marker")) {
// hoverNode.setState("hover", false);
// }
// if (hourItem!=null){
// hourItem.setState("hover", false)
// }
// hoverNode.setState("hover", false);
// },
// onNodeLeave(event) {
// console.log('移出节点2')
// if (hourItem!=null){
// hourItem.setState("hover", false)
// }
// }
}
};

View File

@@ -0,0 +1,30 @@
/**
* @author: clay
* @data: 2019/07/16
* @description: register behaviors
*/
import dragAddEdge from './drag-add-edge'
import hoverEventEdit from './hover-event-edit'
import dragEventEdit from './drag-event-edit'
import keyupEventEdit from './keyup-event-edit'
import clickErNode from './click-er-node'
import clickErEdge from './click-er-edge'
const obj = {
dragAddEdge,
hoverEventEdit,
dragEventEdit,
keyupEventEdit,
clickErNode,
clickErEdge
}
export default {
obj,
register(G6) {
Object.values(obj).map(item => {
G6.registerBehavior(item.name, item.options)
})
}
}

View File

@@ -0,0 +1,67 @@
/**
* @author: clay
* @data: 2019/07/16
* @description: edit mode: 键盘事件的交互,主要是删除节点和连线(记录删除前后的数据,用于【撤销】和【重做】)
*/
// 用来获取调用此js的vue组件实例this
let vm = null;
const sendThis = (_this) => {
vm = _this;
};
export default {
sendThis, // 暴露函数
name: "keyup-event-edit",
options: {
getEvents() {
return {
"keyup": "onKeyup",
"keydown": "onKeydown",
};
},
onKeyup(event) {
let graph = vm.getGraph();
let selectedNodes = graph.findAllByState("node", "selected");
let selectedEdges = graph.findAllByState("edge", "selected");
//按住键盘delete-46删除功能
if (event.keyCode === 46 && (selectedNodes.length > 0 || selectedEdges.length > 0)) {
// ************** 记录【删除】前的数据状态 start **************
let historyData = JSON.stringify(graph.save());
let key = `graph_history_${vm.getHistoryIndex()}`;
vm.addHistoryData(key, historyData);
// ************** 记录【删除】前的数据状态 end **************
// 开始删除
selectedNodes.forEach(item=>graph.removeItem(item))
selectedEdges.forEach(item=>graph.removeItem(item))
// ************** 记录【删除】后的数据状态 start **************
// 如果当前点过【撤销】了,拖拽节点后将取消【重做】功能
// 重置undoCount【删除】后的数据状态给(当前所在historyIndex + 1),且清空这个时间点之后的记录
if (vm.getUndoCount() > 0) {
vm.editHistoryIndex( vm.getHistoryIndex() - vm.getUndoCount()); // 此时的historyIndex应当更新为【撤销】后所在的索引位置
for (let i = 1; i <= vm.getUndoCount(); i++) {
let key = `graph_history_${vm.getHistoryIndex() + i}`;
vm.removeHistoryData(key);
}
vm.editUndoCount(0);
}
// 记录【删除】后的数据状态
const index=vm.getHistoryIndex()+ 1;
vm.editHistoryIndex(index);
key = `graph_history_${index}`;
let currentData = JSON.stringify(graph.save());
vm.addHistoryData(key, currentData);
// ************** 记录【删除】后的数据状态 end **************
} else if (event.keyCode == 17) {//ctrl键将状态传给父组件在clickErNode里面编写功能按住ctrl滚动后放大缩小ER图
vm.editClickCtrl(false)//松开则为false
}
},
onKeydown(event) {
if (event.keyCode == 17) {//ctrl键
vm.editClickCtrl(true)//为true时一直按着ctrl以实现滚轮放大缩小功能
}
}
}
};

View File

@@ -0,0 +1,91 @@
<template>
<div class="footer-bar">
<div class="links">
<ul>
<li>
<a href="https://github.com/clay" target="_blank">
<i class="iconfont icon-github"></i>
</a>
</li>
<li>
<a href="javascript:void(0);">
<i class="fab fa-weixin"></i>
</a>
</li>
<li>
<a href="javascript:void(0);">更新日志</a>
</li>
<li>
<a href="https://github.com/clay/style-guide" target="_blank">代码规范</a>
</li>
</ul>
</div>
<div class="copyright">
<ul>
<li class="nav-item">
<span>ClayTop 0.1.0</span>
</li>
<li>
<span>|</span>
</li>
<li class="nav-item">
<span>ChainCloud FE</span>
</li>
</ul>
</div>
</div>
</template>
<script setup>
const home = () => {
this.$router.push({
path: '/'
})
}
</script>
<style lang="scss" scoped>
.footer-bar {
.links {
float: left;
ul {
padding-left: 60px;
}
li {
display: inline;
padding: 10px;
}
i {
font-size: 16px;
}
a {
color: rgba(255, 255, 255, 0.45);
}
}
.copyright {
float: right;
ul {
padding-right: 60px;
}
li {
display: inline;
padding: 5px;
}
i {
font-size: 16px;
}
a {
color: rgba(255, 255, 255, 0.45);
}
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="header-bar">
<el-row>
<el-col :span="8" style="text-align: left">
<router-link to="/">
<div class="navbar-brand">
<img id="logo" src="@/assets/logo.svg" width="38px" height="38px" alt="ClayTop"/>
<span> ClayTop </span>
</div>
</router-link>
</el-col>
<el-col :span="8" style="text-align: center">
<div class="navbar-title">&nbsp;</div>
</el-col>
<el-col :span="8" style="text-align: right">
<div class="navbar-btns">
<el-button type="primary" @click="home" link>首页</el-button>
<el-button type="primary" @click="about" link>关于ClayTop</el-button>
<el-button type="primary" @click="tutorial" link>使用教程</el-button>
<el-button type="primary" @click="$message('敬请期待...')" link>其它</el-button>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
const home = () => {
this.$router.push({
path: '/'
})
}
const about = () => {
this.$router.push({
path: '/about'
})
}
const tutorial = () => {
this.$message({
dangerouslyUseHTMLString: true,
message: '<div style="text-align: left">时间原因,暂未编写。<br/><br/>目前【开发方式】和【使用方式】可先参考 README.md。 <br/><br/><div style="text-align: right">—— by clay</div></div>',
type: 'info',
center: true,
duration: 5000,
showClose: false,
offset: 100
})
}
</script>
<style lang="scss" scoped>
.header-bar {
.navbar-brand {
display: inline-block;
margin: 0 18px 0 35px;
/*padding: 0 0 0 8px;*/
line-height: 22px;
font-size: 20px;
img {
vertical-align: middle;
}
span {
vertical-align: middle;
/*font-family: "Microsoft YaHei";*/
color: #06080A;
}
}
.navbar-title {
vertical-align: middle;
text-align: center;
font-size: 20px;
}
.navbar-btns {
vertical-align: middle;
text-align: right;
.el-button:last-child {
margin-right: 20px;
}
.el-button--text {
color: #06080A;
}
}
}
</style>

View File

@@ -0,0 +1,13 @@
/**
* @author: clay
* @data: 2019/08/16
* @description: 线条的后期设置
*/
export default {
type: 'top-cubic',
style: {
startArrow: false,
endArrow: true
}
}

View File

@@ -0,0 +1,11 @@
/**
* @author: clay
* @data: 2019/08/16
* @description: 配置
*/
import edge from './edge'
export default {
edge
}

View File

@@ -0,0 +1,30 @@
/**
* @author: Clay
* @data: 2019/07/18
* @description: 线公共方法
*/
import utils from '../../utils'
export default {
draw(cfg, group) {
const { startPoint, endPoint } = cfg
const keyShape = group.addShape('path', {
className: 'edge-shape',
attrs: {
...cfg.style,
path: [
['M', startPoint.x, startPoint.y],
['L', endPoint.x, endPoint.y]
]
},
name: 'edge-shape'
})
keyShape.attrs.endArrow = true
return keyShape
},
setState(name, value, item) {
// 设置边状态
utils.edge.setState(name, value, item)
}
}

View File

@@ -0,0 +1,18 @@
/**
* @author: clay
* @data: 2019/07/18
* @description: register edges
*/
import topCubic from './top-cubic'
const obj = {
topCubic
}
export default function(G6) {
Object.values(obj).map(item => {
G6.registerEdge(item.name, item.options, item.extendName)
})
}

View File

@@ -0,0 +1,14 @@
/**
* @author: Clay
* @data: 2019/07/18
* @description: 曲线
*/
import base from './base'
export default {
name: 'top-cubic',
extendName: 'cubic',
options: {
...base
}
}

View File

@@ -0,0 +1,92 @@
<template>
<button
class="cc-button"
@click="handleClick"
:disabled="buttonDisabled"
:class="[
type ? 'cc-button--' + type : '',
buttonSize ? 'cc-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-plain': plain
}
]"
>
<span><slot></slot></span>
</button>
</template>
<script setup>
import {defineProps, computed} from "vue";
const emit = defineEmits(['click'])
const props = defineProps({
type: {
type: String,
default: 'default'
},
size: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
plain: {
type: Boolean,
default: false
}
})
const buttonSize = computed(() => {
return props.size
})
const buttonDisabled = computed(() => {
return props.disabled
})
const handleClick = (evt) => {
emit('click', evt)
}
</script>
<style lang="scss" scoped>
.cc-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
text-align: center;
-webkit-box-sizing: border-box;
box-sizing: border-box;
outline: 0;
margin: 0;
-webkit-transition: .1s;
transition: .1s;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
.cc-button:focus, .cc-button:hover {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
.cc-button--mini, .cc-button--mini.is-round {
padding: 7px 15px;
}
.cc-button--mini, .cc-button--small {
font-size: 12px;
border-radius: 3px;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="cc-checkbox" :class="{ 'checkbox-checked': checkVal }">
<input
id="checkbox"
type="checkbox"
v-model="checkVal"
@change="handleChange"
/>
<label for="checkbox"></label>
<slot></slot>
</div>
</template>
<script setup>
import {ref} from "vue";
const emit = defineEmits(['change'])
const checkVal = ref(false);
const handleChange = () => {
emit('change', checkVal.value)
}
</script>
<style lang="scss" scoped>
.cc-checkbox {
display: inline-block;
color: #606266;
font-size: 14px;
input[type='checkbox'] + label {
margin: 0 8px;
cursor: pointer;
}
input[type='checkbox'] + label::before {
content: '\a0';
display: inline-block;
position: relative;
border: 1px solid #dcdfe6;
border-radius: 2px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 14px;
height: 14px;
line-height: 0.9;
background-color: #fff;
z-index: 1;
-webkit-transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46);
transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46);
}
input[type='checkbox']:checked + label::before {
content: '\2714';
color: #fff;
background-color: #409eff;
border-color: #409eff;
}
input[type='checkbox'] {
opacity: 0;
outline: 0;
position: absolute;
margin: 0;
width: 0;
height: 0;
z-index: -1;
}
}
.checkbox-checked {
color: #409eff;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="graph-op">
<span v-for="edgeItem in dropdownItems" @click="onItemClick(edgeItem.value, $event)">{{ edgeItem.label }}</span>
</div>
</template>
<script setup>
import $ from 'jquery'
import {defineProps, ref, watch} from "vue";
const emit = defineEmits(['change'])
const props = defineProps({
dropdownItems: {
type: Array,
default() {
return []
}
},
defaultIndex: {
type: Number,
default() {
return 0
}
}
})
const activeIndex = ref(props.defaultIndex)
const timeout = ref(null)
//todo 可能有问题
watch(() => activeIndex.value, (value) => {
emit('change', activeIndex.value)
})
const onItemClick = (index, e) => {
let lineList = $('.iconfonts')
for (let i = 0; i < lineList.length; i++) {
lineList[i].style.backgroundColor = '#ffffff'
}
// if (e.path[0].style.backgroundColor == 'rgb(255, 255, 255)') {
// e.path[0].style.backgroundColor = '#ebeef2'
// }
if (index !== activeIndex.value) {
activeIndex.value = index
}
}
</script>
<style lang="scss" scoped>
.iconfonts {
color: #666666;
font-size: 12px;
line-height: 20px;
height: 20px;
padding-bottom: 2px;
}
.graph-op {
display: inline-block;
vertical-align: middle;
span {
width: 120px;
margin: 0 6px;
color: #a8a8a8;
text-align: center;
border-radius: 2px;
display: inline-block;
border: 1px solid rgba(2, 2, 2, 0);
}
span:hover {
cursor: pointer;
border: 1px solid #E9E9E9;
}
}
</style>

View File

@@ -0,0 +1,17 @@
/**
* @author: clay
* @data: 2019/11/14
* @description: ClayTop内部的通用组件
*/
import Checkbox from './checkbox.vue'
import Button from './button.vue'
import Dropdown from './dropdown.vue'
import Loading from './loading.vue'
export {
Checkbox,
Button,
Dropdown,
Loading
}

View File

@@ -0,0 +1,91 @@
<template>
<div class="model-overlay">
<div class="cc-loading">
<svg class="circular" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" fill="none" stroke="#409EFF" stroke-width="5%" stroke-linecap="round"/>
</svg>
</div>
<span class="loading-text">{{ loadingText }}</span>
</div>
</template>
<script setup>
import {defineProps} from "vue";
const props = defineProps({
loadingText: {
type: String,
default: '加载中...'
}
})
</script>
<style lang="scss" scoped>
.model-overlay {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
text-align: center;
z-index: 1000;
overflow: auto;
background-color: transparent;
}
.cc-loading {
position: absolute;
width: 50px;
top: 45%;
left: 50%;
&:before {
content: '';
display: block;
padding-top: 100%;
}
.circular {
position: absolute;
top: 0;
left: 0;
animation: rotate 2s linear infinite;
}
circle {
animation: circle-dash 1.5s ease-in-out infinite;
}
}
@keyframes circle-dash {
0% {
stroke-dasharray: 1, 125;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 100, 125;
stroke-dashoffset: -25px;
}
100% {
stroke-dasharray: 100, 125;
stroke-dashoffset: -125px;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-text {
position: absolute;
top: calc(45% + 60px);
left: calc(50% - 10px);
color: #409EFF;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,162 @@
/**
* @author: clay
* @data: 2019/07/05
* @description: 图的布局方式/图的初始化
*/
// import d3 from '../plugins/d3-installer'
import * as d3 from 'd3'
import theme from '../theme'
/**
* 图的布局方式/图的初始化
* @type {{commonGraph: (function(*, *): G6.Graph)}}
*/
const initGraph = {
/**
* 一般布局
* @param G6
* @param options
* @returns {G6.Graph}
*/
commonGraph: function(G6, options) {
let graphData = options.graphData
let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体
// 生成G6实例
let graph = new G6.Graph({
plugins: options.plugins,//向 graph 注册插件
container: options.container,//图的 DOM 容器,可以传入该 DOM 的 id 或者直接传入容器的 HTML 节点对象。
width: options.width,//指定画布宽度,单位为 'px',默认为画布容器宽度。
height: options.height,//指定画布高度
// layout: {
// type: 'random',
// width: options.width,
// height: options.height
// },
defaultNode: {//默认状态下节点的配置
type: 'dice-er-box',
labelCfg: {
position: 'bottom'
}
},
defaultEdge: {//默认状态下线的配置
type: 'top-cubic',
labelCfg: {
position: 'center',
autoRotate: false
}
},
nodeStateStyles: themeStyle.nodeStyle,//各个状态下节点的样式
// nodeStyle: {
// selected: {
// shadowColor: '#626262',
// shadowBlur: 8,
// shadowOffsetX: -1,
// shadowOffsetY: 3
// }
// },
edgeStateStyles:{
edgeStyle: {
default: {
stroke: '#e2e2e2',
lineWidth: 3,
lineAppendWidth: 10
},
selected: {
shadowColor: '#626262',
shadowBlur: 3
}
},
},//各个状态下边的样式
modes: options.modes//设置画布的交互模式
})
// 将 read 方法分解成 data() 和 render 方法,便于整个生命周期的管理
graph.read(graphData)
// 渲染图
graph.render()
// 返回G6实例
return graph
},
/**
* 力导布局
* @param G6
* @param options
* @returns {*}
*/
forceLayoutGraph: function(resolve, G6, options) {
let graphData = options.graphData
let themeStyle = theme.defaultStyle // todo...先使用默认主题,后期可能增加其它风格的主体
// 生成G6实例
let graph = new G6.Graph({
container: options.container,
width: options.width,
height: options.height,
nodeStateStyles: themeStyle.nodeStyle,
edgeStateStyles: themeStyle.edgeStyle
})
// 初始化力导布局
let simulation = d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id(function(d) {
return d.id
})
.distance(linkDistance)
.strength(0.5)
)
.force('charge', d3.forceManyBody().strength(-500).distanceMax(500).distanceMin(100))
.force('center', d3.forceCenter(options.width / 2, options.height / 2))
// 定义节点数据
simulation.nodes(graphData.nodes).on('tick', ticked)
// 定义连线数据
let edges = []
for (let i = 0; i < graphData.edges.length; i++) {
edges.push({
id: graphData.edges[i].id,
source: graphData.edges[i].source,
target: graphData.edges[i].target
})
}
simulation.force('link').links(edges)
graph.data(graphData)
graph.render()
function linkDistance(d) {
return 150
}
function ticked() {
// protect: planA: 移除事件监听器 planB: 手动停止力模拟
if (graph.destroyed) {
// simulation.nodes(graphData.nodes).on('tick', null)
simulation.stop()
return
}
if (!graph.get('data')) {
// 若是第一次渲染,定义数据,渲染
graph.data(graphData)
graph.render()
} else {
// 后续渲染,直接刷新所有点和边的位置
graph.refreshPositions()
}
}
// 控制时间: 只布局10秒
let t = setTimeout(function() {
simulation.stop()
resolve(graph)
}, 10000)
// 判断force-layout结束
simulation.on('end', () => {
clearTimeout(t)
resolve(graph)
})
}
}
export default initGraph

View File

@@ -0,0 +1,12 @@
<template>
<el-button @click="handleClick">设计</el-button>
</template>
<script setup>
import {useRouter} from "vue-router";
const router = useRouter()
const handleClick=()=>{
router.push('/top/edit/1')
}
</script>

View File

@@ -0,0 +1,302 @@
/**
* @author: clay
* @data: 2021/5/15 0:16
* @email: clay@hchyun.com
* @description: draw anchor 绘制锚点
*/
import utils from '../../utils/index'
const itemHeight = 30;
export default {
name: 'dice-er-box',
options: {
/**
* 响应节点的状态变化。
* 在需要使用动画来响应状态变化时需要被复写
* @param {String} name 状态名称
* @param {Object} value 状态值
* @param {item} item 节点
*/
setState(name, value, item) {
// 设置节点状态
utils.node.setState(name, value, item);
// 设置锚点状态
utils.anchor.setState(name, value, item);
},
draw(cfg, group) {
const width = 250;
const height = 316;
const itemCount = 10;
const boxStyle = {
stroke: "#096DD9",
radius: 4,
};
const {
columns = [],
startIndex = 0,
selectedIndex,
collapsed,
icon,
} = cfg;
const list = columns;
const afterList = list.slice(
Math.floor(startIndex),
Math.floor(startIndex + itemCount - 1)
);
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,
},
});
//设置收缩部分的容器
group.addShape("rect", {
attrs: {
x: 0,
y: collapsed ? 30 : 300,
height: 15,
width,
fill: "#eee",
radius: [0, 0, boxStyle.radius, boxStyle.radius],
cursor: "pointer",
},
name: collapsed ? "expand" : "collapse",
});
//设置收缩显示字符
group.addShape("text", {
attrs: {
x: width / 2 - 6,
y: (collapsed ? 30 : 300) + 12,
text: collapsed ? "+" : "-",
width,
fill: "#000",
radius: [0, 0, boxStyle.radius, boxStyle.radius],
cursor: "pointer",
},
name: collapsed ? "expand" : "collapse",
});
//设置外边框
const keyshape = group.addShape("rect", {
attrs: {
name: 'border',
x: 0,
y: 0,
width,
height: collapsed ? 45 : height,
...boxStyle,
},
//是否被允许拖拽
draggable: true
});
//如果收缩状态,则返回当前图形
if (collapsed) {
return keyshape;
}
//添加空白组
const listContainer = group.addGroup({});
//todo 设置裁剪对象,字体加粗?
listContainer.setClip({
type: "rect",
attrs: {
x: -8,
y: 30,
width: width + 16,
height: 300 - 30,
},
});
listContainer.addShape({
type: "rect",
attrs: {
x: 1,
y: 30,
width: width - 2,
height: 300 - 30,
fill: "#fff",
},
draggable: true,
});
//如果list中的column字段超过10个
if (list.length > 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: height - 30,
...barStyle.boxStyle,
},
});
//设置矩形高度
const indexHeight =
afterList.length > itemCount ?
(afterList.length / list.length) * height :
10;
listContainer.addShape("rect", {
attrs: {
y: 30 +
barStyle.padding +
(startIndex / (list.length-8)) * (height - 30),
x: width - barStyle.padding - barStyle.width,
width: barStyle.width,
height: Math.min(height, indexHeight),
...barStyle.innerStyle,
},
});
}
//渲染显示区域
if (afterList) {
afterList.forEach((e, i) => {
//设置选中的列
const isSelected = Math.floor(startIndex) + i === Number(selectedIndex);
let {
columnName = "", columnType,columnComment
} = e;
if (columnComment){
columnName+= " : " + columnComment
}
if (columnType) {
columnName += " - " + columnType;
}
const label = columnName.length > 26 ? columnName.slice(0, 24) + "..." : columnName;
listContainer.addShape("rect", {
attrs: {
x: 1,
y: i * itemHeight - itemHeight / 2 + offsetY,
width: width - 4,
height: itemHeight,
radius: 2,
fill:isSelected ? "#ddd" : "#fff",
lineWidth: 1,
cursor: "pointer",
},
name: `item-${Math.floor(startIndex) + i}-content`,
draggable: true,
});
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",
},
name: `item-${Math.floor(startIndex) + i}`,
});
//未来设置字段之间有锚点
// if (!cfg.hideDot) {
// utils.anchor.erDrawLeft(group, label, 0, i * itemHeight + offsetY)
// utils.anchor.erDrawLeft(group,label,width,i * itemHeight + offsetY)
// listContainer.addShape("marker", {
// attrs: {
// x: 0,
// y: i * itemHeight + offsetY,
// r: 3,
// stroke: boxStyle.stroke,
// fill: "white",
// radius: 2,
// lineWidth: 1,
// cursor: "crosshair",
// },
//
// name: 'marker-shape'
// });
// listContainer.addShape("marker", {
// attrs: {
// x: width,
// y: i * itemHeight + offsetY,
// r: 3,
// stroke: boxStyle.stroke,
// fill: "white",
// radius: 2,
// lineWidth: 1,
// cursor: "crosshair",
//
//
// },
// name: 'marker-shape'
// });
// }
});
}
return keyshape;
},
// getAnchorPoints() {
// return [
// [0, 0],
// [1, 0],
// ];
// },
// 绘制后附加锚点
afterDraw(cfg, group) {
// 绘制锚点
utils.anchor.drawMark(cfg, group)
},
}
}

View File

@@ -0,0 +1,20 @@
/**
* @author: clay
* @data: 2019/07/05
* @description: register nodes
*/
import diceErBox from './dice-er-box'
const obj = {
diceErBox
}
export default {
obj,
register(G6) {
Object.values(obj).map(item => {
G6.registerNode(item.name, item.options, item.extendName)
})
}
}

View File

@@ -0,0 +1,177 @@
/**
* @author: clay
* @data: 2019/11/20
* @description: dark style
*/
export default {
// 节点样式
nodeStyle: {
default: {
stroke: '#CED4D9',
fill: 'transparent',
shadowOffsetX: 0,
shadowOffsetY: 4,
shadowBlur: 10,
shadowColor: 'rgba(13, 26, 38, 0.08)',
lineWidth: 1,
radius: 4,
strokeOpacity: 0.7
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
// shadowColor: '#626262',
// shadowBlur: 8,
// shadowOffsetX: -1,
// shadowOffsetY: 3
},
unselected: {
shadowColor: ''
}
},
//todo 节点标签样式??修改不起
nodeLabelCfg: {
positions: 'center',
style: {
fill: '#fff'
}
},
// 连线样式
edgeStyle: {
default: {
stroke: '#000000',
lineWidth: 2,
strokeOpacity: 0.92,
lineAppendWidth: 10
// endArrow: true
},
active: {
shadowColor: 'red',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
inactive: {
shadowColor: ''
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
unselected: {
shadowColor: ''
}
},
// 锚点样式
anchorStyle: {
default: {
radius: 3,
symbol: 'circle',
fill: '#FFFFFF',
fillOpacity: 0,
stroke: '#1890FF',
strokeOpacity: 0,
cursor: 'crosshair'
},
hover: {
fillOpacity: 1,
strokeOpacity: 1
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 1,
strokeOpacity: 1
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
// 锚点背景样式
anchorBgStyle: {
default: {
radius: 10,
symbol: 'circle',
fill: '#1890FF',
fillOpacity: 0,
stroke: '#1890FF',
strokeOpacity: 0,
cursor: 'crosshair'
},
hover: {
fillOpacity: 1,
strokeOpacity: 1
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 0.3,
strokeOpacity: 0.5
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
nodeActivedOutterStyle: { lineWidth: 0 },
groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 },
nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' },
groupActivedStyle: { stroke: '#1890FF' },
edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' },
nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 },
groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 },
groupBackgroundPadding: [40, 10, 10, 10],
groupLabelOffsetX: 10,
groupLabelOffsetY: 10,
edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
edgeLabelRectPadding: 4,
edgeLabelRectStyle: { fill: 'white' },
nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
groupStyle: { stroke: '#CED4D9', radius: 4 },
groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' },
multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 },
dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 },
dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 },
anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 },
anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 },
anchorHotsoptActivedStyle: { radius: 14 },
anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' },
nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' },
edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 },
nodeSelectedBoxStyle: { stroke: '#C2C2C2' },
cursor: {
panningCanvas: '-webkit-grabbing',
beforePanCanvas: '-webkit-grab',
hoverNode: 'move',
hoverEffectiveAnchor: 'crosshair',
hoverEdge: 'default',
hoverGroup: 'move',
hoverUnEffectiveAnchor: 'default',
hoverEdgeControllPoint: 'crosshair',
multiSelect: 'crosshair'
},
nodeDelegationStyle: {
stroke: '#1890FF',
fill: '#1890FF',
fillOpacity: .08,
lineDash: [4, 4],
radius: 4,
lineWidth: 1
},
edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 }
}

View File

@@ -0,0 +1,179 @@
/**
* @author: clay
* @data: 2019/08/15
* @description: default style
*/
export default {
// 节点样式
nodeStyle: {
default: {
stroke: '#CED4D9',
fill: 'transparent',
// shadowOffsetX: 0,
// shadowOffsetY: 4,
shadowBlur: 10,
shadowColor: 'rgba(13, 26, 38, 0.08)',
lineWidth: 1,
radius: 4,
strokeOpacity: 0.7
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 2,
// shadowOffsetX: 0,
// shadowOffsetY: 0,
// fontSize:'50'
// shadowColor: '#626262',
// shadowBlur: 8,
// shadowOffsetX: -1,
// shadowOffsetY: 3
},
unselected: {
shadowColor: ''
}
},
// 节点标签样式
nodeLabelCfg: {
positions: 'center',
style: {
fill: '#000'
}
},
// 连线样式
edgeStyle: {
default: {
stroke: '#A3B1BF',
lineWidth: 2,
strokeOpacity: 0.92,
lineAppendWidth: 10
// endArrow: true
},
active: {
shadowColor: 'red',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
inactive: {
shadowColor: ''
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
unselected: {
shadowColor: ''
}
},
// 锚点样式
anchorStyle: {
default: {
r: 3,
symbol: 'circle',
lineWidth: 1,
fill: '#FFFFFF',
fillOpacity: 1,
stroke: '#096DD9',
strokeOpacity: 1,
cursor: 'crosshair'
},
hover: {
fillOpacity: 0.3,
strokeOpacity: 0.5
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 1,
strokeOpacity: 1
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
// 锚点背景样式
anchorBgStyle: {
default: {
r: 10,
symbol: 'circle',
fill: '#1890FF',
fillOpacity: 0,
stroke: '#1890FF',
strokeOpacity: 0,
cursor: 'crosshair'
},
hover: {
fillOpacity: 1,
strokeOpacity: 1
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 0.3,
strokeOpacity: 0.5
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
nodeActivedOutterStyle: { lineWidth: 0 },
groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 },
nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' },
groupActivedStyle: { stroke: '#1890FF' },
edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' },
nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 },
groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 },
groupBackgroundPadding: [40, 10, 10, 10],
groupLabelOffsetX: 10,
groupLabelOffsetY: 10,
edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
edgeLabelRectPadding: 4,
edgeLabelRectStyle: { fill: 'white' },
nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
groupStyle: { stroke: '#CED4D9', radius: 4 },
groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' },
multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 },
dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 },
dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 },
anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 },
anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 },
anchorHotsoptActivedStyle: { radius: 14 },
anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' },
nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' },
edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 },
nodeSelectedBoxStyle: { stroke: '#C2C2C2' },
cursor: {
panningCanvas: '-webkit-grabbing',
beforePanCanvas: '-webkit-grab',
hoverNode: 'move',
hoverEffectiveAnchor: 'crosshair',
hoverEdge: 'default',
hoverGroup: 'move',
hoverUnEffectiveAnchor: 'default',
hoverEdgeControllPoint: 'crosshair',
multiSelect: 'crosshair'
},
nodeDelegationStyle: {
stroke: '#1890FF',
fill: '#1890FF',
fillOpacity: .08,
lineDash: [4, 4],
radius: 4,
lineWidth: 1
},
edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 }
}

View File

@@ -0,0 +1,15 @@
/**
* @author: clay
* @data: 2019/08/15
* @description: 编辑器主题样式 - 节点、连线的预设样式
*/
import defaultStyle from './default-style'
import darkStyle from './dark-style'
import officeStyle from './office-style'
export default {
defaultStyle,
darkStyle,
officeStyle
}

View File

@@ -0,0 +1,177 @@
/**
* @author: clay
* @data: 2019/11/21
* @description: office style
*/
export default {
// 节点样式
nodeStyle: {
default: {
stroke: '#CED4D9',
fill: '#FFFFFF',
shadowOffsetX: 0,
shadowOffsetY: 4,
shadowBlur: 10,
shadowColor: 'rgba(13, 26, 38, 0.08)',
lineWidth: 1,
radius: 4,
strokeOpacity: 0.7
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 4,
shadowOffsetX: 1,
shadowOffsetY: 1
// shadowColor: '#626262',
// shadowBlur: 8,
// shadowOffsetX: -1,
// shadowOffsetY: 3
},
unselected: {
shadowColor: ''
}
},
// 节点标签样式
nodeLabelCfg: {
positions: 'center',
style: {
fill: '#000'
}
},
// 连线样式
edgeStyle: {
default: {
stroke: '#41c23a',
lineWidth: 2,
strokeOpacity: 0.92,
lineAppendWidth: 10
// endArrow: true
},
active: {
shadowColor: 'red',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
inactive: {
shadowColor: ''
},
selected: {
shadowColor: '#ff240b',
shadowBlur: 4,
shadowOffsetX: 0,
shadowOffsetY: 0
},
unselected: {
shadowColor: ''
}
},
// 锚点样式
anchorStyle: {
default: {
radius: 3,
symbol: 'circle',
fill: '#FFFFFF',
fillOpacity: 0,
stroke: '#1890FF',
strokeOpacity: 0,
cursor: 'crosshair'
},
hover: {
fillOpacity: 1,
strokeOpacity: 1
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 1,
strokeOpacity: 1
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
// 锚点背景样式
anchorBgStyle: {
default: {
radius: 10,
symbol: 'circle',
fill: '#1890FF',
fillOpacity: 0,
stroke: '#1890FF',
strokeOpacity: 0,
cursor: 'crosshair'
},
hover: {
fillOpacity: 1,
strokeOpacity: 1
},
unhover: {
fillOpacity: 0,
strokeOpacity: 0
},
active: {
fillOpacity: 0.3,
strokeOpacity: 0.5
},
inactive: {
fillOpacity: 0,
strokeOpacity: 0
}
},
nodeActivedOutterStyle: { lineWidth: 0 },
groupSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
nodeSelectedOutterStyle: { stroke: '#E0F0FF', lineWidth: 2 },
edgeActivedStyle: { stroke: '#1890FF', strokeOpacity: .92 },
nodeActivedStyle: { fill: '#F3F9FF', stroke: '#1890FF' },
groupActivedStyle: { stroke: '#1890FF' },
edgeSelectedStyle: { lineWidth: 2, strokeOpacity: .92, stroke: '#A3B1BF' },
nodeSelectedStyle: { fill: '#F3F9FF', stroke: '#1890FF', fillOpacity: .4 },
groupSelectedStyle: { stroke: '#1890FF', fillOpacity: .92 },
groupBackgroundPadding: [40, 10, 10, 10],
groupLabelOffsetX: 10,
groupLabelOffsetY: 10,
edgeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
edgeLabelRectPadding: 4,
edgeLabelRectStyle: { fill: 'white' },
nodeLabelStyle: { fill: '#666', textAlign: 'center', textBaseline: 'middle' },
groupStyle: { stroke: '#CED4D9', radius: 4 },
groupLabelStyle: { fill: '#666', textAlign: 'left', textBaseline: 'top' },
multiSelectRectStyle: { fill: '#1890FF', fillOpacity: .08, stroke: '#1890FF', opacity: .1 },
dragNodeHoverToGroupStyle: { stroke: '#1890FF', lineWidth: 2 },
dragNodeLeaveFromGroupStyle: { stroke: '#BAE7FF', lineWidth: 2 },
anchorPointStyle: { radius: 3.5, fill: '#fff', stroke: '#1890FF', lineAppendWidth: 12 },
anchorHotsoptStyle: { radius: 12, fill: '#1890FF', fillOpacity: .25 },
anchorHotsoptActivedStyle: { radius: 14 },
anchorPointHoverStyle: { radius: 4, fill: '#1890FF', fillOpacity: 1, stroke: '#1890FF' },
nodeControlPointStyle: { radius: 4, fill: '#fff', shadowBlur: 4, shadowColor: '#666' },
edgeControlPointStyle: { radius: 6, symbol: 'square', lineAppendWidth: 6, fillOpacity: 0, strokeOpacity: 0 },
nodeSelectedBoxStyle: { stroke: '#C2C2C2' },
cursor: {
panningCanvas: '-webkit-grabbing',
beforePanCanvas: '-webkit-grab',
hoverNode: 'move',
hoverEffectiveAnchor: 'crosshair',
hoverEdge: 'default',
hoverGroup: 'move',
hoverUnEffectiveAnchor: 'default',
hoverEdgeControllPoint: 'crosshair',
multiSelect: 'crosshair'
},
nodeDelegationStyle: {
stroke: '#1890FF',
fill: '#1890FF',
fillOpacity: .08,
lineDash: [4, 4],
radius: 4,
lineWidth: 1
},
edgeDelegationStyle: { stroke: '#1890FF', lineDash: [4, 4], lineWidth: 1 }
}

View File

@@ -0,0 +1,217 @@
<template>
<div class="toolbar">
<div class="left">
<!-- <el-checkbox class="edge-enabled" title="连线模式" @change="$parent.enableEdgeHandler"></el-checkbox>-->
<!-- <dropdown-->
<!-- class="edge-shape"-->
<!-- :dropdown-items="edgeShapeList"-->
<!-- @change="changeEdgeShapeHandler"-->
<!-- >-->
<!-- </dropdown>-->
<el-button size="mini" @click="router.push('/custom/query/topo')">返回</el-button>
</div>
<div class="center">
<div class="graph-ops">
<svg-icon name="undo" title="撤回" :class-name="disableUndo ? 'disabled-icon':'er-icon'"
@handle="undoHandler(disableUndo)"/>
<svg-icon name="redo" title="重做" :class-name="disableRedo ? 'disabled-icon':'er-icon'" @handle="redoHandler"/>
<span class="separator"></span>
<svg-icon name="copy" title="复制" :class-name="disableCopy ? 'disabled-icon':'er-icon'" @handle="copyHandler"/>
<svg-icon name="paste" title="粘贴" :class-name="disablePaste ? 'disabled-icon':'er-icon'"
@handle="pasteHandler"/>
<svg-icon name="clear" title="删除" :class-name="disableDelete ? 'disabled-icon':'er-icon'"
@handle="deleteHandler"/>
<span class="separator"></span>
<svg-icon name="zoom-in" title="放大" :class-name="'er-icon'" @handle="zoomInHandler"/>
<svg-icon name="zoom-out" title="缩小" :class-name="'er-icon'" @handle="zoomOutHandler"/>
<svg-icon name="fit" title="适应画布" :class-name="'er-icon'" @handle="autoZoomHandler"/>
<svg-icon name="actual_size" title="实际尺寸" :class-name="'er-icon'" @handle="resetZoomHandler"/>
<span class="separator"></span>
<!--id="multi-select"-->
<svg-icon name="selector" title="框选" id="multi-select" :class-name="'er-icon'" @handle="multiSelectHandler"/>
<!-- <div v-if="graphMode === 'edit'" class="iconfont zx">-->
<!-- <dropdown-->
<!-- class="edge-shape"-->
<!-- :dropdown-items="edgeShapeList"-->
<!-- @change="changeEdgeShapeHandler"-->
<!-- >-->
<!-- </dropdown>-->
<!-- </div>-->
</div>
</div>
<div class="right">
<el-button size="mini" @click="autoLayout">自动布局</el-button>
<!--<el-button size="mini" @click="circularLayoutHandler">环形布局</el-button>-->
<!--<el-button size="mini" @click="radialLayoutHandler">辐射</el-button>-->
<!--<el-button size="mini" @click="mdsLayoutHandler">MDS</el-button>-->
<!--<el-button size="mini" @click="dagreLayoutHandler">层次</el-button>-->
<!--<el-button size="mini" @click="autoLayoutHandler">自动old</el-button>-->
<!-- todo 返回到预览模式 -->
<el-button size="mini" @click="handlePreview('preview')">预览</el-button>
<el-button size="mini" @click="saveDataHandler">保存</el-button>
<el-button size="mini" @click="handleTopLine">上线</el-button>
</div>
</div>
</template>
<script setup>
import SvgIcon from '@/components/svgIcon/index.vue'
import Dropdown from './elements/dropdown.vue'
import {defineProps} from "vue";
import {useRouter} from "vue-router";
const router = useRouter()
const emit = defineEmits(["autoLayout", "handlePreview", "saveDataHandler",
"undoHandler", "redoHandler", "copyHandler", "pasteHandler", "deleteHandler", "zoomInHandler",
"zoomOutHandler", "autoZoomHandler", "resetZoomHandler", "multiSelectHandler"]);
const props = defineProps({
disableUndo: {
type: String, //参数类型
default: String, //默认值
},
disableRedo: {
type: String, //参数类型
default: String, //默认值
},
disableCopy: {
type: String, //参数类型
default: String, //默认值
},
disablePaste: {
type: String, //参数类型
default: String, //默认值
},
disableDelete: {
type: String, //参数类型
default: String, //默认值
},
graphMode: {
type: String, //参数类型
default: String, //默认值
},
edgeShapeList: {
type: Array, //参数类型
default: [], //默认值
}
})
const autoLayout = () => {
emit('autoLayout');
}
const handlePreview = (graphMode) => {
emit('handlePreview', graphMode);
}
const saveDataHandler = () => {
emit('saveDataHandler');
}
const handleTopLine = () => {
emit('handleTopLine');
}
const undoHandler = (flag) => {
if (flag) return
emit('undoHandler');
}
const redoHandler = () => {
emit('redoHandler');
}
const copyHandler = () => {
emit('copyHandler');
}
const pasteHandler = () => {
emit('pasteHandler');
}
const deleteHandler = () => {
emit('deleteHandler');
}
const zoomInHandler = () => {
emit('zoomInHandler');
}
const zoomOutHandler = () => {
emit('zoomOutHandler');
}
const autoZoomHandler = () => {
emit('autoZoomHandler');
}
const resetZoomHandler = () => {
emit('resetZoomHandler');
}
const multiSelectHandler = () => {
emit('multiSelectHandler');
}
const changeEdgeShapeHandler = () => {
emit('changeEdgeShapeHandler');
}
</script>
<style>
</style>
<style lang="scss" scoped>
.zx {
float: right;
}
.toolbar {
padding-right: 10px;
color: #333;
text-align: left;
vertical-align: middle;
line-height: 42px;
background-color: #ffffff;
border: 1px solid #E9E9E9;
box-shadow: 0 8px 12px 0 rgba(0, 52, 107, 0.04);
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
.left {
display: inline-block;
width: 12%;
padding-left: 10px;
}
.center {
display: inline-block;
width: 58%;
}
.right {
display: inline-block;
width: 30%;
text-align: right;
}
.edge-enabled {
width: 40%;
text-align: center;
}
.edge-shape {
width: 100%;
/*margin-right: 20px;*/
line-height: 25px;
text-align: center;
/*border-right: 1px solid #E6E9ED;*/
}
.graph-ops {
display: inline-block;
vertical-align: middle;
margin-left: 20px;
.icon-select {
background-color: #EEEEEE;
}
.separator {
margin-right: 5px;
border-left: 1px solid #E9E9E9;
}
}
.button {
margin: 0 5px;
}
}
</style>

View File

@@ -0,0 +1,183 @@
<template>
<div class="toolbar">
<div class="left">
<dropdown
class="theme-style"
:dropdown-items="themeOptions"
@change="toggleThemeChange"
>
</dropdown>
</div>
<div class="center">
<div class="graph-ops">
<svg-icon name="zoom-in" title="放大" id="zoom-in" @handle="zoomInHandler"/>
<svg-icon name="zoom-out" title="缩小" @handle="zoomOutHandler"/>
<svg-icon name="fit" title="适应画布" @handle="autoZoomHandler"/>
<svg-icon name="actual_size" title="实际尺寸" @handle="resetZoomHandler"/>
<span class="separator"></span>
<el-checkbox @change="enableMinimapHandler">导航器</el-checkbox>
<dropdown :dropdown-items="refreshOptions" @change="toggleAutoRefresh"></dropdown>
</div>
<div class="right">
<el-button size="mini" @click="manualRefreshHandler">刷新</el-button>
<el-button size="mini" @click="handleEdit('edit')">编辑</el-button>
</div>
</div>
</div>
</template>
<script setup>
import SvgIcon from '@/components/svgIcon/index.vue'
import Dropdown from './elements/dropdown.vue'
import {ref} from "vue";
const emit = defineEmits([ "handleEdit","zoomInHandler",
"zoomOutHandler","autoZoomHandler","resetZoomHandler","enableMinimapHandler","autoRefreshHandler","manualRefreshHandler"]);
const themeOptions = ref([
{value: 'defaultStyle', label: '默认主题'},
{value: 'darkStyle', label: '暗黑风格'},
{value: 'officeStyle', label: '办公风格'}
])
const refreshOptions = ref([
{value: -1, label: '不自动刷新'},
{value: 10000, label: '10秒自动刷新'},
{value: 30000, label: '30秒自动刷新'},
{value: 60000, label: '60秒自动刷新'}
])
const toggleThemeChange = (clickItem) => {
emit('changeGraphTheme',clickItem);
}
const changeThemeActiveIndex = (index) => {
// 给父组件调用(代码触发主题修改)
themeOptions.value.activeIndex = index
}
const zoomInHandler = () => {
emit('zoomInHandler');
}
const zoomOutHandler = () => {
emit('zoomOutHandler');
}
const autoZoomHandler = () => {
emit('autoZoomHandler');
}
const resetZoomHandler = () => {
emit('resetZoomHandler');
}
const enableMinimapHandler = (e) => {
emit('enableMinimapHandler',e);
}
const toggleAutoRefresh = (activeIndex) => {
emit('autoRefreshHandler',activeIndex);
}
const manualRefreshHandler = () => {
emit('manualRefreshHandler');
}
const handleEdit = (graphMode) => {
emit('handleEdit', graphMode);
}
</script>
<style lang="scss" scoped>
.toolbar {
padding-right: 10px;
color: #333;
text-align: left;
vertical-align: middle;
line-height: 42px;
background-color: #ffffff;
border: 1px solid #E9E9E9;
box-shadow: 0 8px 12px 0 rgba(0, 52, 107, 0.04);
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
.left {
display: inline-block;
width: 12%;
}
.center {
display: inline-block;
width: 58%;
}
.right {
display: inline-block;
width: 30%;
text-align: right;
}
.edge-enabled {
width: 40%;
text-align: center;
}
.edge-type {
width: 60%;
line-height: 25px;
text-align: left;
}
.theme-style {
width: 100%;
line-height: 25px;
text-align: center;
}
.graph-ops {
display: inline-block;
vertical-align: middle;
margin-left: 20px;
i {
width: 20px;
height: 20px;
margin: 0 6px;
line-height: 20px;
color: #a8a8a8;
text-align: center;
border-radius: 2px;
display: inline-block;
border: 1px solid rgba(2, 2, 2, 0);
}
i:hover {
cursor: pointer;
border: 1px solid #E9E9E9;
}
.disabled {
color: rgba(0, 0, 0, 0.25);
}
.disabled:hover {
cursor: not-allowed;
border: 1px solid rgba(2, 2, 2, 0);
}
.icon-select {
background-color: #EEEEEE;
}
.separator {
margin: 4px;
border-left: 1px solid #E9E9E9;
}
.auto-refresh {
margin: 0 8px 0 12px;
}
.checkbox {
margin: 0 6px;
}
}
.button {
margin: 0 5px;
}
}
</style>

File diff suppressed because one or more lines are too long