Commit 6ddd03b6 authored by 郭铭瑶's avatar 郭铭瑶 🤘

主体客体区分形状&编辑节点&添加标签

parent a5189eeb
......@@ -8,14 +8,16 @@ switch (process.env.NODE_ENV) {
export default {
BASE_URL,
GET_SUBJECTS: '/subjects/v2',
PUT_SUBJECT: '/subject/{id}',
GET_RELATIONS: '/relations',
POST_SUBJECT: '/subject',
POST_NODE: '/node/relation/v2',
DELETE_RELATION: '/node/relation/{id}',
GET_NODES: '/node/relations',
DELETE_NODE: '/node/{id}',
PUT_NODE: '/node/{nodeType}/{id}',
GET_SYSTEMS: '/systems',
POST_LINK_SUBJECTS: '/node/relations',
GET_TAGS: '/label/classification',
GET_TAG_CLASS: '/label/classification',
GET_TAGS: '/labels',
POST_TAGS: '/node/labels',
}
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1629870391503" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2057" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M729.483636 995.607273H218.996364A127.301818 127.301818 0 0 1 91.810909 868.421818V233.658182A127.301818 127.301818 0 0 1 218.996364 106.472727h382.952727a29.090909 29.090909 0 1 1 0 58.181818H218.996364a69.003636 69.003636 0 0 0-69.003637 69.003637v634.763636a69.003636 69.003636 0 0 0 69.003637 69.003637h510.487272a69.003636 69.003636 0 0 0 69.003637-69.003637v-400.290909a29.090909 29.090909 0 0 1 58.181818 0v400.290909a127.301818 127.301818 0 0 1-127.185455 127.185455z" fill="#ffffff" p-id="2058"></path><path d="M514.792727 569.250909a29.090909 29.090909 0 0 1-28.974545-31.883636C488.727273 506.298182 500.363636 402.269091 529.105455 368.290909l254.836363-303.709091a99.607273 99.607273 0 1 1 152.552727 128l-254.836363 303.709091c-28.509091 33.978182-129.047273 63.767273-159.185455 72.029091a28.974545 28.974545 0 0 1-7.68 0.930909zM860.16 87.156364a41.309091 41.309091 0 0 0-31.650909 14.778181l-254.836364 303.709091c-8.494545 10.123636-18.036364 50.734545-24.552727 93.090909 41.192727-13.847273 79.476364-30.254545 87.970909-40.378181l254.836364-303.709091a41.309091 41.309091 0 0 0-31.650909-67.956364z" fill="#ffffff" p-id="2059"></path></svg>
\ No newline at end of file
......@@ -40,6 +40,7 @@ import branch from '@/assets/images/branch.svg'
import link from '@/assets/images/link.svg'
import add from '@/assets/images/add.svg'
import del from '@/assets/images/delete.svg'
import edit from '@/assets/images/edit.svg'
export default {
name: 'D3',
props: {
......@@ -54,7 +55,7 @@ export default {
},
},
},
emits: ['link', 'add', 'del', 'branch', 'curNode', 'del-link'],
emits: ['link', 'add', 'del', 'edit', 'branch', 'curNode', 'del-link'],
setup(props, ctx) {
const colorList = {
default: 'skyblue',
......@@ -76,6 +77,12 @@ export default {
// action: (d) => ctx.emit('branch', d),
// title: '分支',
// },
{
key: 'edit',
icon: edit,
action: (d) => ctx.emit('edit', d),
title: '编辑',
},
{
key: 'link',
icon: link,
......@@ -96,27 +103,49 @@ export default {
},
]
const curTagKey = ref(null)
const setCurNode = (type) => {
if (type.nodeLabel !== 'Subject') {
const setCurNode = (node) => {
if (node.nodeLabel !== 'Subject') {
curTagKey.value = null
}
ctx.emit('curNode', type)
ctx.emit('curNode', node)
}
const handleLinkClick = (d) => {
if (d.target.nodeLabel === 'System') {
const { nodeLabel } = d.target
if (nodeLabel === 'System' || nodeLabel === 'Subject') {
ctx.emit('del-link', d)
}
}
const showBranch = (d) => {
ctx.emit('branch', d)
}
function init() {
if (!container.value) return
const data = {
links: props.data.links,
nodes: props.data.nodes.map((node) => {
const data = transData(props.data)
if (instance) {
updateGraph(props.data)
} else {
instance = new RelationGraph(
container.value,
data,
props.config,
menuData,
colorList,
setCurNode,
handleLinkClick,
showBranch
)
}
setLegend(data)
}
function transData(data) {
return {
links: data.links,
nodes: data.nodes.map((node) => {
let key = node.nodeLabel
if (key === 'Property') {
const relation = props.data.links.find((link) => {
......@@ -134,21 +163,10 @@ export default {
return node
}),
}
if (instance) {
instance.update(data)
} else {
instance = new RelationGraph(
container.value,
data,
props.config,
menuData,
colorList,
setCurNode,
handleLinkClick,
showBranch
)
}
setLegend(data)
function updateGraph(data) {
instance.update(transData(data))
}
function setKey(key) {
......@@ -269,6 +287,7 @@ export default {
colorList,
container,
setKey,
updateGraph,
nodeList,
linkList,
defaultColor: {
......
......@@ -9,6 +9,7 @@
@add="openAddDrawer"
@curNode="curNode = $event"
@del="deleteNode"
@edit="openEditDrawer"
@link="openLinkDrawer"
@del-link="deleteLink"
/>
......@@ -50,7 +51,30 @@
:options="relationOptions"
/>
</n-form-item>
<n-form-item label="所属系统" path="systemId">
<template v-if="getTagName(data.relationId) === '标签'">
<n-form-item label="标签分类" path="classification">
<n-select
v-model:value="data.classification"
placeholder="请选择"
:options="tagClassOptions"
/>
</n-form-item>
<n-form-item label="标签名称" path="tag">
<n-select
v-model:value="data.tag"
placeholder="请选择"
filterable
tag
:options="tagOptions"
/>
</n-form-item>
<n-form-item path="isPositive" :show-label="false">
<n-checkbox v-model:checked="data.isPositive"
>是否为积极的标签</n-checkbox
>
</n-form-item>
</template>
<n-form-item v-else label="所属系统" path="systemId">
<n-select
v-model:value="data.systemIds"
placeholder="请选择"
......@@ -141,7 +165,7 @@
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="showLinkDrawer" :width="340" placement="left">
<n-drawer-content title="连接要素" closable>
<n-drawer-content title="关联要素" closable>
<n-form ref="linkFormRef" :model="linkData" :rules="rules" size="small">
<n-form-item path="sourceNodeId" label="源要素">
<n-select
......@@ -171,6 +195,50 @@
</n-form>
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="showEditDrawer" :width="340" placement="left">
<n-drawer-content title="编辑节点" closable>
<n-form ref="editFormRef" :model="editData" :rules="rules" size="small">
<template v-if="curNode.nodeLabel === 'Subject'">
<n-form-item path="subjectName" label="名称">
<n-input
v-model:value="editData.subjectName"
placeholder="请输入"
@keydown.enter.prevent
/>
</n-form-item>
<n-form-item path="subjectType" label="类型">
<n-select
v-model:value="editData.subjectType"
placeholder="请选择"
:options="[
{ label: '主体', value: '1' },
{ label: '客体', value: '2' },
]"
/>
</n-form-item>
</template>
<template v-else-if="curNode.nodeLabel === 'Property'">
<n-form-item label="节点名称" path="propertyName">
<n-input
v-model:value="editData.propertyName"
placeholder="请输入"
@keydown.enter.prevent
/>
</n-form-item>
</template>
<n-space justify="end">
<n-button
:loading="isLoading"
size="small"
type="primary"
@click="editNode"
>
提交
</n-button>
</n-space>
</n-form>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
......@@ -219,13 +287,30 @@ function fetchSubjects() {
}
fetchSubjects()
const tagClassOptions = ref([])
const tagOptions = ref([])
function fetchTags() {
ajax
.get({
url: api.GET_TAG_CLASS,
})
.then((res) => {
const data = res.data.content
tagClassOptions.value = data.map((e) => ({
label: e.classification,
value: e.classification,
}))
})
ajax
.get({
url: api.GET_TAGS,
})
.then((res) => {
console.log('tags', res.data.content)
const data = (res.data && res.data.content) || []
tagOptions.value = data.map((e) => ({
label: e.labelName,
value: e.id,
}))
})
}
fetchTags()
......@@ -302,31 +387,80 @@ function setLabelKey(key) {
d3Ref.value.setKey(key)
}
function getTagName(relationId) {
if (!relationId) return null
const tag = relationOptions.value.find((e) => e.value === relationId)
return tag && tag.label
}
function submitNewNode(e) {
e.preventDefault()
formRef.value.validate((errors) => {
formRef.value.validate(async (errors) => {
if (!errors) {
isLoading.value = true
const { nodeId, subjectName, subjectType } = curSubject.value
ajax
.post({
const tagData = []
const otherData = []
formData.value.forEach((item) => {
if (getTagName(item.relationId) === '标签') {
tagData.push(item)
} else {
otherData.push(item)
}
})
if (otherData.length > 0) {
await ajax.post({
url: api.POST_NODE,
params: {
subjectId: nodeId,
subjectName,
subjectType,
propertyList: formData.value,
propertyList: otherData.map((item) => ({
propertyName: item.propertyName,
systemIds: item.systemIds,
relationId: item.relationId,
})),
},
})
.then(() => {
}
if (tagData.length > 0) {
await ajax.post({
url: api.POST_TAGS,
params: {
subjectId: nodeId,
subjectName,
subjectType,
relationId: tagData[0].relationId,
labelList: tagData.map((item) => {
let obj = {
id: null,
labelName: item.tag,
}
if (tagOptions.value.findIndex((e) => e.value == item.tag) >= 0) {
obj = {
id: item.tag,
labelName: null,
}
}
return {
classification: item.classification,
isPositive: item.isPositive,
...obj,
}
}),
},
})
}
fetchSubordinates({
nodeId: nodeId,
nodeId,
nodeLabel: 'Subject',
})
isLoading.value = false
showDrawer.value = false
message.success('提交成功')
})
message.success('新增成功')
}
})
}
......@@ -366,9 +500,12 @@ function deleteNode(data) {
function deleteLink(data) {
const { source, target } = data
const name = `'${source.propertyName || target.subjectName}_${
target.systemName || source.subjectName
}'`
dialog.error({
title: '删除关联',
content: `确定是否删除 '${source.propertyName}_${target.systemName}' 关联关系?`,
content: `确定是否删除 ${name} 关联关系?`,
positiveText: '确定',
negativeText: '取消',
maskClosable: false,
......@@ -410,7 +547,7 @@ const addSubject = (e) => {
fetchSubjects()
isLoading.value = false
showSubjectDrawer.value = false
message.success('提交成功')
message.success('新增成功')
})
}
})
......@@ -467,7 +604,7 @@ const linkSubject = (e) => {
})
isLoading.value = false
showLinkDrawer.value = false
message.success('提交成功')
message.success('关联成功')
})
}
})
......@@ -497,7 +634,21 @@ const rules = {
relationId: [
{
required: true,
message: '请选择从属类型',
message: '请选择从属关系',
trigger: ['change', 'blur'],
},
],
classification: [
{
required: true,
message: '请选择标签分类',
trigger: ['change', 'blur'],
},
],
tag: [
{
required: true,
message: '请选择标签名称',
trigger: ['change', 'blur'],
},
],
......@@ -553,7 +704,7 @@ function openSubjectDrawer() {
function openLinkDrawer() {
showLinkDrawer.value = true
linkData.value = {
sourceNodeId: null,
sourceNodeId: curSubject.value.nodeId,
targetNodeId: null,
}
}
......@@ -567,6 +718,55 @@ function addNewItem() {
function deleteItem(i) {
formData.value.splice(i, 1)
}
const editData = ref(null)
const showEditDrawer = ref(false)
const editFormRef = ref(null)
function openEditDrawer(d) {
editData.value = d
showEditDrawer.value = true
}
function editNode(e) {
e.preventDefault()
editFormRef.value.validate((errors) => {
if (!errors) {
isLoading.value = true
const { nodeLabel, nodeId } = curNode.value
let params = {}
if (nodeLabel === 'Subject') {
params = {
subjectName: editData.value.subjectName,
subjectType: editData.value.subjectType,
}
} else if (nodeLabel === 'Property') {
params = {
propertyName: editData.value.propertyName,
}
}
ajax
.put({
url: api.PUT_NODE.replace('{nodeType}', nodeLabel).replace(
'{id}',
nodeId
),
params,
})
.then(() => {
graphData.value.nodes.forEach((node) => {
if (node.nodeId === nodeId) {
for (const key in params) {
node[key] = params[key]
}
}
})
d3Ref.value.updateGraph(graphData.value)
isLoading.value = false
showEditDrawer.value = false
message.success('编辑成功')
})
}
})
}
</script>
<style lang="stylus" scoped>
......
......@@ -11,6 +11,7 @@ import {
NForm,
NFormItem,
NInput,
NCheckbox,
NInputGroup,
NSelect,
NMessageProvider,
......@@ -29,6 +30,7 @@ const naive = create({
NForm,
NFormItem,
NInput,
NCheckbox,
NInputGroup,
NSelect,
NMessageProvider,
......
......@@ -34,11 +34,11 @@ function textWrap(text, width) {
text.each(function () {
const text = d3.select(this),
words = text.text().split('').reverse(),
lineHeight = 1.2, // ems
lineHeight = 1.1, // ems
dy = 0,
x = text.attr('x')
let y = text.attr('y')
let y = +text.attr('y') + 4
if (words.length > 4 && words.length <= 8) {
y = +y - 4
......@@ -85,8 +85,8 @@ function polygonPoints(cx, cy, r, n) {
let a = Math.PI / 2 + alpha / 2
let points = ''
for (let i = 0; i < n; i++) {
x = cx + r * Math.cos(a)
y = cy + r * Math.sin(a)
const x = cx + r * Math.cos(a)
const y = cy + r * Math.sin(a)
points += x + ',' + y + ','
a += alpha
}
......@@ -103,7 +103,7 @@ const defaultConfig = {
chargeStrength: 300, // 万有引力
collide: 80, // 碰撞力的大小 (节点之间的间距)
alphaDecay: 0.1, // 控制力学模拟衰减率
r: 36, // 圈圈的半径 [30 - 45]
r: 40, // 圈圈的半径 [30 - 45]
nodeColor: 'skyblue', // 圈圈节点背景颜色
fontColor: '#2c3e50', // 圈圈内文字的颜色
linkSrc: 30, // 划线时候的弧度
......@@ -207,7 +207,7 @@ export default class RelationGraph {
d3.selectAll('.node-text')
.text((d) => (key && d[key]) || '无')
// .text((d) => (key && d[key]) || d[d.nodeLabel.toLowerCase() + 'Name'])
.call(textWrap, this.config.r * 1.8)
.call(textWrap, this.config.r * 1.6)
}
initPatterns() {
......@@ -239,7 +239,7 @@ export default class RelationGraph {
.append('text')
.attr('class', 'node-text')
.attr('x', this.config.r)
.attr('y', this.config.r) // edit
.attr('y', this.config.r)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('fill', '#000')
......@@ -313,27 +313,72 @@ export default class RelationGraph {
.text((d) => d.name)
}
initCircles(circleWrapper) {
const self = this
// 给圈圈节点添加g便于后面添加menu
if (circleWrapper) {
this.circlesWrapper = circleWrapper
initCircles(wrappers) {
if (this.config.others.length === 0) return
if (wrappers) {
this.circleWrappers = wrappers
} else {
this.circlesWrapper = this.relMap_g
// 给节点添加g便于后面添加menu
this.circleWrappers = this.relMap_g
.selectAll('g.circle-wrapper')
.data(this.config.nodes)
.data(this.config.others)
.enter()
.append('g')
.attr('class', 'circle-wrapper')
.attr('class', 'circle-wrapper node')
.raise()
}
// 6.关系图添加用于显示圈圈的节点
this.circles && this.circles.remove()
this.circles = this.circlesWrapper
this.circles = this.circleWrappers
.append('circle')
.attr('class', '.circle')
.attr('r', this.config.r)
this.addEvent(this.circles)
}
initSquares(wrappers) {
if (this.config.subjects.length === 0) return
if (wrappers) {
this.squareWrappers = wrappers
} else {
this.squareWrappers = this.relMap_g
.selectAll('g.square-wrapper')
.data(this.config.subjects)
.enter()
.append('g')
.attr('class', 'square-wrapper')
.raise()
}
this.squares && this.squares.remove()
this.squares = this.squareWrappers.append('polygon').attr('class', 'square')
this.addEvent(this.squares)
}
initPentagons(wrappers) {
if (this.config.objects.length === 0) return
if (wrappers) {
this.pentagonWrappers = wrappers
} else {
this.pentagonWrappers = this.relMap_g
.selectAll('g.pentagon-wrapper')
.data(this.config.objects)
.enter()
.append('g')
.attr('class', 'pentagon-wrapper')
.raise()
}
this.pentagons && this.pentagons.remove()
this.pentagons = this.pentagonWrappers
.append('polygon')
.attr('class', 'pentagons')
this.addEvent(this.pentagons)
}
addEvent(graph) {
if (!graph) return
const self = this
graph
.attr('class', 'node')
.style('cursor', 'pointer')
.attr('fill', (d) => `url(#${d.id})`)
......@@ -374,8 +419,10 @@ export default class RelationGraph {
self.closeMenu(true)
if (d.nodeLabel === 'Subject') {
self.openMenu(this, d)
} else if (d.nodeLabel === 'System') {
self.openMenu(this, d, ['del'])
} else {
self.openMenu(this, d, ['del', 'branch'])
self.openMenu(this, d, ['del', 'edit'])
}
}
})
......@@ -404,17 +451,20 @@ export default class RelationGraph {
// 要取消节点,请将节点 .fx和节点 .fy设置为空,或删除这些属性。
d.fx = e.x
d.fy = e.y
d.x = e.x
d.y = e.y
})
.on('end', (e, d) => {
// 让alpha目标值值恢复为默认值0,停止力模型
if (!e.active) this.simulation.alphaTarget(0)
d.fx = e.x
d.fy = e.y
d.x = e.x
d.y = e.y
})
)
// 文字折行
this.SVG.selectAll('text.node-text').call(textWrap, this.config.r * 1.8)
this.SVG.selectAll('text.node-text').call(textWrap, this.config.r * 1.5)
}
// 创建力学模拟器
......@@ -445,26 +495,53 @@ export default class RelationGraph {
}
update(data) {
const { nodes, links } = this.transformData(data)
const { links, nodes, subjects, objects, others } = this.transformData(data)
this.config.nodes = [...nodes]
this.config.links = [...links]
this.config.subjects = [...subjects]
this.config.objects = [...objects]
this.config.others = [...others]
console.log('update', this.config)
this.initPatterns()
this.initLinks()
const updateNodes = this.relMap_g
.selectAll('g.circle-wrapper')
.data(this.config.nodes)
updateNodes.exit().remove()
const newCircles = this.relMap_g.selectAll('g.circle-wrapper').data(others)
newCircles.exit().remove()
this.initCircles(
updateNodes
newCircles
.enter()
.append('g')
.attr('class', 'circle-wrapper')
.merge(updateNodes)
.merge(newCircles)
.raise()
)
const newSquares = this.relMap_g
.selectAll('g.square-wrapper')
.data(subjects)
newSquares.exit().remove()
this.initSquares(
newSquares
.enter()
.append('g')
.attr('class', 'square-wrapper')
.merge(newSquares)
.raise()
)
const newPentagons = this.relMap_g
.selectAll('g.pentagon-wrapper')
.data(objects)
newPentagons.exit().remove()
this.initPentagons(
newPentagons
.enter()
.append('g')
.attr('class', 'pentagon-wrapper')
.merge(newPentagons)
.raise()
)
......@@ -564,9 +641,12 @@ export default class RelationGraph {
this.initLinks()
this.initCircles()
this.initPentagons()
this.initSquares()
}
ticked() {
const { r } = this.config
// 7.1 修改每条容器edge的位置
this.edges.attr('transform', (d) =>
getTransform(d.source, d.target, getDis(d.source, d.target))
......@@ -596,7 +676,6 @@ export default class RelationGraph {
return 'rotate(0)'
}
})
// 7.4 修改线中装文本矩形rect的位置
this.rects.attr('x', function (d) {
return getDis(d.source, d.target) / 2 - d3.select(this).attr('width') / 2
......@@ -604,6 +683,14 @@ export default class RelationGraph {
// 5.修改节点的位置
this.circles.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
this.squares
.attr('x', (d) => d.x - r)
.attr('y', (d) => d.y - r)
.attr('points', (d) => polygonPoints(d.x, d.y, r, 6))
this.pentagons
.attr('x', (d) => d.x - r)
.attr('y', (d) => d.y - r)
.attr('points', (d) => polygonPoints(d.x, d.y, r, 10))
// 让menu随圆圈移动
this.circles.each(function () {
......@@ -612,7 +699,25 @@ export default class RelationGraph {
const self = d3.select(this)
const cx = self.attr('cx')
const cy = self.attr('cy')
menuEle.attr('transform', `translate(${cx},${cy})`)
menuEle.attr('transform', `translate(${cx},${cy})`).raise()
}
})
this.squares.each(function () {
const menuEle = d3.select(this.nextSibling)
if (!menuEle.empty()) {
const self = d3.select(this)
const cx = +self.attr('x') + r
const cy = +self.attr('y') + r
menuEle.attr('transform', `translate(${cx},${cy})`).raise()
}
})
this.pentagons.each(function () {
const menuEle = d3.select(this.nextSibling)
if (!menuEle.empty()) {
const self = d3.select(this)
const cx = +self.attr('x') + r
const cy = +self.attr('y') + r
menuEle.attr('transform', `translate(${cx},${cy})`).raise()
}
})
}
......@@ -650,7 +755,7 @@ export default class RelationGraph {
this.dependsLink = Array.from(new Set(this.dependsLink))
// 隐藏节点
this.SVG.selectAll('circle')
this.SVG.selectAll('.node')
.filter((d) => this.dependsNode.indexOf(d.index) == -1)
.transition()
.style('opacity', 0.2)
......@@ -663,7 +768,7 @@ export default class RelationGraph {
} else {
// 取消高亮
// 恢复隐藏的线
this.SVG.selectAll('circle').transition().style('opacity', 1)
this.SVG.selectAll('.node').transition().style('opacity', 1)
// 恢复隐藏的线
this.SVG.selectAll('.edge').transition().style('opacity', 1)
......@@ -676,7 +781,7 @@ export default class RelationGraph {
highlightLinks(obj) {
if (obj) {
// 隐藏节点
this.SVG.selectAll('circle')
this.SVG.selectAll('.node')
.filter((d) => {
const sourceIndex = this.config.nodes.findIndex(
(e) => e.id == obj.source.id
......@@ -695,7 +800,7 @@ export default class RelationGraph {
.transition()
.style('opacity', 0.1)
} else {
this.SVG.selectAll('circle').transition().style('opacity', 1)
this.SVG.selectAll('.node').transition().style('opacity', 1)
this.SVG.selectAll('.edge').transition().style('opacity', 1)
}
}
......@@ -703,7 +808,7 @@ export default class RelationGraph {
setHighlights(key) {
this.curHighlightKey = key
this.config.isHighLight = false
this.SVG.selectAll('circle').transition().style('opacity', 1)
this.SVG.selectAll('.node').transition().style('opacity', 1)
this.SVG.selectAll('.edge').transition().style('opacity', 1)
this.dependsNode = []
this.dependsLink = []
......@@ -733,7 +838,7 @@ export default class RelationGraph {
})
// 隐藏节点
this.SVG.selectAll('circle')
this.SVG.selectAll('.node')
.filter((d) => this.dependsNode.indexOf(d.index) < 0)
.transition()
.style('opacity', 0.2)
......
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