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

View File

@ -54,6 +54,54 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<span class="icon iconfont">&#xea02;</span> <span class="icon iconfont">&#xea02;</span>
<div class="name">veops-rear</div> <div class="name">veops-rear</div>
@ -6162,9 +6210,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1732673294759') format('woff2'), src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'), url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype'); url('iconfont.ttf?t=1735191938771') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -6190,6 +6238,78 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<span class="icon iconfont veops-rear"></span> <span class="icon iconfont veops-rear"></span>
<div class="name"> <div class="name">
@ -15352,6 +15472,70 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rear"></use> <use xlink:href="#veops-rear"></use>

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1732673294759') format('woff2'), src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'), url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype'); url('iconfont.ttf?t=1735191938771') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,38 @@
-moz-osx-font-smoothing: grayscale; -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 { .veops-rear:before {
content: "\ea02"; content: "\ea02";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,62 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "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", "icon_id": "42510712",
"name": "veops-rear", "name": "veops-rear",

Binary file not shown.

View File

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

View File

@ -88,6 +88,7 @@ import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue' import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue' import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType' import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default { export default {
name: 'GrantComp', name: 'GrantComp',
@ -186,6 +187,13 @@ export default {
}, },
getFilterPermissions() { getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => { 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 this.filerPerimissions = res
}) })
}, },

View File

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

View File

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

View File

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

View File

@ -11,7 +11,48 @@
> >
<template #one> <template #one>
<div class="relation-views-left" :style="{ height: `${windowHeight - 64}px` }"> <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 <a-input
:placeholder="$t('cmdb.serviceTree.searchTips')" :placeholder="$t('cmdb.serviceTree.searchTips')"
class="relation-views-left-input" class="relation-views-left-input"
@ -214,7 +255,7 @@
</SplitPane> </SplitPane>
</div> </div>
<a-alert <a-alert
:message="$t('cmdb.serviceTreealert1')" :message="$t('noData')"
banner banner
v-else-if="relationViews.name2id && !relationViews.name2id.length" v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert> ></a-alert>
@ -271,6 +312,8 @@ import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue' import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
import CITable from '@/modules/cmdb/components/ciTable/index.vue' import CITable from '@/modules/cmdb/components/ciTable/index.vue'
const relationViewKeyStorage = 'cmdb_relation_view_menu_key'
export default { export default {
name: 'RelationViews', name: 'RelationViews',
components: { components: {
@ -370,7 +413,7 @@ export default {
return !!this.selectedRowKeys.length return !!this.selectedRowKeys.length
}, },
topo_flatten() { topo_flatten() {
return this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? [] return this.relationViews?.views[this.viewName]?.topo_flatten ?? []
}, },
descendant_ids() { descendant_ids() {
return this.topo_flatten.slice(this.treeKeys.length).join(',') return this.topo_flatten.slice(this.treeKeys.length).join(',')
@ -393,6 +436,15 @@ export default {
leaf_tree_sort() { leaf_tree_sort() {
return this.viewOption?.sort ?? 1 return this.viewOption?.sort ?? 1
}, },
relationViewMenu() {
const name2id = this?.relationViews?.name2id || []
return name2id.map((item) => {
return {
id: item?.[1] || -1,
name: item?.[0] || ''
}
})
}
}, },
provide() { provide() {
return { return {
@ -429,10 +481,6 @@ export default {
}, },
inject: ['reload'], inject: ['reload'],
watch: { watch: {
'$route.path': function(newPath, oldPath) {
this.viewId = this.$route.params.viewId
this.reload()
},
pageNo: function(newPage, oldPage) { pageNo: function(newPage, oldPage) {
this.loadData({ parameter: { pageNo: newPage }, refreshType: undefined, sortByTable: this.sortByTable }) this.loadData({ parameter: { pageNo: newPage }, refreshType: undefined, sortByTable: this.sortByTable })
}, },
@ -869,36 +917,46 @@ export default {
this.relationViews = res this.relationViews = res
} }
if ((Object.keys(this.relationViews.views) || []).length) { if ((Object.keys(this.relationViews.views) || []).length) {
this.viewId = let viewId = parseInt(localStorage.getItem(relationViewKeyStorage)) || parseInt(this.$route.params.viewId) || this.relationViews.name2id[0][1]
parseInt(this.$route.path.split('/')[this.$route.path.split('/').length - 1]) || let viewName = null
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 ?? {}
this.$nextTick(() => { const currentView = this.relationViews.name2id.find((item) => item?.[1] === viewId)
this.refreshTable() 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() { async loadColumns() {
if (this.currentTypeId[0]) { if (this.currentTypeId[0]) {
this.getAttributeList() this.getAttributeList()
@ -954,7 +1012,7 @@ export default {
const that = this const that = this
this.$confirm({ this.$confirm({
title: that.$t('warning'), 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() { onOk() {
deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => { deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {
that.$message.success(that.$t('deleteSuccess')) that.$message.success(that.$t('deleteSuccess'))
@ -1063,18 +1121,20 @@ export default {
}) })
} }
}, },
handlePerm() { handlePerm(resourceName) {
const _resource_name = resourceName ?? this.viewName
roleHasPermissionToGrant({ roleHasPermissionToGrant({
app_id: 'cmdb', app_id: 'cmdb',
resource_type_name: 'RelationView', resource_type_name: 'RelationView',
perm: 'grant', perm: 'grant',
resource_name: this.$route.meta.title, resource_name: _resource_name,
}).then((res) => { }).then((res) => {
if (res.result) { if (res.result) {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => { searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms } this.resource_type = { groups: res.groups, id2perms: res.id2perms }
this.$nextTick(() => { 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 { } else {
@ -1100,7 +1160,7 @@ export default {
}, },
columnDrop() { columnDrop() {
this.$nextTick(() => { this.$nextTick(() => {
const xTable = this.$refs.xTable.getVxetableRef() const xTable = this.$refs?.xTable?.getVxetableRef?.()
this.sortable = Sortable.create( this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'), xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{ {
@ -1282,7 +1342,7 @@ export default {
async openBatchDownload() { async openBatchDownload() {
this.$refs.batchDownload.open({ this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference), preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.$route.meta.name, ciTypeName: this.viewName,
}) })
}, },
batchDownload({ filename, type, checkedKeys }) { batchDownload({ filename, type, checkedKeys }) {
@ -1629,6 +1689,13 @@ export default {
openDetail(id, activeTabKey, ciDetailRelationKey) { openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(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; overflow: auto;
} }
.relation-views-left-header { .relation-views-left-header {
border-left: 4px solid @primary-color; display: flex;
height: 32px; align-items: center;
line-height: 32px; max-width: 100%;
padding-left: 12px;
margin-bottom: 12px;
color: @text-color_1;
font-weight: bold;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; padding-bottom: 12px;
white-space: nowrap; border-bottom: @border-color-base;
cursor: default; 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 { .ant-tree li {
padding: 2px 0; padding: 2px 0;
@ -1680,14 +1791,14 @@ export default {
} }
.relation-views-left-input { .relation-views-left-input {
margin-bottom: 12px; margin-bottom: 12px;
input {
background-color: transparent; .ant-input {
border-top: none; background-color: #FFFFFF;
border-right: none; border: solid 1px transparent;
border-left: none;
} &:hover, &:focus {
.ant-input:focus { border-color: @primary-color;
box-shadow: none; }
} }
} }
} }
@ -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> </style>

View File

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