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

引入naive丰富页面和功能

parent 1739d222
This diff is collapsed.
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"eslint-config-prettier": "^7.1.0", "eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.4.1", "eslint-plugin-vue": "^7.4.1",
"naive-ui": "^2.16.2",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"stylus": "^0.54.8", "stylus": "^0.54.8",
"vite": "^2.1.0" "vite": "^2.1.0"
......
<template> <template>
<Main /> <n-dialog-provider>
<n-message-provider>
<Main />
</n-message-provider>
</n-dialog-provider>
</template> </template>
<script> <script>
...@@ -15,9 +19,11 @@ html, ...@@ -15,9 +19,11 @@ html,
body body
margin 0 margin 0
padding 0 padding 0
overflow hidden
#app #app
font-family Avenir, Helvetica, Arial, sans-serif font-family Avenir, Helvetica, Arial, sans-serif
-webkit-font-smoothing antialiased -webkit-font-smoothing antialiased
-moz-osx-font-smoothing grayscale -moz-osx-font-smoothing grayscale
color #2c3e50 color #2c3e50
overflow hidden
</style> </style>
<template> <template>
<div ref="container" /> <div class="graph">
<div ref="container" style="width: 100%; height: 100%" />
<div class="legend">
<n-space>
<n-tag
v-for="n in nodeList"
:key="n.name"
size="small"
round
:color="n.color || defaultColor"
>
{{ `${n.name}(${n.val})` }}
</n-tag>
</n-space>
<n-space>
<n-tag
v-for="n in linkList"
:key="n.name"
size="small"
:color="n.color || defaultColor"
>
{{ `${n.name}(${n.val})` }}
</n-tag>
</n-space>
</div>
</div>
</template> </template>
<script> <script>
...@@ -23,16 +48,45 @@ export default { ...@@ -23,16 +48,45 @@ export default {
}, },
}, },
}, },
emits: ['more', 'add', 'del', 'branch'], emits: ['more', 'add', 'del', 'branch', 'curNode'],
setup(props, ctx) { setup(props, ctx) {
const colorList = {
default: 'skyblue',
System: '#58b2dc',
Property: '#ffb11b',
Subject: '#42b983',
}
let instance = null let instance = null
const container = ref(null) const container = ref(null)
const menuData = [ const menuData = [
{ icon: branch, action: (d) => ctx.emit('branch', d), title: '分支' }, {
{ icon: more, action: (d) => ctx.emit('more', d), title: '更多' }, key: 'branch',
{ icon: add, action: (d) => ctx.emit('add', d), title: '新增' }, icon: branch,
{ icon: del, action: (d) => ctx.emit('del', d), title: '删除' }, action: (d) => ctx.emit('branch', d),
title: '分支',
},
{
key: 'more',
icon: more,
action: (d) => ctx.emit('more', d),
title: '更多',
},
{
key: 'add',
icon: add,
action: (d) => ctx.emit('add', d),
title: '新增',
},
{
key: 'del',
icon: del,
action: (d) => ctx.emit('del', d),
title: '删除',
},
] ]
const setCurNode = (type) => {
ctx.emit('curNode', type)
}
function init() { function init() {
if (!container.value) return if (!container.value) return
if (instance) { if (instance) {
...@@ -42,8 +96,11 @@ export default { ...@@ -42,8 +96,11 @@ export default {
container.value, container.value,
props.data, props.data,
props.config, props.config,
menuData menuData,
colorList,
setCurNode
) )
setLegend()
} }
function setKey(key) { function setKey(key) {
instance.setKey(key) instance.setKey(key)
...@@ -53,13 +110,80 @@ export default { ...@@ -53,13 +110,80 @@ export default {
() => init(), () => init(),
{ immediate: true } { immediate: true }
) )
const nodeList = ref([])
const linkList = ref([])
function setLegend() {
const { nodes, links } = props.data
if (!nodes || !links) return
const nodeKey = {}
const linkKey = {}
nodes.forEach((node) => {
if (nodeKey.hasOwnProperty(node.nodeLabel)) {
nodeKey[node.nodeLabel] += 1
} else {
nodeKey[node.nodeLabel] = 1
}
})
links.forEach((link) => {
const key = `${link.source.nodeLabel}_${link.target.nodeLabel}`
if (linkKey.hasOwnProperty(key)) {
linkKey[key] += 1
} else {
linkKey[key] = 1
}
})
const nodeResult = []
for (const key in nodeKey) {
nodeResult.push({
name: key,
val: nodeKey[key],
color: {
color: colorList[key],
textColor: '#2c3e50',
borderColor: colorList[key],
},
})
}
const linkResult = []
for (const key in linkKey) {
linkResult.push({
name: key,
val: linkKey[key],
})
}
nodeList.value = nodeResult
linkList.value = linkResult
}
return { return {
colorList,
container, container,
setKey, setKey,
nodeList,
linkList,
defaultColor: {
color: '#a5abb6',
textColor: '#fff',
borderColor: '#a5abb6',
},
} }
}, },
} }
</script> </script>
<style lang="stylus" scoped>
.graph
background #f9fbfd
width 100%
height 100%
position relative
.legend
position absolute
top 20px
left 20px
</style>
<style lang="stylus"> <style lang="stylus">
.relation-tooltip .relation-tooltip
......
<template>
<div class="footer">
<n-space>
<n-tag v-show="nodeVal" size="small" round :color="color">
{{ nodeVal && nodeVal.nodeLabel }}
</n-tag>
<n-button
v-for="name in list"
:key="name"
type="info"
:dashed="name !== curKey"
size="tiny"
@click="setKey(name)"
>
{{ name }}
</n-button>
</n-space>
</div>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
name: 'Footer',
props: {
node: {
type: Object,
default: () => ({
nodeLabel: 'Subject',
}),
},
},
emits: ['set'],
setup(props, ctx) {
const colorList = {
default: 'skyblue',
System: '#58b2dc',
Property: '#ffb11b',
Subject: '#42b983',
}
const nodeVal = computed(
() =>
props.node || {
nodeLabel: 'Subject',
}
)
const color = computed(() => {
if (!nodeVal.value) return {}
return {
color: colorList[nodeVal.value.nodeLabel],
textColor: '#fff',
borderColor: colorList[nodeVal.value.nodeLabel],
}
})
const curKey = ref('subjectName')
function setKey(key) {
curKey.value = key
ctx.emit('set', key)
}
const list = ref([
'subjectName',
'propertyName',
'systemName',
'nodeId',
'nodeLabel',
])
return {
curKey,
setKey,
list,
color,
nodeVal,
}
},
})
</script>
<style lang="stylus" scoped>
.footer
display flex
align-items center
justify-content flex-end
padding 0 15px 8px
background #f8f9fb
box-shadow 10px 6px 10px 0 #000
margin-top 4px
</style>
<template> <template>
<D3 <div class="main">
ref="d3Ref" <Nav style="grid-area: nav" />
:data="data" <D3
:config="config" ref="d3Ref"
style="background: #fff; width: 100%; height: 100vh" style="grid-area: content"
@branch="handleBranch" class="graph"
@add="handleAdd" :data="data"
/> @branch="handleBranch"
<div class="btns"> @add="handleAdd"
<button @click="setKey('systemName')">systemName</button> @curNode="curNode = $event"
<button @click="setKey('propertyName')">propertyName</button> @del="handleDelete"
<button @click="setKey('subjectName')">subjectName</button> />
<Side style="grid-area: side" />
<Footer style="grid-area: footer" :node="curNode" @set="setKey" />
</div> </div>
<n-drawer v-model:show="showDrawer" :width="400" placement="left">
<n-drawer-content title="新增节点">
<n-form ref="formRef" :model="formData" :rules="rules">
<n-form-item path="propertyName" label="节点名称">
<n-input
v-model:value="formData.propertyName"
placeholder="请输入"
@keydown.enter.prevent
/>
</n-form-item>
<n-form-item label="关系" path="relationId">
<n-select
v-model:value="formData.relationId"
placeholder="请选择"
:options="relationOptions"
/>
</n-form-item>
<n-form-item label="所属系统" path="systemId">
<n-select
v-model:value="formData.systemId"
placeholder="请选择"
:options="systemOptions"
/>
</n-form-item>
<n-space justify="end">
<n-button :loading="isLoading" type="primary" @click="submit">
提交
</n-button>
</n-space>
</n-form>
</n-drawer-content>
</n-drawer>
</template> </template>
<script> <script>
import D3 from './d3.vue' import D3 from './d3.vue'
import Nav from './nav.vue'
import Side from './side.vue'
import Footer from './footer.vue'
import mockData from '@/util/mock.js' import mockData from '@/util/mock.js'
import { ref, shallowRef } from 'vue' import { ref } from 'vue'
import { ajax, api } from '@/ajax' import { ajax, api } from '@/ajax'
import { useMessage, useDialog } from 'naive-ui'
export default { export default {
name: 'Main', name: 'Main',
components: { D3 }, components: { D3, Nav, Side, Footer },
setup() { setup() {
const message = useMessage()
const dialog = useDialog()
const subjectId = ref(null)
const formRef = ref(null)
const formData = ref({
propertyName: null,
relationId: null,
})
const showDrawer = ref(false)
const isLoading = ref(false)
const d3Ref = ref(null) const d3Ref = ref(null)
const relationOptions = ref([])
const systemOptions = ref([])
const data = ref([]) const data = ref([])
function getData() {
ajax
.get({
url: api.GET_NODES,
})
.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 }))
})
}
getData()
ajax ajax
.get({ .get({
url: api.GET_NODES, url: api.GET_RELATIONS,
}) })
.then((res) => { .then((res) => {
console.log('nodes:', res) if (!res.data || !res.data.content) return
data.value = res.data.content relationOptions.value = res.data.content.map((item) => ({
label: item.relationName,
value: item.id,
}))
}) })
const config = ref({
config: {
strokeColor: 'skyblue',
},
})
function handleBranch({ nodeId }) { function handleBranch({ nodeId }) {
ajax ajax
.get({ .get({
...@@ -50,37 +112,104 @@ export default { ...@@ -50,37 +112,104 @@ export default {
// data.value = res.data.content // data.value = res.data.content
}) })
} }
function handleAdd() { function handleAdd({ nodeId }) {
const { nodes, links } = data.value subjectId.value = nodeId
data.value = { showDrawer.value = true
nodes: nodes.slice(0, 2),
links: links.slice(0, 2),
}
console.log(data.value)
} }
function setKey(key) { function setKey(key) {
d3Ref.value.setKey(key) d3Ref.value.setKey(key)
} }
function submit(e) {
e.preventDefault()
formRef.value.validate((errors) => {
if (!errors) {
isLoading.value = true
ajax
.post({
url: api.POST_NODE,
params: {
subjectId: subjectId.value,
...formData.value,
},
})
.then(() => {
getData()
isLoading.value = false
showDrawer.value = false
message.success('提交成功')
})
}
})
}
const curNode = ref(null)
function handleDelete(data) {
dialog.error({
title: '删除节点',
content: `确定是否删除 '${
data[data.nodeLabel.toLowerCase() + 'Name']
}' 节点?`,
positiveText: '确定',
negativeText: '取消',
maskClosable: false,
onPositiveClick: () => {
console.log('delete', data)
message.success('删除成功!')
},
})
}
return { return {
formRef,
formData,
showDrawer,
submit,
isLoading,
relationOptions,
systemOptions,
data, data,
mockData, mockData,
config,
handleBranch, handleBranch,
handleAdd, handleAdd,
d3Ref, d3Ref,
setKey, setKey,
rules: {
propertyName: [
{
required: true,
message: '请输入节点名称',
trigger: ['input', 'blur'],
},
],
relationId: [
{
required: true,
message: '请选择从属类型',
trigger: ['input', 'blur'],
},
],
systemId: [
{
required: true,
message: '请选择所属系统',
trigger: ['input', 'blur'],
},
],
},
curNode,
handleDelete,
} }
}, },
} }
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.btns .main
position fixed width 100vw
top 10px height 100vh
left 10px display grid
z-index 100 overflow hidden
>button grid-template-areas 'nav nav' 'side content' 'side footer'
margin-right 10px grid-template-columns 0px 1fr
cursor pointer grid-template-rows 0px 1fr 70px
padding 4px
font-weight bold !important
</style> </style>
<template>
<div class="nav"></div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Nav',
setup() {},
})
</script>
<style lang="stylus" scoped>
.nav
background #f9fbfd
border-bottom 4px solid #fff
</style>
<template>
<div></div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Side',
setup() {},
})
</script>
<style lang="stylus" scoped></style>
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
createApp(App).mount('#app') import {
create,
NButton,
NTag,
NSpace,
NDrawer,
NDrawerContent,
NForm,
NFormItem,
NInput,
NSelect,
NMessageProvider,
NDialogProvider,
} from 'naive-ui'
const naive = create({
components: [
NButton,
NTag,
NSpace,
NDrawer,
NDrawerContent,
NForm,
NFormItem,
NInput,
NSelect,
NMessageProvider,
NDialogProvider,
],
})
createApp(App).use(naive).mount('#app')
...@@ -38,7 +38,7 @@ function textWrap(text, width) { ...@@ -38,7 +38,7 @@ function textWrap(text, width) {
text.each(function () { text.each(function () {
const text = d3.select(this), const text = d3.select(this),
words = text.text().split('').reverse(), words = text.text().split('').reverse(),
lineHeight = 1.1, // ems lineHeight = 1, // ems
x = text.attr('x'), x = text.attr('x'),
y = text.attr('y'), y = text.attr('y'),
dy = 0 dy = 0
...@@ -83,7 +83,7 @@ const defaultConfig = { ...@@ -83,7 +83,7 @@ const defaultConfig = {
alphaDecay: 0.0228, // 控制力学模拟衰减率 alphaDecay: 0.0228, // 控制力学模拟衰减率
r: 45, // 圈圈的半径 [30 - 45] r: 45, // 圈圈的半径 [30 - 45]
nodeColor: 'skyblue', // 圈圈节点背景颜色 nodeColor: 'skyblue', // 圈圈节点背景颜色
fontColor: '#000', // 圈圈内文字的颜色 fontColor: '#2c3e50', // 圈圈内文字的颜色
linkSrc: 30, // 划线时候的弧度 linkSrc: 30, // 划线时候的弧度
linkColor: 'gray', // 链接线默认的颜色 linkColor: 'gray', // 链接线默认的颜色
strokeColor: 'gray', // 圈圈外围包裹的颜色 strokeColor: 'gray', // 圈圈外围包裹的颜色
...@@ -105,8 +105,10 @@ const defaultConfig = { ...@@ -105,8 +105,10 @@ const defaultConfig = {
let menu = null let menu = null
export default class RelationGraph { export default class RelationGraph {
constructor(selector, data, configs = {}, menuData) { constructor(selector, data, configs = {}, menuData, colorList, setCurNode) {
this.menuData = menuData this.menuData = menuData
this.colorLIst = colorList
this.setCurNode = setCurNode
const mapW = selector.offsetWidth const mapW = selector.offsetWidth
const mapH = selector.offsetHeight const mapH = selector.offsetHeight
...@@ -149,7 +151,8 @@ export default class RelationGraph { ...@@ -149,7 +151,8 @@ export default class RelationGraph {
setKey(key) { setKey(key) {
d3.selectAll('.node-text') d3.selectAll('.node-text')
.text((d) => (key && d[key]) || d[d.nodeLabel.toLowerCase() + 'Name']) .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.8)
} }
update(data) { update(data) {
...@@ -160,12 +163,17 @@ export default class RelationGraph { ...@@ -160,12 +163,17 @@ export default class RelationGraph {
d3.selectAll('g.circle-wrapper').data(this.config.nodes) d3.selectAll('g.circle-wrapper').data(this.config.nodes)
d3.selectAll('g.edge').data(this.config.links) d3.selectAll('g.edge').data(this.config.links)
} }
openMenu(self, d) { openMenu(self, d, type) {
menu = new RadialMenu() this.setCurNode(d)
.radius(50) menu = new RadialMenu().radius(50).thickness(40).appendTo(self.parentNode)
.thickness(40) if (!type) {
.appendTo(self.parentNode) menu.show(this.menuData, d)
.show(this.menuData, d) } else {
menu.show(
this.menuData.filter((e) => e.key === type),
d
)
}
} }
closeMenu() { closeMenu() {
if (menu) { if (menu) {
...@@ -272,6 +280,7 @@ export default class RelationGraph { ...@@ -272,6 +280,7 @@ export default class RelationGraph {
.attr('x', this.config.r) .attr('x', this.config.r)
.attr('y', this.config.r) // edit .attr('y', this.config.r) // edit
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('fill', this.config.fontColor) .attr('fill', this.config.fontColor)
.style('font-size', this.config.r / 3.8) .style('font-size', this.config.r / 3.8)
.text((d) => d[d.nodeLabel.toLowerCase() + 'Name']) .text((d) => d[d.nodeLabel.toLowerCase() + 'Name'])
...@@ -330,7 +339,7 @@ export default class RelationGraph { ...@@ -330,7 +339,7 @@ export default class RelationGraph {
.attr('y', -10) .attr('y', -10)
.attr('width', 40) .attr('width', 40)
.attr('height', 20) .attr('height', 20)
.attr('fill', 'white') .attr('fill', '#f9fbfd')
.attr('stroke', 'transparent') .attr('stroke', 'transparent')
// 5.5 文本标签 坐标(x,y)代表 文本的左下角的点 // 5.5 文本标签 坐标(x,y)代表 文本的左下角的点
...@@ -393,7 +402,11 @@ export default class RelationGraph { ...@@ -393,7 +402,11 @@ export default class RelationGraph {
self.closeMenu() self.closeMenu()
} else { } else {
self.closeMenu() self.closeMenu()
self.openMenu(this, d) if (d.nodeLabel === 'Subject') {
self.openMenu(this, d)
} else {
self.openMenu(this, d, 'del')
}
} }
//阻止事件冒泡 阻止事件默认行为 //阻止事件冒泡 阻止事件默认行为
e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true) e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true)
......
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