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;">
<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,14 +917,26 @@ 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]
let viewId = parseInt(localStorage.getItem(relationViewKeyStorage)) || parseInt(this.$route.params.viewId) || this.relationViews.name2id[0][1]
let viewName = null
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 = []
@ -895,8 +955,6 @@ export default {
this.$nextTick(() => {
this.refreshTable()
})
}
})
},
async loadColumns() {
@ -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 {
background-color: #FFFFFF;
border: solid 1px transparent;
&:hover, &:focus {
border-color: @primary-color;
}
.ant-input:focus {
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>

View File

@ -187,9 +187,9 @@ export default {
]
} 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 _menuList
},