feat(ui): update relative views menu display

This commit is contained in:
songlh 2024-12-26 13:58:53 +08:00
parent b669775cd6
commit bc3201656c
14 changed files with 914 additions and 443 deletions
cmdb-ui
public/iconfont
src
components/Menu
modules/cmdb
components/cmdbGrant
lang
router
views/relation_views

View File

@ -54,6 +54,54 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xea0b;</span>
<div class="name">veops-servicetree</div>
<div class="code-name">&amp;#xea0b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0a;</span>
<div class="name">veops-switch (1)</div>
<div class="code-name">&amp;#xea0a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea09;</span>
<div class="name">veops-label</div>
<div class="code-name">&amp;#xea09;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea08;</span>
<div class="name">top_acl</div>
<div class="code-name">&amp;#xea08;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea06;</span>
<div class="name">top_ticket</div>
<div class="code-name">&amp;#xea06;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea07;</span>
<div class="name">top_agent</div>
<div class="code-name">&amp;#xea07;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea05;</span>
<div class="name">itsm-table_download</div>
<div class="code-name">&amp;#xea05;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea04;</span>
<div class="name">itsm-image_download</div>
<div class="code-name">&amp;#xea04;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea02;</span>
<div class="name">veops-rear</div>
@ -6162,9 +6210,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1735191938771') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -6190,6 +6238,78 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont veops-servicetree"></span>
<div class="name">
veops-servicetree
</div>
<div class="code-name">.veops-servicetree
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-switch1"></span>
<div class="name">
veops-switch (1)
</div>
<div class="code-name">.veops-switch1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-label"></span>
<div class="name">
veops-label
</div>
<div class="code-name">.veops-label
</div>
</li>
<li class="dib">
<span class="icon iconfont top_acl"></span>
<div class="name">
top_acl
</div>
<div class="code-name">.top_acl
</div>
</li>
<li class="dib">
<span class="icon iconfont top_ticket"></span>
<div class="name">
top_ticket
</div>
<div class="code-name">.top_ticket
</div>
</li>
<li class="dib">
<span class="icon iconfont top_agent"></span>
<div class="name">
top_agent
</div>
<div class="code-name">.top_agent
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-table_download"></span>
<div class="name">
itsm-table_download
</div>
<div class="code-name">.itsm-table_download
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-image_download"></span>
<div class="name">
itsm-image_download
</div>
<div class="code-name">.itsm-image_download
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rear"></span>
<div class="name">
@ -15352,6 +15472,70 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-servicetree"></use>
</svg>
<div class="name">veops-servicetree</div>
<div class="code-name">#veops-servicetree</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-switch1"></use>
</svg>
<div class="name">veops-switch (1)</div>
<div class="code-name">#veops-switch1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-label"></use>
</svg>
<div class="name">veops-label</div>
<div class="code-name">#veops-label</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_acl"></use>
</svg>
<div class="name">top_acl</div>
<div class="code-name">#top_acl</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_ticket"></use>
</svg>
<div class="name">top_ticket</div>
<div class="code-name">#top_ticket</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_agent"></use>
</svg>
<div class="name">top_agent</div>
<div class="code-name">#top_agent</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-table_download"></use>
</svg>
<div class="name">itsm-table_download</div>
<div class="code-name">#itsm-table_download</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-image_download"></use>
</svg>
<div class="name">itsm-image_download</div>
<div class="code-name">#itsm-image_download</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rear"></use>

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1735191938771') format('truetype');
}
.iconfont {
@ -13,6 +13,38 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-servicetree:before {
content: "\ea0b";
}
.veops-switch1:before {
content: "\ea0a";
}
.veops-label:before {
content: "\ea09";
}
.top_acl:before {
content: "\ea08";
}
.top_ticket:before {
content: "\ea06";
}
.top_agent:before {
content: "\ea07";
}
.itsm-table_download:before {
content: "\ea05";
}
.itsm-image_download:before {
content: "\ea04";
}
.veops-rear:before {
content: "\ea02";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,62 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "42930714",
"name": "veops-servicetree",
"font_class": "veops-servicetree",
"unicode": "ea0b",
"unicode_decimal": 59915
},
{
"icon_id": "42921461",
"name": "veops-switch (1)",
"font_class": "veops-switch1",
"unicode": "ea0a",
"unicode_decimal": 59914
},
{
"icon_id": "42857659",
"name": "veops-label",
"font_class": "veops-label",
"unicode": "ea09",
"unicode_decimal": 59913
},
{
"icon_id": "42790685",
"name": "top_acl",
"font_class": "top_acl",
"unicode": "ea08",
"unicode_decimal": 59912
},
{
"icon_id": "42790687",
"name": "top_ticket",
"font_class": "top_ticket",
"unicode": "ea06",
"unicode_decimal": 59910
},
{
"icon_id": "42790686",
"name": "top_agent",
"font_class": "top_agent",
"unicode": "ea07",
"unicode_decimal": 59911
},
{
"icon_id": "42732510",
"name": "itsm-table_download",
"font_class": "itsm-table_download",
"unicode": "ea05",
"unicode_decimal": 59909
},
{
"icon_id": "42732515",
"name": "itsm-image_download",
"font_class": "itsm-image_download",
"unicode": "ea04",
"unicode_decimal": 59908
},
{
"icon_id": "42510712",
"name": "veops-rear",

Binary file not shown.

View File

@ -171,7 +171,6 @@ export default {
},
renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/'
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
@ -205,11 +204,9 @@ export default {
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
</a-popover>
}
{isShowGrant && <a-icon class="custom-menu-extra-ellipsis" onClick={e => this.handlePerm(e, menu, 'RelationView')} type="user-add" />}
</span>
</tag>
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
{isShowGrant && <CMDBGrant ref="cmdbGrantRelationView" resourceType="RelationView" app_id="cmdb" />}
</Item>
)
},

View File

@ -88,6 +88,7 @@ import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'GrantComp',
@ -186,6 +187,13 @@ export default {
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
Object.keys(res).forEach((key) => {
const attr_filter = res?.[key]?.attr_filter
if (attr_filter?.length) {
res[key].attr_filter = attr_filter.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item))
}
})
this.filerPerimissions = res
})
},

View File

@ -26,7 +26,8 @@ const cmdb_en = {
ad: 'AutoDiscovery',
cidetail: 'CI Detail',
scene: 'Scene',
dcim: 'DCIM'
dcim: 'DCIM',
serviceTree: 'Service Tree'
},
ciType: {
ciType: 'CIType',

View File

@ -26,7 +26,8 @@ const cmdb_zh = {
ad: '自动发现',
cidetail: 'CI 详情',
scene: '场景',
dcim: '数据中心'
dcim: '数据中心',
serviceTree: '服务树'
},
ciType: {
ciType: '模型',

View File

@ -27,6 +27,17 @@ const genCmdbRoutes = async () => {
name: 'cmdb_disabled1',
meta: { title: 'cmdb.menu.resources', disabled: true },
},
{
path: '/cmdb/relationviews/:viewId?',
name: 'cmdb_relation_views',
component: () => import('../views/relation_views/index'),
meta: {
title: 'cmdb.menu.serviceTree',
appName: 'cmdb',
icon: 'veops-servicetree',
keepAlive: false
},
},
{
path: '/cmdb/resourceviews',
name: 'cmdb_resource_views',
@ -194,15 +205,14 @@ const genCmdbRoutes = async () => {
} else {
routes.redirect = '/cmdb/dashboard'
}
const relationViews = relation.name2id.map(item => {
return {
path: `/cmdb/relationviews/${item[1]}`,
name: `cmdb_relation_views_${item[1]}`,
component: () => import('../views/relation_views/index'),
meta: { title: item[0], icon: 'ops-cmdb-relation', selectedIcon: 'ops-cmdb-relation', keepAlive: false, name: item[0] },
if (relation?.name2id?.length === 0) {
const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views')
if (relationViewRouteIndex >= 0) {
routes.children.splice(relationViewRouteIndex, 1)
}
})
routes.children.splice(resourceViewsIndex, 0, ...relationViews)
}
return routes
}

View File

@ -11,7 +11,48 @@
>
<template #one>
<div class="relation-views-left" :style="{ height: `${windowHeight - 64}px` }">
<div class="relation-views-left-header" :title="$route.meta.name">{{ $route.meta.name }}</div>
<div class="relation-views-left-header">
<div class="relation-views-left-header-icon">
<ops-icon type="ops-cmdb-relation" />
</div>
<div class="relation-views-left-header-name relation-views-text-scroll">
<span>
{{ viewName }}
</span>
</div>
<a-dropdown
overlayClassName="relation-views-left-header-dropdown"
>
<div class="relation-views-left-header-down">
<ops-icon type="veops-switch1" />
</div>
<a-menu
slot="overlay"
:selectedKeys="[viewId]"
class="relation-views-left-header-menu"
>
<a-menu-item
v-for="(item) in relationViewMenu"
:key="item.id"
@click="clickRelationViewMenu(item.id)"
>
<a class="relation-views-left-header-menu-item">
<div class="relation-views-left-header-menu-name relation-views-text-scroll">
<span>{{ item.name }}</span>
</div>
<a-icon
class="relation-views-left-header-menu-grant"
type="user-add"
@click.stop="handlePerm(item.name)"
/>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
<a-input
:placeholder="$t('cmdb.serviceTree.searchTips')"
class="relation-views-left-input"
@ -214,7 +255,7 @@
</SplitPane>
</div>
<a-alert
:message="$t('cmdb.serviceTreealert1')"
:message="$t('noData')"
banner
v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert>
@ -271,6 +312,8 @@ import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
const relationViewKeyStorage = 'cmdb_relation_view_menu_key'
export default {
name: 'RelationViews',
components: {
@ -370,7 +413,7 @@ export default {
return !!this.selectedRowKeys.length
},
topo_flatten() {
return this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
return this.relationViews?.views[this.viewName]?.topo_flatten ?? []
},
descendant_ids() {
return this.topo_flatten.slice(this.treeKeys.length).join(',')
@ -393,6 +436,15 @@ export default {
leaf_tree_sort() {
return this.viewOption?.sort ?? 1
},
relationViewMenu() {
const name2id = this?.relationViews?.name2id || []
return name2id.map((item) => {
return {
id: item?.[1] || -1,
name: item?.[0] || ''
}
})
}
},
provide() {
return {
@ -429,10 +481,6 @@ export default {
},
inject: ['reload'],
watch: {
'$route.path': function(newPath, oldPath) {
this.viewId = this.$route.params.viewId
this.reload()
},
pageNo: function(newPage, oldPage) {
this.loadData({ parameter: { pageNo: newPage }, refreshType: undefined, sortByTable: this.sortByTable })
},
@ -869,36 +917,46 @@ export default {
this.relationViews = res
}
if ((Object.keys(this.relationViews.views) || []).length) {
this.viewId =
parseInt(this.$route.path.split('/')[this.$route.path.split('/').length - 1]) ||
this.relationViews.name2id[0][1]
this.relationViews.name2id.forEach((item) => {
if (item[1] === this.viewId) {
this.viewName = item[0]
}
})
this.levels = this.relationViews.views[this.viewName].topo
this.origShowTypes = this.relationViews.views[this.viewName].show_types
const showTypeIds = []
this.origShowTypes.forEach((item) => {
showTypeIds.push(item.id)
})
this.origShowTypeIds = showTypeIds
this.leaf2showTypes = this.relationViews.views[this.viewName].leaf2show_types
this.node2ShowTypes = this.relationViews.views[this.viewName].node2show_types
this.level2constraint = this.relationViews.views[this.viewName].level2constraint
this.leaf = this.relationViews.views[this.viewName].leaf
this.currentView = `${this.viewId}`
this.typeId = this.levels[0][0]
this.viewOption = this.relationViews.views[this.viewName].option ?? {}
let viewId = parseInt(localStorage.getItem(relationViewKeyStorage)) || parseInt(this.$route.params.viewId) || this.relationViews.name2id[0][1]
let viewName = null
this.$nextTick(() => {
this.refreshTable()
})
const currentView = this.relationViews.name2id.find((item) => item?.[1] === viewId)
if (currentView) {
viewName = currentView[0]
} else {
viewId = this.relationViews.name2id[0][1]
viewName = this.relationViews.name2id[0][0]
}
localStorage.setItem(relationViewKeyStorage, viewId)
this.viewId = viewId
this.viewName = viewName
this.refreshData()
}
})
},
refreshData() {
this.levels = this.relationViews.views[this.viewName].topo
this.origShowTypes = this.relationViews.views[this.viewName].show_types
const showTypeIds = []
this.origShowTypes.forEach((item) => {
showTypeIds.push(item.id)
})
this.origShowTypeIds = showTypeIds
this.leaf2showTypes = this.relationViews.views[this.viewName].leaf2show_types
this.node2ShowTypes = this.relationViews.views[this.viewName].node2show_types
this.level2constraint = this.relationViews.views[this.viewName].level2constraint
this.leaf = this.relationViews.views[this.viewName].leaf
this.currentView = `${this.viewId}`
this.typeId = this.levels[0][0]
this.viewOption = this.relationViews.views[this.viewName].option ?? {}
this.$nextTick(() => {
this.refreshTable()
})
},
async loadColumns() {
if (this.currentTypeId[0]) {
this.getAttributeList()
@ -954,7 +1012,7 @@ export default {
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>,
content: that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] }),
onOk() {
deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {
that.$message.success(that.$t('deleteSuccess'))
@ -1063,18 +1121,20 @@ export default {
})
}
},
handlePerm() {
handlePerm(resourceName) {
const _resource_name = resourceName ?? this.viewName
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name: 'RelationView',
perm: 'grant',
resource_name: this.$route.meta.title,
resource_name: _resource_name,
}).then((res) => {
if (res.result) {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
this.$nextTick(() => {
this.$refs.cmdbGrant.open({ name: this.$route.meta.title, cmdbGrantType: 'relation_view' })
this.$refs.cmdbGrant.open({ name: _resource_name, cmdbGrantType: 'relation_view' })
})
})
} else {
@ -1100,7 +1160,7 @@ export default {
},
columnDrop() {
this.$nextTick(() => {
const xTable = this.$refs.xTable.getVxetableRef()
const xTable = this.$refs?.xTable?.getVxetableRef?.()
this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{
@ -1282,7 +1342,7 @@ export default {
async openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.$route.meta.name,
ciTypeName: this.viewName,
})
},
batchDownload({ filename, type, checkedKeys }) {
@ -1629,6 +1689,13 @@ export default {
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
},
clickRelationViewMenu(id) {
if (id) {
localStorage.setItem(relationViewKeyStorage, id)
this.reload()
}
}
},
}
@ -1649,17 +1716,61 @@ export default {
overflow: auto;
}
.relation-views-left-header {
border-left: 4px solid @primary-color;
height: 32px;
line-height: 32px;
padding-left: 12px;
margin-bottom: 12px;
color: @text-color_1;
font-weight: bold;
display: flex;
align-items: center;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
padding-bottom: 12px;
border-bottom: @border-color-base;
margin-bottom: 14px;
&-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 22px;
background-color: @primary-color;
i {
font-size: 12px;
color: #FFFFFF;
}
}
&-name {
margin-left: 9px;
span {
font-size: 17px;
font-weight: 700;
color: @primary-color;
}
}
&-down {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 1px;
background-color: @primary-color_3;
cursor: pointer;
margin-left: auto;
i {
font-size: 18px;
color: @primary-color;
}
&:hover {
background-color: @primary-color_4;
}
}
}
.ant-tree li {
padding: 2px 0;
@ -1680,14 +1791,14 @@ export default {
}
.relation-views-left-input {
margin-bottom: 12px;
input {
background-color: transparent;
border-top: none;
border-right: none;
border-left: none;
}
.ant-input:focus {
box-shadow: none;
.ant-input {
background-color: #FFFFFF;
border: solid 1px transparent;
&:hover, &:focus {
border-color: @primary-color;
}
}
}
}
@ -1703,4 +1814,75 @@ export default {
}
}
}
.relation-views-left-header-dropdown {
background-color: #FFFFFF;
.relation-views-left-header-menu {
box-shadow: none;
max-height: 400px;
min-height: 150px;
overflow-y: auto;
overflow-x: hidden;
&-item {
width: 150px;
overflow: hidden;
display: flex !important;
align-items: center;
&:hover {
.relation-views-left-header-menu-grant {
display: inline-block;
}
}
}
&-name {
margin-right: 8px;
}
&-grant {
margin-left: 8px;
flex-shrink: 0;
font-size: 12px;
display: none;
margin-left: auto;
color: @text-color_4;
&:hover {
color: @primary-color;
}
}
}
}
.relation-views-text-scroll {
max-width: 100%;
overflow: hidden;
& > span {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&:hover {
& > span {
overflow: visible;
animation: scroll-left 3s linear infinite;
}
}
@keyframes scroll-left {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
}
</style>

View File

@ -1,367 +1,367 @@
<template>
<div
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span class="relation-views-node-switch">
<a-icon v-if="!isLeaf" :type="switchIcon"></a-icon>
</span>
<span class="relation-views-node-content">
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
v-if="icon.includes('$$') && icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else-if="icon.includes('$$') && icon.split('$$')[0]"
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
<span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
</template>
<span
class="relation-views-node-title"
v-if="!isEditNodeName"
:title="title"
v-highlight="{ value: fullSearchValue, class: 'relation-views-node-title-highlight' }"
>{{ title }}
</span>
<a-input
ref="input"
@blur="changeNodeName"
@pressEnter="
() => {
$refs.input.blur()
}
"
size="small"
v-else
v-model="editNodeName"
:style="{ marginLeft: '5px' }"
/>
<span class="relation-views-node-number">{{ number }}</span>
<a-dropdown overlayClassName="relation-views-node-dropdown" :overlayStyle="{ width: '200px' }">
<a-menu slot="overlay" @click="({ key: menuKey }) => onContextMenuClick(this.treeKey, menuKey)">
<template v-if="showBatchLevel === null">
<a-divider orientation="left">{{ $t('cmdb.relation') }}</a-divider>
<a-menu-item
v-for="item in menuList"
:key="item.id"
><a-icon type="plus-circle" />{{ $t('add') }} {{ item.alias }}</a-menu-item
>
<a-menu-item
v-if="showDelete"
key="delete"
><ops-icon type="icon-xianxing-delete" />{{
$t('cmdb.serviceTree.deleteNode', { name: title })
}}</a-menu-item
>
<a-divider orientation="left">{{ $t('cmdb.components.perm') }}</a-divider>
<a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
<a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
<a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
<a-menu-divider />
<a-menu-item
key="editNodeName"
><ops-icon type="icon-xianxing-edit" />{{ $t('cmdb.serviceTree.editNodeName') }}</a-menu-item
>
<a-menu-item key="batch"><ops-icon type="veops-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item>
</template>
<template v-else>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchGrant"
><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchRevoke"
><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<a-menu-divider />
<template v-if="showBatchLevel > 0">
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchDelete"
><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.remove') }}</a-menu-item
>
<a-menu-divider />
</template>
<a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
</template>
</a-menu>
<a-icon class="relation-views-node-operation" type="ellipsis" />
</a-dropdown>
</span>
</div>
</template>
<script>
import { updateCI } from '../../../api/ci.js'
import highlight from '@/directive/highlight'
export default {
name: 'ContextMenu',
directives: {
highlight,
},
props: {
treeNodeData: {
type: Object,
default: () => {},
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
fullSearchValue: {
type: String,
default: '',
},
},
data() {
return {
switchIcon: 'caret-right',
isEditNodeName: false,
editNodeName: '',
}
},
computed: {
childLength() {
return this.number
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
return { id: item.id, alias: item.alias || item.name }
})
}
return _menuList
},
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
title() {
return this.treeNodeData.title
},
number() {
return this.treeNodeData.number
},
treeKey() {
return this.treeNodeData.key
},
isLeaf() {
return this.treeNodeData.isLeaf
},
showName() {
return this.treeNodeData.showName
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
if (menuKey === 'editNodeName') {
this.isEditNodeName = true
this.editNodeName = this.title
this.$nextTick(() => {
this.$refs.input.focus()
})
return
}
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'caret-right' ? 'caret-down' : 'caret-right'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
changeNodeName(e) {
const value = e.target.value
if (value !== this.title) {
const ci = this.treeKey
.split('@^@')
.slice(-1)[0]
.split('%')
const unique = Object.keys(JSON.parse(ci[2]))[0]
const ciId = Number(ci[0])
let editAttrName = unique
if (this.showName) {
editAttrName = this.showName
}
updateCI(ciId, { [editAttrName]: value }).then((res) => {
this.$message.success(this.$t('updateSuccess'))
this.$emit('updateTreeData', ciId, value)
})
}
this.isEditNodeName = false
this.editNodeName = ''
},
},
}
</script>
<style lang="less" scoped>
.relation-views-node {
width: 100%;
display: inline-flex;
justify-content: space-between;
align-items: center;
.relation-views-node-switch {
display: inline-block;
width: 15px;
color: @text-color_5;
i {
opacity: 0;
font-size: 10px;
}
}
.relation-views-node-content {
display: flex;
overflow: hidden;
align-items: center;
width: 100%;
.relation-views-node-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #d3d3d3;
color: #fff;
text-align: center;
line-height: 16px;
font-size: 12px;
}
.relation-views-node-title {
padding-left: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1;
color: @text-color_1;
}
.relation-views-node-number {
color: @text-color_4;
font-size: 12px;
margin: 0 5px;
}
.relation-views-node-operation {
opacity: 0;
width: 15px;
}
}
}
.relation-views-node-checkbox {
> span {
.relation-views-node-checkbox {
margin-right: 10px;
}
.relation-views-node-title {
width: calc(100% - 42px);
}
}
}
.relation-views-left .ant-tree:hover {
.relation-views-node .relation-views-node-switch i {
opacity: 1;
}
}
</style>
<style lang="less">
.relation-views-node-title-highlight {
color: @func-color_1;
}
.relation-views-left {
ul:has(.relation-views-node-checkbox) > li > ul {
margin-left: 26px;
}
ul:has(.relation-views-node-checkbox) {
margin-left: 0 !important;
}
.ant-tree-node-content-wrapper:hover {
.relation-views-node-operation {
opacity: 1;
}
}
.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected,
.ant-tree li .ant-tree-node-content-wrapper:hover {
background-color: @primary-color_3;
}
}
.relation-views-node-dropdown {
.ant-divider {
margin: 0;
.ant-divider-inner-text {
font-size: 12px;
color: @text-color_3;
}
}
.ant-dropdown-menu-item {
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>
<template>
<div
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span class="relation-views-node-switch">
<a-icon v-if="!isLeaf" :type="switchIcon"></a-icon>
</span>
<span class="relation-views-node-content">
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
v-if="icon.includes('$$') && icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else-if="icon.includes('$$') && icon.split('$$')[0]"
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
<span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
</template>
<span
class="relation-views-node-title"
v-if="!isEditNodeName"
:title="title"
v-highlight="{ value: fullSearchValue, class: 'relation-views-node-title-highlight' }"
>{{ title }}
</span>
<a-input
ref="input"
@blur="changeNodeName"
@pressEnter="
() => {
$refs.input.blur()
}
"
size="small"
v-else
v-model="editNodeName"
:style="{ marginLeft: '5px' }"
/>
<span class="relation-views-node-number">{{ number }}</span>
<a-dropdown overlayClassName="relation-views-node-dropdown" :overlayStyle="{ width: '200px' }">
<a-menu slot="overlay" @click="({ key: menuKey }) => onContextMenuClick(this.treeKey, menuKey)">
<template v-if="showBatchLevel === null">
<a-divider orientation="left">{{ $t('cmdb.relation') }}</a-divider>
<a-menu-item
v-for="item in menuList"
:key="item.id"
><a-icon type="plus-circle" />{{ $t('add') }} {{ item.alias }}</a-menu-item
>
<a-menu-item
v-if="showDelete"
key="delete"
><ops-icon type="icon-xianxing-delete" />{{
$t('cmdb.serviceTree.deleteNode', { name: title })
}}</a-menu-item
>
<a-divider orientation="left">{{ $t('cmdb.components.perm') }}</a-divider>
<a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
<a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
<a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
<a-menu-divider />
<a-menu-item
key="editNodeName"
><ops-icon type="icon-xianxing-edit" />{{ $t('cmdb.serviceTree.editNodeName') }}</a-menu-item
>
<a-menu-item key="batch"><ops-icon type="veops-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item>
</template>
<template v-else>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchGrant"
><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchRevoke"
><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<a-menu-divider />
<template v-if="showBatchLevel > 0">
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchDelete"
><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.remove') }}</a-menu-item
>
<a-menu-divider />
</template>
<a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
</template>
</a-menu>
<a-icon class="relation-views-node-operation" type="ellipsis" />
</a-dropdown>
</span>
</div>
</template>
<script>
import { updateCI } from '../../../api/ci.js'
import highlight from '@/directive/highlight'
export default {
name: 'ContextMenu',
directives: {
highlight,
},
props: {
treeNodeData: {
type: Object,
default: () => {},
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
fullSearchValue: {
type: String,
default: '',
},
},
data() {
return {
switchIcon: 'caret-right',
isEditNodeName: false,
editNodeName: '',
}
},
computed: {
childLength() {
return this.number
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews?.node2show_types?.[this._tempTree?.[1]]?.map?.((item) => {
return { id: item.id, alias: item.alias || item.name }
}) || []
}
return _menuList
},
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
title() {
return this.treeNodeData.title
},
number() {
return this.treeNodeData.number
},
treeKey() {
return this.treeNodeData.key
},
isLeaf() {
return this.treeNodeData.isLeaf
},
showName() {
return this.treeNodeData.showName
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
if (menuKey === 'editNodeName') {
this.isEditNodeName = true
this.editNodeName = this.title
this.$nextTick(() => {
this.$refs.input.focus()
})
return
}
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'caret-right' ? 'caret-down' : 'caret-right'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
changeNodeName(e) {
const value = e.target.value
if (value !== this.title) {
const ci = this.treeKey
.split('@^@')
.slice(-1)[0]
.split('%')
const unique = Object.keys(JSON.parse(ci[2]))[0]
const ciId = Number(ci[0])
let editAttrName = unique
if (this.showName) {
editAttrName = this.showName
}
updateCI(ciId, { [editAttrName]: value }).then((res) => {
this.$message.success(this.$t('updateSuccess'))
this.$emit('updateTreeData', ciId, value)
})
}
this.isEditNodeName = false
this.editNodeName = ''
},
},
}
</script>
<style lang="less" scoped>
.relation-views-node {
width: 100%;
display: inline-flex;
justify-content: space-between;
align-items: center;
.relation-views-node-switch {
display: inline-block;
width: 15px;
color: @text-color_5;
i {
opacity: 0;
font-size: 10px;
}
}
.relation-views-node-content {
display: flex;
overflow: hidden;
align-items: center;
width: 100%;
.relation-views-node-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #d3d3d3;
color: #fff;
text-align: center;
line-height: 16px;
font-size: 12px;
}
.relation-views-node-title {
padding-left: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1;
color: @text-color_1;
}
.relation-views-node-number {
color: @text-color_4;
font-size: 12px;
margin: 0 5px;
}
.relation-views-node-operation {
opacity: 0;
width: 15px;
}
}
}
.relation-views-node-checkbox {
> span {
.relation-views-node-checkbox {
margin-right: 10px;
}
.relation-views-node-title {
width: calc(100% - 42px);
}
}
}
.relation-views-left .ant-tree:hover {
.relation-views-node .relation-views-node-switch i {
opacity: 1;
}
}
</style>
<style lang="less">
.relation-views-node-title-highlight {
color: @func-color_1;
}
.relation-views-left {
ul:has(.relation-views-node-checkbox) > li > ul {
margin-left: 26px;
}
ul:has(.relation-views-node-checkbox) {
margin-left: 0 !important;
}
.ant-tree-node-content-wrapper:hover {
.relation-views-node-operation {
opacity: 1;
}
}
.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected,
.ant-tree li .ant-tree-node-content-wrapper:hover {
background-color: @primary-color_3;
}
}
.relation-views-node-dropdown {
.ant-divider {
margin: 0;
.ant-divider-inner-text {
font-size: 12px;
color: @text-color_3;
}
}
.ant-dropdown-menu-item {
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>