Merge pull request #181 from veops/dev_ui

前端更新
This commit is contained in:
wang-liang0615 2023-09-26 20:34:27 +08:00 committed by GitHub
commit 4f7eddf906
4 changed files with 352 additions and 237 deletions

View File

@ -47,6 +47,134 @@ export default {
this.$store.dispatch('setWindowSize') this.$store.dispatch('setWindowSize')
}) })
) )
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
}, },
beforeDestroy() { beforeDestroy() {
clearInterval(this.timer) clearInterval(this.timer)

View File

@ -1,163 +1,168 @@
<template> <template>
<div <div
id="ci-detail-relation-topo" id="ci-detail-relation-topo"
class="ci-detail-relation-topo" class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }" :style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
></div> ></div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag' import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation' import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import Node from './node.js' import Node from './node.js'
import 'butterfly-dag/dist/index.css' import 'butterfly-dag/dist/index.css'
import './index.less' import './index.less'
export default { export default {
name: 'CiDetailRelationTopo', name: 'CiDetailRelationTopo',
data() { data() {
return { return {
topoData: {}, topoData: {},
} exsited_ci: [],
}, }
inject: ['ci_types'], },
mounted() {}, inject: ['ci_types'],
methods: { mounted() {},
init() { methods: {
const root = document.getElementById('ci-detail-relation-topo') init() {
this.canvas = new TreeCanvas({ const root = document.getElementById('ci-detail-relation-topo')
root: root, this.canvas = new TreeCanvas({
disLinkable: false, // 可删除连线 root: root,
linkable: false, // 可连线 disLinkable: false, // 可删除连线
draggable: true, // 可拖动 linkable: false, // 可连线
zoomable: true, // 可放大 draggable: true, // 可拖动
moveable: true, // 可平移 zoomable: true, // 可放大
theme: { moveable: true, // 可平移
edge: { theme: {
shapeType: 'AdvancedBezier', edge: {
arrow: true, shapeType: 'AdvancedBezier',
arrowPosition: 1, arrow: true,
}, arrowPosition: 1,
}, },
layout: { },
type: 'mindmap', layout: {
options: { type: 'mindmap',
direction: 'H', options: {
getSide(d) { direction: 'H',
return d.data.side || 'right' getSide(d) {
}, return d.data.side || 'right'
getHeight(d) { },
return 10 getHeight(d) {
}, return 10
getWidth(d) { },
return 40 getWidth(d) {
}, return 40
getHGap(d) { },
return 80 getHGap(d) {
}, return 80
getVGap(d) { },
return 40 getVGap(d) {
}, return 40
}, },
}, },
}) },
this.canvas.setZoomable(true, true) })
this.canvas.on('events', ({ type, data }) => { this.canvas.setZoomable(true, true)
const sourceNode = data?.id || null this.canvas.on('events', ({ type, data }) => {
if (type === 'custom:clickLeft') { const sourceNode = data?.id || null
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => { if (type === 'custom:clickLeft') {
this.redrawData(res, sourceNode, 'left') searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
}) this.redrawData(res, sourceNode, 'left')
} })
if (type === 'custom:clickRight') { }
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => { if (type === 'custom:clickRight') {
this.redrawData(res, sourceNode, 'right') searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
}) this.redrawData(res, sourceNode, 'right')
} })
}) }
}, })
setTopoData(data) { },
this.canvas = null setTopoData(data) {
this.init() this.canvas = null
this.topoData = _.cloneDeep(data) this.init()
this.canvas.draw(data, {}, () => { this.topoData = _.cloneDeep(data)
this.canvas.focusCenterWithAnimate() this.canvas.draw(data, {}, () => {
}) this.canvas.focusCenterWithAnimate()
}, })
redrawData(res, sourceNode, side) { },
const newNodes = [] redrawData(res, sourceNode, side) {
const newEdges = [] const newNodes = []
if (!res.result.length) { const newEdges = []
this.$message.info('无层级关系!') if (!res.result.length) {
return this.$message.info('无层级关系!')
} return
const ci_types_list = this.ci_types() }
res.result.forEach((r) => { const ci_types_list = this.ci_types()
const _findCiType = ci_types_list.find((item) => item.id === r._type) res.result.forEach((r) => {
newNodes.push({ if (!this.exsited_ci.includes(r._id)) {
id: `${r._id}`, const _findCiType = ci_types_list.find((item) => item.id === r._type)
Class: Node, newNodes.push({
title: r.ci_type_alias || r.ci_type, id: `${r._id}`,
name: r.ci_type, Class: Node,
side: side, title: r.ci_type_alias || r.ci_type,
unique_alias: r.unique_alias, name: r.ci_type,
unique_name: r.unique, side: side,
unique_value: r[r.unique], unique_alias: r.unique_alias,
children: [], unique_name: r.unique,
icon: _findCiType?.icon || '', unique_value: r[r.unique],
endpoints: [ children: [],
{ icon: _findCiType?.icon || '',
id: 'left', endpoints: [
orientation: [-1, 0], {
pos: [0, 0.5], id: 'left',
}, orientation: [-1, 0],
{ pos: [0, 0.5],
id: 'right', },
orientation: [1, 0], {
pos: [0, 0.5], id: 'right',
}, orientation: [1, 0],
], pos: [0, 0.5],
}) },
newEdges.push({ ],
id: `${r._id}`, })
source: 'right', }
target: 'left', newEdges.push({
sourceNode: side === 'right' ? sourceNode : `${r._id}`, id: `${r._id}`,
targetNode: side === 'right' ? `${r._id}` : sourceNode, source: 'right',
type: 'endpoint', target: 'left',
}) sourceNode: side === 'right' ? sourceNode : `${r._id}`,
}) targetNode: side === 'right' ? `${r._id}` : sourceNode,
const { nodes, edges } = this.canvas.getDataMap() type: 'endpoint',
// 删除原节点和边 })
this.canvas.removeNodes(nodes.map((node) => node.id)) })
this.canvas.removeEdges(edges) const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
const _topoData = _.cloneDeep(this.topoData) this.canvas.removeNodes(nodes.map((node) => node.id))
let result this.canvas.removeEdges(edges)
const getTreeItem = (data, id) => {
for (let i = 0; i < data.length; i++) { const _topoData = _.cloneDeep(this.topoData)
if (data[i].id === id) { _topoData.edges.push(...newEdges)
result = data[i] // 结果赋值 let result
break const getTreeItem = (data, id) => {
} else { for (let i = 0; i < data.length; i++) {
if (data[i].children && data[i].children.length) { if (data[i].id === id) {
getTreeItem(data[i].children, id) result = data[i] // 结果赋值
} result.edges = _topoData.edges
} break
} } else {
} if (data[i].children && data[i].children.length) {
getTreeItem(data[i].children, id)
getTreeItem(_topoData.nodes.children, sourceNode) }
result.children.push(...newNodes) }
_topoData.edges.push(...newEdges) }
}
this.topoData = _topoData
this.canvas.draw(_topoData, {}, () => {}) getTreeItem(_topoData.nodes.children, sourceNode)
}, result.children.push(...newNodes)
},
} this.topoData = _topoData
</script> this.canvas.draw(_topoData, {}, () => {})
this.exsited_ci = [...new Set([...this.exsited_ci, ...res.result.map((r) => r._id)])]
<style></style> },
},
}
</script>
<style></style>

View File

@ -1,56 +1,56 @@
/* eslint-disable no-useless-constructor */ /* eslint-disable no-useless-constructor */
import { TreeNode } from 'butterfly-dag' import { TreeNode } from 'butterfly-dag'
import $ from 'jquery' import $ from 'jquery'
class BaseNode extends TreeNode { class BaseNode extends TreeNode {
constructor(opts) { constructor(opts) {
super(opts) super(opts)
} }
draw = (opts) => { draw = (opts) => {
const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`) const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`)
.css('top', opts.top) .css('top', opts.top)
.css('left', opts.left) .css('left', opts.left)
.attr('id', opts.id) .attr('id', opts.id)
let icon let icon
if (opts.options.icon) { if (opts.options.icon) {
if (opts.options.icon.split('$$')[2]) { if (opts.options.icon.split('$$')[2]) {
icon = $(`<img style="max-width:16px;max-height:16px;" src="/api/common-setting/v1/file/${opts.options.icon.split('$$')[3]}" />`) icon = $(`<img style="max-width:16px;max-height:16px;" src="/api/common-setting/v1/file/${opts.options.icon.split('$$')[3]}" />`)
} else { } else {
icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`) icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`)
} }
} else { } else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`) icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
} }
const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`) const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`)
const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`) const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`)
container.append(icon) container.append(icon)
container.append(titleContent) container.append(titleContent)
container.append(uniqueDom) container.append(uniqueDom)
if (opts.options.side && !opts.options.children.length) { if (opts.options.side && (!opts.options.children.length && !(opts.options.edges && opts.options.edges.length && opts.options.edges.find(e => e.source === opts.options.side && e.sourceNode === opts.options.id)))) {
const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`) const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`)
container.append(addIcon) container.append(addIcon)
addIcon.on('click', () => { addIcon.on('click', () => {
if (opts.options.side === 'left') { if (opts.options.side === 'left') {
this.emit('events', { this.emit('events', {
type: 'custom:clickLeft', type: 'custom:clickLeft',
data: { ...this } data: { ...this }
}) })
} }
if (opts.options.side === 'right') { if (opts.options.side === 'right') {
this.emit('events', { this.emit('events', {
type: 'custom:clickRight', type: 'custom:clickRight',
data: { ...this } data: { ...this }
}) })
} }
}) })
} }
return container[0] return container[0]
} }
} }
export default BaseNode export default BaseNode

View File

@ -194,7 +194,6 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import { getWX } from '../../api/perm'
import { addTrigger, updateTrigger, deleteTrigger, getAllDagsName } from '../../api/CIType' import { addTrigger, updateTrigger, deleteTrigger, getAllDagsName } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp' import FilterComp from '@/components/CMDBFilterComp'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue' import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
@ -264,16 +263,6 @@ export default {
} }
}, },
computed: { computed: {
filterWxUsers() {
if (!this.filterValue) {
return this.WxUsers
}
return this.WxUsers.filter(
(user) =>
user.nickname.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0 ||
user.username.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
)
},
canAddTriggerAttr() { canAddTriggerAttr() {
return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4') return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4')
}, },
@ -299,7 +288,6 @@ export default {
}, },
createFromTriggerTable(attrList) { createFromTriggerTable(attrList) {
this.visible = true this.visible = true
this.getWxList()
// this.getDags() // this.getDags()
this.attrList = attrList this.attrList = attrList
this.triggerId = null this.triggerId = null
@ -319,7 +307,6 @@ export default {
}, },
async open(property, attrList) { async open(property, attrList) {
this.visible = true this.visible = true
this.getWxList()
// await this.getDags() // await this.getDags()
this.attrList = attrList this.attrList = attrList
if (property.has_trigger) { if (property.has_trigger) {
@ -393,11 +380,6 @@ export default {
this.filterExp = '' this.filterExp = ''
this.visible = false this.visible = false
}, },
getWxList() {
getWX().then((res) => {
this.WxUsers = res.filter((item) => item.wx_id)
})
},
filterChange(value) { filterChange(value) {
this.filterValue = value this.filterValue = value
}, },