Commit bcfd01c7 authored by 郭铭瑶's avatar 郭铭瑶 🤘

节点添加及删除操作

parent 57460837
......@@ -7,6 +7,7 @@ switch (process.env.NODE_ENV) {
export default {
BASE_URL,
GET_SUBJECTS: '/subjects',
GET_RELATIONS: '/relations',
POST_SUBJECT: '/subject',
POST_NODE: '/node/relation',
......
......@@ -100,6 +100,18 @@ export default {
colorList,
setCurNode
)
// if (instance) {
// instance.update(props.data)
// } else {
// instance = new RelationGraph(
// container.value,
// props.data,
// props.config,
// menuData,
// colorList,
// setCurNode
// )
// }
setLegend()
}
function setKey(key) {
......
<template>
<div class="footer">
<n-button type="primary" ghost @click="addSubject">Add Subject</n-button>
<n-space>
<n-tag v-show="nodeVal" size="small" round :color="color">
{{ nodeVal && nodeVal.nodeLabel }}
......@@ -31,7 +32,7 @@ export default defineComponent({
}),
},
},
emits: ['set'],
emits: ['set', 'add'],
setup(props, ctx) {
const colorList = {
default: 'skyblue',
......@@ -65,12 +66,16 @@ export default defineComponent({
'nodeId',
'nodeLabel',
])
const addSubject = () => {
ctx.emit('add')
}
return {
curKey,
setKey,
list,
color,
nodeVal,
addSubject,
}
},
})
......@@ -80,7 +85,7 @@ export default defineComponent({
.footer
display flex
align-items center
justify-content flex-end
justify-content space-between
padding 0 15px 8px
background #f8f9fb
box-shadow 10px 6px 10px 0 #000
......
<template>
<div class="main">
<Nav style="grid-area: nav" />
<Nav style="grid-area: nav" @search="handleSearch" />
<D3
ref="d3Ref"
style="grid-area: content"
class="graph"
:data="data"
@branch="handleBranch"
@branch="fetchSubordinates"
@add="handleAdd"
@curNode="curNode = $event"
@del="handleDelete"
/>
<Side style="grid-area: side" />
<Footer style="grid-area: footer" :node="curNode" @set="setKey" />
<Footer
style="grid-area: footer"
:node="curNode"
@add="showSubjectDrawer = true"
@set="setKey"
/>
</div>
<n-drawer v-model:show="showDrawer" :width="400" placement="left">
<n-drawer-content title="新增节点">
......@@ -46,6 +51,24 @@
</n-form>
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="showSubjectDrawer" :width="400" placement="left">
<n-drawer-content title="新增Suject">
<n-form ref="subjectFormRef" :model="subjectData" :rules="rules">
<n-form-item path="subjectName" label="名称">
<n-input
v-model:value="subjectData.subjectName"
placeholder="请输入"
@keydown.enter.prevent
/>
</n-form-item>
<n-space justify="end">
<n-button :loading="isLoading" type="primary" @click="addSubject">
提交
</n-button>
</n-space>
</n-form>
</n-drawer-content>
</n-drawer>
</template>
<script>
......@@ -69,6 +92,7 @@ export default {
const formData = ref({
propertyName: null,
relationId: null,
systemId: null,
})
const showDrawer = ref(false)
const isLoading = ref(false)
......@@ -76,20 +100,32 @@ export default {
const relationOptions = ref([])
const systemOptions = ref([])
const data = ref([])
function getData() {
const subjectNodes = ref([])
function fetchSubjects() {
// ajax
// .get({
// url: api.GET_NODES,
// params: { nodeId: '572563400077869056' },
// })
// .then((res) => {
// console.log('nodes:', res)
// data.value = res.data.content
// systemOptions.value = data.value.nodes
// .filter((node) => node.nodeLabel === 'System')
// .map((node) => ({ label: node.systemName, value: node.nodeId }))
// })
ajax
.get({
url: api.GET_NODES,
url: api.GET_SUBJECTS,
})
.then((res) => {
console.log('nodes:', res)
data.value = res.data.content
systemOptions.value = data.value.nodes
.filter((node) => node.nodeLabel === 'System')
.map((node) => ({ label: node.systemName, value: node.nodeId }))
console.log('subject', res.data.content)
subjectNodes.value = res.data.content
data.value = { nodes: subjectNodes.value, links: [] }
})
}
getData()
fetchSubjects()
ajax
.get({
url: api.GET_RELATIONS,
......@@ -101,18 +137,32 @@ export default {
value: item.id,
}))
})
function handleBranch({ nodeId }) {
function fetchSubordinates(params = null) {
ajax
.get({
url: api.GET_NODES,
params: { nodeId },
params,
})
.then((res) => {
console.log('nodes:', res)
// data.value = res.data.content
const { links, nodes } = res.data.content
data.value = {
nodes: [
...data.value.nodes,
...nodes.filter((node) => node.nodeLabel !== 'Subject'),
],
links: [...data.value.links, ...links],
}
systemOptions.value = data.value.nodes
.filter((node) => node.nodeLabel === 'System')
.map((node) => ({ label: node.systemName, value: node.nodeId }))
})
}
function handleAdd({ nodeId }) {
formData.value = {
propertyName: null,
relationId: null,
systemId: null,
}
subjectId.value = nodeId
showDrawer.value = true
}
......@@ -133,7 +183,8 @@ export default {
},
})
.then(() => {
getData()
// fetchSubjects()
fetchSubordinates({ nodeId: subjectId.value })
isLoading.value = false
showDrawer.value = false
message.success('提交成功')
......@@ -152,11 +203,61 @@ export default {
negativeText: '取消',
maskClosable: false,
onPositiveClick: () => {
console.log('delete', data)
ajax
.delete({
url: api.DELETE_NODE.replace('{id}', data.id),
})
.then(() => {
if (data.nodeLabel === 'Subject') {
fetchSubjects()
} else {
fetchSubordinates({ nodeId: null })
}
message.success('删除成功!')
})
},
})
}
const subjectFormRef = ref(null)
const showSubjectDrawer = ref(false)
const subjectData = ref({ subjectName: null })
const addSubject = (e) => {
e.preventDefault()
subjectFormRef.value.validate((errors) => {
if (!errors) {
isLoading.value = true
ajax
.post({
url: api.POST_SUBJECT,
params: subjectData.value,
})
.then(() => {
fetchSubjects()
isLoading.value = false
showSubjectDrawer.value = false
message.success('提交成功')
})
}
})
}
const handleSearch = (key) => {
if (!key) {
fetchSubjects()
return
}
ajax
.get({
url: api.GET_NODES,
params: { nodeName: key },
})
.then((res) => {
data.value = res.data.content
systemOptions.value = data.value.nodes
.filter((node) => node.nodeLabel === 'System')
.map((node) => ({ label: node.systemName, value: node.nodeId }))
})
}
return {
formRef,
formData,
......@@ -167,35 +268,47 @@ export default {
systemOptions,
data,
mockData,
handleBranch,
fetchSubordinates,
handleAdd,
d3Ref,
setKey,
rules: {
propertyName: [
subjectName: [
{
required: true,
message: '请输入节点名称',
message: '请输入名称',
trigger: ['input', 'blur'],
},
],
relationId: [
propertyName: [
{
required: true,
message: '请选择从属类型',
message: '请输入节点名称',
trigger: ['input', 'blur'],
},
],
systemId: [
relationId: [
{
required: true,
message: '请选择所属系统',
message: '请选择从属类型',
trigger: ['input', 'blur'],
},
],
// systemId: [
// {
// required: true,
// message: '请选择所属系统',
// trigger: ['input', 'blur'],
// },
// ],
},
curNode,
handleDelete,
showSubjectDrawer,
subjectFormRef,
subjectData,
addSubject,
handleSearch,
}
},
}
......@@ -209,7 +322,7 @@ export default {
overflow hidden
grid-template-areas 'nav nav' 'side content' 'side footer'
grid-template-columns 0px 1fr
grid-template-rows 0px 1fr 70px
grid-template-rows 40px 1fr 70px
padding 4px
font-weight bold !important
</style>
<template>
<div class="nav"></div>
<div class="nav">
<n-input
v-model:value="searchKey"
type="text"
placeholder="搜索Subject"
clearable
@keypress.enter="search"
/>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Nav',
setup() {},
emits: ['search'],
setup(_, ctx) {
const searchKey = ref(null)
return {
searchKey,
search: () => ctx.emit('search', searchKey.value),
}
},
})
</script>
......@@ -15,4 +30,5 @@ export default defineComponent({
.nav
background #f9fbfd
border-bottom 4px solid #fff
padding 0 10px
</style>
......@@ -78,7 +78,7 @@ const defaultConfig = {
isHighLight: true, // 是否启动 鼠标 hover 到节点上高亮与节点有关的节点,其他无关节点透明的功能
isScale: true, // 是否启用缩放平移zoom功能
scaleExtent: [0.2, 1.5], // 缩放的比例尺
chargeStrength: -300, // 万有引力
chargeStrength: -100, // 万有引力
collide: 100, // 碰撞力的大小 (节点之间的间距)
alphaDecay: 0.0228, // 控制力学模拟衰减率
r: 45, // 圈圈的半径 [30 - 45]
......@@ -86,8 +86,8 @@ const defaultConfig = {
fontColor: '#2c3e50', // 圈圈内文字的颜色
linkSrc: 30, // 划线时候的弧度
linkColor: 'gray', // 链接线默认的颜色
strokeColor: 'gray', // 圈圈外围包裹的颜色
strokeWidth: 0, // 圈圈外围包裹的宽度
strokeColor: 'rgba(255,255,255,0.4)', // 圈圈外围包裹的颜色
strokeWidth: 6, // 圈圈外围包裹的宽度
colorList: {
default: 'skyblue',
System: '#58b2dc',
......@@ -145,7 +145,8 @@ export default class RelationGraph {
this.dependsNode = []
this.dependsLinkAndText = []
console.log('init', this.config)
// console.log('init', this.config)
this.createSimulation()
this.init()
}
......@@ -156,12 +157,202 @@ export default class RelationGraph {
.call(textWrap, this.config.r * 1.8)
}
update(data) {
this.config = Object.assign({}, this.config, data)
const self = this
console.log('====', data)
// this.config = Object.assign({}, this.config, data)
this.config.nodes = [...data.nodes]
this.config.links = [...data.links]
console.log('update', this.config)
this.createSimulation()
d3.selectAll('pattern.circle-bg').data(this.config.nodes)
d3.selectAll('g.circle-wrapper').data(this.config.nodes)
d3.selectAll('g.edge').data(this.config.links)
// d3.selectAll('pattern.circle-bg').data(this.config.nodes)
// d3.selectAll('g.circle-wrapper').data(this.config.nodes)
// d3.selectAll('g.edge').data(this.config.links)
this.patterns = this.defs
.selectAll('pattern.circle-bg')
.data(this.config.nodes)
.enter()
.append('pattern')
.attr('class', 'circle-bg')
.attr('id', (d) => d.id)
.attr('width', '1')
.attr('height', '1')
this.patterns
.append('rect')
.attr('width', 2 * this.config.r)
.attr('height', 2 * this.config.r)
.attr(
'fill',
(d) =>
(d && this.config.colorList[d.nodeLabel || 'default']) ||
this.config.nodeColor
)
this.patterns
.append('text')
.attr('class', 'node-text')
.attr('x', this.config.r)
.attr('y', this.config.r) // edit
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('fill', this.config.fontColor)
.style('font-size', this.config.r / 3.8)
.text((d) => d[d.nodeLabel.toLowerCase() + 'Name'])
// 给圈圈节点添加g便于后面添加menu
this.circlesWrapper = this.relMap_g
.selectAll('g.circle-wrapper')
.data(this.config.nodes)
.enter()
.append('g')
.attr('class', 'circle-wrapper')
// 6.关系图添加用于显示圈圈的节点
this.circles = this.circlesWrapper
.append('circle')
.attr('r', this.config.r)
.attr('class', 'node')
.style('cursor', 'pointer')
.attr('fill', (d) => `url(#${d.id})`)
.attr('stroke', this.config.strokeColor)
.attr('stroke-width', this.config.strokeWidth)
.on('mouseover', function (e, d) {
d3.select(this).attr(
'stroke-width',
self.config.strokeWidth == 0 ? 3 : 1.5 * self.config.strokeWidth
)
d3.select(this).attr('stroke', 'rgba(0,0,0,0.1)')
// d3.select(this).attr('stroke', d.strokeColor || self.config.strokeColor)
if (self.config.isHighLight) {
self.highlightObject(d)
}
// tooltip
// .html(d.name)
// .style('left', e.pageX + 10 + 'px')
// .style('top', e.pageY + 10 + 'px')
// .style('opacity', 1)
})
.on('mouseout', function (e, d) {
d3.select(this).attr('stroke-width', self.config.strokeWidth)
d3.select(this).attr('stroke', d.strokeColor || self.config.strokeColor)
if (self.config.isHighLight) {
self.highlightObject(null)
}
// tooltip.style('opacity', 0)
})
.on('click', function (e, d) {
if (menu && menu.curNodeData == d) {
self.closeMenu()
} else {
self.closeMenu()
if (d.nodeLabel === 'Subject') {
self.openMenu(this, d)
} else {
self.openMenu(this, d, 'del')
}
}
//阻止事件冒泡 阻止事件默认行为
e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true)
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
})
.on('contextmenu', function (e) {
// 取消鼠标右键菜单默认行为
e.cancelBubble = true
e.returnValue = false
})
// 应用 自定义的 拖拽事件
.call(
d3
.drag()
.on('start', (e, d) => {
e.sourceEvent.stopPropagation()
// restart()方法重新启动模拟器的内部计时器并返回模拟器。
// 与simulation.alphaTarget或simulation.alpha一起使用时,此方法可用于在交互
// 过程中进行“重新加热”模拟,例如在拖动节点时,在simulation.stop暂停之后恢复模拟。
// 当前alpha值为0,需设置alphaTarget让节点动起来
if (!e.active) this.simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
})
.on('drag', (e, d) => {
// d.fx属性- 节点的固定x位置
// 在每次tick结束时,d.x被重置为d.fx ,并将节点 d.vx设置为零
// 要取消节点,请将节点 .fx和节点 .fy设置为空,或删除这些属性。
d.fx = e.x
d.fy = e.y
})
.on('end', (e, d) => {
// 让alpha目标值值恢复为默认值0,停止力模型
if (!e.active) this.simulation.alphaTarget(0)
d.fx = null
d.fy = null
})
)
// 文字折行
this.SVG.selectAll('text.node-text').call(textWrap, this.config.r * 1.8)
// 5.关系图添加线
// 5.1 每条线是个容器,有线 和一个装文字的容器
this.edges = this.relMap_g
.selectAll('g.edge')
.data(this.config.links)
.enter()
.append('g')
.attr('class', 'edge')
.on('mouseover', function (e, d) {
if (self.config.isHighLight) {
self.highlightLinks(d)
}
d3.select(this).selectAll('path.links').attr('stroke-width', 2)
d3.select(this).selectAll('.rect_g text').style('font-weight', 'bold')
})
.on('mouseout', function () {
if (self.config.isHighLight) {
self.highlightLinks(null)
}
d3.select(this).selectAll('path.links').attr('stroke-width', 1)
d3.select(this).selectAll('.rect_g text').style('font-weight', 'normal')
})
.on('click', function (e, d) {
console.log('线click')
})
.attr('fill', (d) => d.color || this.config.linkColor)
// 5.2 添加线
this.links = this.edges
.append('path')
.attr('class', 'links')
.attr(
'd',
(d) => `M${this.config.linkSrc},0 L${getDis(d.source, d.target)},0`
)
.style('marker-end', 'url(#marker)')
.attr('stroke', (d) => d.color || this.config.linkColor)
// 5.3 添加关系文字的容器
this.rect_g = this.edges.append('g').attr('class', 'rect_g')
// 5.4 添加rect
this.rects = this.rect_g
.append('rect')
.attr('x', 40)
.attr('y', -10)
.attr('width', 50)
.attr('height', 20)
.attr('fill', '#f9fbfd')
.attr('stroke', 'transparent')
// 5.5 文本标签 坐标(x,y)代表 文本的左下角的点
this.texts = this.rect_g
.append('text')
.attr('x', 40)
.attr('y', 5)
.attr('text-anchor', 'middle') // <text>文本中轴对齐方式居中 start | middle | end
.style('font-size', 12)
.text((d) => d.name)
this.simulation.restart()
}
openMenu(self, d, type) {
this.setCurNode(d)
......@@ -184,7 +375,7 @@ export default class RelationGraph {
// 创建力学模拟器
createSimulation() {
console.log('createSimulation', this.config.nodes)
console.log('createSimulation', this.config.nodes, this.config.links)
// 1. 创建一个力学模拟器
this.simulation = d3
.forceSimulation(this.config.nodes)
......@@ -213,7 +404,7 @@ export default class RelationGraph {
init() {
const self = this
this.createSimulation()
// this.createSimulation()
// 2.创建svg标签
this.SVG = this.graph
......@@ -237,8 +428,8 @@ export default class RelationGraph {
this.defs = this.SVG.append('defs')
// 3.1 添加箭头
const border =
this.config.strokeWidth == 0 ? 7.5 : 1.8 * this.config.strokeWidth
const border = 7.5
//const border = this.config.strokeWidth == 0 ? 7.5 : 1.8 * this.config.strokeWidth
this.marker = this.defs
.append('marker')
.attr('id', 'marker')
......@@ -290,7 +481,6 @@ export default class RelationGraph {
.attr('class', 'relMap_g')
.attr('width', this.config.width)
.attr('height', this.config.height)
// 5.关系图添加线
// 5.1 每条线是个容器,有线 和一个装文字的容器
this.edges = this.relMap_g
......@@ -337,7 +527,7 @@ export default class RelationGraph {
.append('rect')
.attr('x', 40)
.attr('y', -10)
.attr('width', 40)
.attr('width', 50)
.attr('height', 20)
.attr('fill', '#f9fbfd')
.attr('stroke', 'transparent')
......@@ -379,7 +569,8 @@ export default class RelationGraph {
'stroke-width',
self.config.strokeWidth == 0 ? 3 : 1.5 * self.config.strokeWidth
)
d3.select(this).attr('stroke', d.strokeColor || self.config.strokeColor)
d3.select(this).attr('stroke', 'rgba(0,0,0,0.1)')
// d3.select(this).attr('stroke', d.strokeColor || self.config.strokeColor)
if (self.config.isHighLight) {
self.highlightObject(d)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment