Files
cmdb/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue
2024-09-09 10:44:58 +08:00

1117 lines
37 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div :style="{ marginBottom: '-24px' }">
<div v-if="!subscribeTreeViewCiTypesLoading && subscribeTreeViewCiTypes.length === 0">
<a-alert :message="$t('cmdb.tree.tips1')" banner></a-alert>
</div>
<div class="tree-views" v-else>
<SplitPane
:min="200"
:max="500"
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-tree-views"
:triggerLength="18"
calcBasedParent
>
<template #one>
<div class="tree-views-left" :style="{ height: `${windowHeight - 64}px` }">
<draggable
v-model="subscribeTreeViewCiTypes"
:animation="300"
@change="
(e) => {
orderChange(e, subscribeTreeViewCiTypes)
}
"
>
<div v-for="ciType in subscribeTreeViewCiTypes" :key="ciType.type_id">
<div
@click="handleChangeCi(ciType.type_id)"
:class="{
'custom-header': true,
'custom-header-selected': Number(ciType.type_id) === Number(typeId) && !treeKeys.length,
}"
>
<OpsMoveIcon class="move-icon" />
<span class="tree-views-left-header-icon">
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</span>
<span class="tree-views-left-header-name">{{ ciType.alias || ciType.name }}</span>
<div class="actions">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<div class="action" @click="(e) => cancelSubscribe(e, ciType)">
<a-icon type="star" />
</div>
</a-tooltip>
<a-tooltip :title="$t('cmdb.tree.subSettings')">
<div class="action" @click="(e) => subscribeSetting(e, ciType)">
<a-icon type="setting" />
</div>
</a-tooltip>
</div>
</div>
<a-tree
:selectedKeys="selectedKeys"
:tree-data="treeData"
:load-data="onLoadData"
:expandedKeys="expandedKeys"
v-if="Number(ciType.type_id) === Number(typeId)"
>
<template #title="{ key: treeKey, title, isLeaf, childLength}">
<TreeViewsNode
:title="title"
:treeKey="treeKey"
:levels="levels"
:childLength="childLength"
:isLeaf="isLeaf"
@onNodeClick="onNodeClick"
/>
</template>
</a-tree>
</div>
</draggable>
</div>
</template>
<template #two>
<div class="tree-views-right" id="tree-views-right" :style="{ height: `${windowHeight - 64}px` }">
<div class="cmdb-views-header">
<span>
<span class="cmdb-views-header-title">{{ currentCiTypeName }}</span>
<span
@click="
() => {
$refs.metadataDrawer.open(typeId)
}
"
class="cmdb-views-header-metadata"
><a-icon type="info-circle" />
{{ $t('cmdb.ci.attributeDesc') }}
</span>
</span>
<a-space>
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="$refs.create.handleOpen(true, 'create')"
><ops-icon type="veops-increase" />
{{ $t('create') }}
</a-button>
<EditAttrsPopover :typeId="Number(typeId)" class="operation-icon" @refresh="refreshAfterEditAttrs">
<a-button
type="primary"
ghost
class="ops-button-ghost"
><ops-icon type="veops-configuration_table" />{{ $t('cmdb.configTable') }}</a-button
>
</EditAttrsPopover>
</a-space>
</div>
<SearchForm
ref="search"
@refresh="reloadData"
:preferenceAttrList="currentAttrList"
:typeId="Number(typeId)"
@copyExpression="copyExpression"
>
<PreferenceSearch
v-show="!selectedRowKeys.length"
ref="preferenceSearch"
@getQAndSort="getQAndSort"
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
/>
<div class="ops-list-batch-action">
<template v-if="selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</template>
</div>
</SearchForm>
<CITable
ref="xTable"
:id="`cmdb-tree-${typeId}`"
:loading="loading"
:attrList="currentAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
:height="`${windowHeight - 240}px`"
:loadingTip="loadTip"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
:current="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ alignSelf: 'flex-end' }"
@showSizeChange="onShowSizeChange"
@change="
(page) => {
currentPage = page
handleLoadInstance({ sortByTable })
}
"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
</SplitPane>
</div>
<SubscribeSetting
ref="subscribeSetting"
@reload="
() => {
reload()
}
"
/>
<CiDetailDrawer ref="detail" :typeId="Number(typeId)" :treeViewsLevels="treeViewsLevels" />
<create-instance-form
ref="create"
:typeIdFromRelation="Number(typeId)"
@reload="sumbitFromCreateInstance"
@submit="batchUpdateFromCreateInstance"
/>
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<MetadataDrawer ref="metadataDrawer" />
</div>
</template>
<script>
/* eslint-disable no-useless-escape */
import _ from 'lodash'
import Sortable from 'sortablejs'
import draggable from 'vuedraggable'
import {
getSubscribeTreeView,
getSubscribeAttributes,
subscribeTreeView,
preferenceCitypeOrder,
} from '@/modules/cmdb/api/preference'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITableColumns } from '../../utils/helper'
import SearchForm from '../../components/searchForm/SearchForm.vue'
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
import SplitPane from '@/components/SplitPane'
import TreeViewsNode from './modules/treeViewsNode.vue'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetailDrawer from '../ci/modules/ciDetailDrawer.vue'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from '../ci/modules/MetadataDrawer.vue'
import { intersection } from '@/utils/functions/set'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'TreeViews',
components: {
SearchForm,
SubscribeSetting,
SplitPane,
TreeViewsNode,
EditAttrsPopover,
CiDetailDrawer,
CreateInstanceForm,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
OpsMoveIcon,
draggable,
CITable
},
data() {
return {
keySplit: '---',
treeData: [],
treeNode: null,
treeKeys: [],
subscribeTreeViewCiTypes: [],
subscribeTreeViewCiTypesLoading: false,
levels: [],
typeId: null,
instanceList: [],
columns: [],
loading: false,
loadTip: '',
pageSizeOptions: ['50', '100', '200', '100000'],
pageSize: 50,
currentPage: 1,
totalNumber: 0,
currentAttrList: [],
trigger: false,
newLoad: true,
formatSearchFormData: '',
sortByTable: undefined,
paneLengthPixel: 205,
expandedKeys: [],
attrList: [],
attributes: {},
selectedRowKeys: [],
// 对照是否编辑
initialInstanceList: [],
citypes: [],
// 表格拖拽的参数
tableDragClassName: [],
// 已经设置过data的node
isSetDataNodes: [],
initialPasswordValue: {},
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
selectedKeys() {
if (this.treeKeys.length <= 1) {
return this.treeKeys.map((item) => `${this.keySplit}${item}`)
}
return [this.treeKeys.join(this.keySplit)]
},
treeViewsLevels() {
// 当前订阅的树型视图的字段
const _find = this.subscribeTreeViewCiTypes.find((item) => item.type_id === Number(this.typeId))
return _find?.levels || []
},
treeViewId() {
// 当前页面的id
const _find = this.subscribeTreeViewCiTypes.find((item) => item.type_id === Number(this.typeId))
return _find?.id
},
currentCiTypeName() {
const _find = this.citypes.find((item) => Number(item.id) === Number(this.typeId))
return _find?.alias || _find?.name || ''
},
},
watch: {
'$route.path': function(newPath, oldPath) {
this.newLoad = true
this.typeId = this.$route.params.typeId
this.initPage()
},
},
provide() {
return {
handleSearch: this.handleLoadInstance,
setPreferenceSearchCurrent: this.setPreferenceSearchCurrent,
attrList: () => {
return this.attrList
},
attributes: () => {
return this.attributes
},
filterCompPreferenceSearch: () => {
return { ptv_id: this.treeViewId }
},
}
},
inject: ['reload'],
async created() {
await this.getTreeViews()
},
mounted() {
setTimeout(() => {
this.columnDrop()
}, 1000)
getCITypes().then((res) => {
this.citypes = res.ci_types
})
},
beforeDestroy() {
if (this.sortable) {
this.sortable.destroy()
}
},
methods: {
async getAttributeList() {
const res = await getCITypeAttributesById(Number(this.typeId))
this.attrList = res.attributes
this.attributes = res
},
async getTreeViews() {
this.subscribeTreeViewCiTypesLoading = true
const res = await getSubscribeTreeView()
this.subscribeTreeViewCiTypesLoading = false
this.subscribeTreeViewCiTypes = res
if (this.subscribeTreeViewCiTypes.length) {
this.typeId = this.$route.params.typeId || this.subscribeTreeViewCiTypes[0].type_id
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.levels = res.find((item) => item.type_id.toString() === this.typeId.toString()).levels
await this.initPage()
}
},
async initPage() {
this.treeNode = null
this.treeKeys = []
this.levels = []
this.currentPage = 1
this.totalNumber = 0
this.instanceList = []
this.selectedRowKeys = []
this.expandedKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
await this.loadCurrentView()
await this.getAttributeList()
await this.loadAttrList()
await this.handleLoadInstance()
},
async loadCurrentView() {
if (this.subscribeTreeViewCiTypes.length) {
this.typeId = this.$route.params.typeId || this.subscribeTreeViewCiTypes[0].type_id
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.levels = this.subscribeTreeViewCiTypes.find(
(item) => item.type_id.toString() === this.typeId.toString()
).levels
}
},
async loadAttrList() {
const res = await getSubscribeAttributes(this.typeId)
this.currentAttrList = res.attributes
},
async handleLoadInstance(params = {}) {
this.trigger = true
this.loading = true
let q = `_type:${this.typeId}`
if (this.treeKeys.length > 0) {
// 再增加垂直分类信息
this.treeKeys.forEach((item, idx) => {
q += `,${this.levels[idx].name}:${item}`
})
}
if (this.formatSearchFormData) {
q = `${q}${this.formatSearchFormData}`
}
const expression = this.$refs['search'] ? this.$refs['search'].expression || '' : ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
if (exp) {
q = `${q},${exp}`
}
const fuzzySearch = this.$refs['search'].fuzzySearch
if (fuzzySearch) {
q = `${q},*${fuzzySearch}*`
}
const payload = { q }
// 如果是表格点击的排序 以表格为准
let sort
const { sortByTable } = params
if (sortByTable) {
sort = sortByTable
} else {
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
}
payload.sort = sort
if (this.levels.length > this.treeKeys.length) {
// 增加切面信息
payload['facet'] = `${this.levels[this.treeKeys.length].name}`
}
payload['page'] = this.currentPage
payload['count'] = this.pageSize
try {
const res = await searchCI(payload)
this.totalNumber = res.numfound
if (Object.values(res.facet).length) {
this.wrapTreeData(res.facet)
}
const jsonAttrList = this.currentAttrList.filter((attr) => attr.value_type === '6')
this.instanceList = res['result'].map((item) => {
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
return { ..._.cloneDeep(item) }
})
this.initialInstanceList = _.cloneDeep(this.instanceList)
const treeViewsRight = document.getElementById('tree-views-right')
if (treeViewsRight) {
const width = treeViewsRight.clientWidth - 50
this.columns = getCITableColumns(res.result, this.currentAttrList, width)
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
}
} catch (e) {
console.log(e)
this.$message.error(e)
} finally {
this.loading = false
this.$nextTick(() => {
this.trigger = false
if (this.$refs.xTable) {
this.$refs.xTable.getVxetableRef().refreshColumn()
}
})
}
this.newLoad = false
},
wrapTreeData(facet) {
// 切面
const _treeData = Object.values(facet)[0].map((item) => {
let title = item[0]
const attr = this.attrList.find((attr) => attr.name === item[2])
if (attr?.choice_value?.length) {
const choice = attr.choice_value.find((choice) => item[0] === choice?.[0])
if (choice?.[1]?.label) {
title = choice[1].label
}
}
return {
title: title,
childLength: item[1],
key: this.treeKeys.join(this.keySplit) + this.keySplit + item[0],
isLeaf: this.levels.length - 1 === this.treeKeys.length,
}
})
if (this.treeNode === null && this.newLoad) {
this.treeData = _treeData
this.treeNode = { dataRef: {} }
} else {
if (!this.isSetDataNodes.includes(this.treeNode.dataRef.key)) {
this.treeNode.dataRef.children = _treeData
this.treeData = [...this.treeData]
this.isSetDataNodes.push(this.treeNode.dataRef.key)
}
}
},
onLoadData(treeNode) {
this.triggerSelect = false
return new Promise((resolve) => {
if (treeNode.dataRef.children) {
resolve()
return
}
this.treeKeys = treeNode.eventKey.split(this.keySplit).filter((item) => item !== '')
this.treeNode = treeNode
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
resolve()
})
},
handleChangeCi(value) {
if (value && Number(this.typeId) !== Number(value)) {
this.treeData = []
this.$router.history.push({
name: 'cmdb_tree_views_item',
params: { typeId: Number(value) },
})
this.typeId = Number(value)
} else {
this.typeId = null
this.$nextTick(() => {
this.typeId = Number(value)
this.newLoad = true
this.initPage()
})
}
this.isSetDataNodes = []
},
async reloadData() {
const queryParams = this.$refs['search'].queryParam || {}
this.formatSearchFormData = this.mergeQ(queryParams)
this.currentPage = 1
this.sortByTable = undefined
const xTable = this.$refs.xTable.getVxetableRef()
xTable.clearSort().then(() => {
this.handleLoadInstance()
})
},
mergeQ(params) {
let q = ''
Object.keys(params).forEach((key) => {
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
if (typeof params[key] === 'object' && params[key] && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:*${params[key]}*`
}
}
})
return q
},
cancelSubscribe(e, ciType) {
e.stopPropagation()
e.preventDefault()
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => (
<div>{that.$t('cmdb.preference.confirmcancelSub2', { name: ciType.alias || ciType.name })}</div>
),
onOk() {
subscribeTreeView(ciType.type_id, []).then(() => {
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
if (Number(that.$route.params.typeId) === Number(ciType.type_id)) {
that.$router.history.push('/cmdb/treeviews')
that.reload()
} else {
that.reload()
}
})
},
})
},
subscribeSetting(e, ciType) {
e.stopPropagation()
e.preventDefault()
this.$refs.subscribeSetting.open(ciType)
},
columnDrop() {
this.$nextTick(() => {
const xTable = this.$refs.xTable.getVxetableRef()
this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{
handle: '.vxe-handle',
onChoose: () => {
const header = xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row')
const classNameList = []
header.childNodes.forEach((item) => {
classNameList.push(item.classList[1])
})
this.tableDragClassName = classNameList
},
onEnd: (params) => {
// 由于开启了虚拟滚动newIndex和oldIndex是虚拟的
const { newIndex, oldIndex } = params
// 从tableDragClassName拿到colid
const fromColid = this.tableDragClassName[oldIndex]
const toColid = this.tableDragClassName[newIndex]
const fromColumn = xTable.getColumnById(fromColid)
const toColumn = xTable.getColumnById(toColid)
const fromIndex = xTable.getColumnIndex(fromColumn)
const toIndex = xTable.getColumnIndex(toColumn)
const tableColumn = xTable.getColumns()
const currRow = tableColumn.splice(fromIndex, 1)[0]
tableColumn.splice(toIndex, 0, currRow)
xTable.loadColumn(tableColumn)
},
}
)
})
},
handleSortCol({ column, property, order, sortBy, sortList, $event }) {
let sortByTable
if (order === 'asc') {
sortByTable = property
} else if (order === 'desc') {
sortByTable = `-${property}`
}
this.sortByTable = sortByTable
this.currentPage = 1
this.handleLoadInstance({ sortByTable })
},
onNodeClick(keys, type) {
console.log(keys)
if (keys) {
const _tempKeys = keys.split(this.keySplit).filter((item) => item !== '')
if (_tempKeys.length === this.levels.length) {
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.selectedRowKeys = []
}
this.treeKeys = _tempKeys
}
const idx = this.expandedKeys.findIndex((item) => item === keys)
if (idx > -1) {
this.expandedKeys.splice(idx, 1)
} else {
this.expandedKeys.push(keys)
}
this.handleLoadInstance()
},
async refreshAfterEditAttrs() {
await this.loadAttrList()
await this.handleLoadInstance()
},
deleteCI(record) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
deleteCI(record.ci_id || record._id).then((res) => {
that.$message.success(that.$t('deleteSuccess'))
that.reload()
})
},
})
},
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
setSelectRows() {
const cached = new Set(this.selectedRowKeys)
const loaded = new Set(this.instanceList.map((i) => i.ci_id || i._id))
const inter = Array.from(intersection(cached, loaded))
if (inter.length === this.instanceList.length) {
this.$refs['xTable'].getVxetableRef().setAllCheckboxRow(true)
} else {
const rows = []
inter.forEach((rid) => {
rows.push(this.$refs['xTable'].getVxetableRef().getRowById(rid))
})
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
}
},
handleEditActived() {
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
const { row, column } = editRecord
if (passwordCol.length && this.lastEditCiId !== row._id) {
this.$nextTick(async () => {
for (let i = 0; i < passwordCol.length; i++) {
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
this.initialPasswordValue[passwordCol[i].field] = res.value
this.passwordValue[passwordCol[i].field] = res.value
})
}
this.isContinueCloseEdit = false
await this.$refs.xTable.getVxetableRef().clearEdit()
this.isContinueCloseEdit = true
this.$nextTick(() => {
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
})
})
}
this.lastEditCiId = row._id
})
},
handleEditClose({ row, rowIndex, column }) {
if (!this.isContinueCloseEdit) {
return
}
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (
!(item.field in this.initialPasswordValue) &&
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
) {
data[item.field] = row[item.field] ?? null
}
})
Object.keys(this.initialPasswordValue).forEach((key) => {
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
data[key] = this.passwordValue[key]
row[key] = this.passwordValue[key]
}
})
this.lastEditCiId = null
if (JSON.stringify(data) !== '{}') {
updateCI(row._id, data)
.then(() => {
this.$message.success(this.$t('saveSuccess'))
const arr1 = this.treeViewsLevels.map((item) => item.name)
const arr2 = Object.keys(data)
const arr3 = arr1.filter((item) => {
return arr2.includes(item)
})
if (arr3.length) {
this.reload()
return
}
$table.reloadRow(row, null)
const _initialInstanceList = _.cloneDeep(this.initialInstanceList)
_initialInstanceList[rowIndex] = {
..._initialInstanceList[rowIndex],
...data,
}
this.initialInstanceList = _initialInstanceList
})
.catch((err) => {
console.log(err)
$table.revertData(row)
})
}
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.currentAttrList.filter((attr) => !attr?.is_reference), ciTypeName: this.currentCiTypeName })
},
batchDownload({ filename, type, checkedKeys }) {
console.log(filename, type)
const jsonAttrList = []
checkedKeys.forEach((key) => {
const _find = this.currentAttrList.find((attr) => attr.name === key)
if (_find && _find.value_type === '6') jsonAttrList.push(key)
})
const data = _.cloneDeep([
...this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords(),
...this.$refs.xTable.getVxetableRef().getCheckboxRecords(true),
])
this.$refs.xTable.getVxetableRef().exportData({
filename,
type,
columnFilterMethod({ column }) {
return checkedKeys.includes(column.property)
},
data: [
...data.map((item) => {
jsonAttrList.forEach((jsonAttr) => (item[jsonAttr] = item[jsonAttr] ? JSON.stringify(item[jsonAttr]) : ''))
return { ...item }
}),
],
})
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
batchDelete() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
that.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchDeleting')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.reload()
},
sumbitFromCreateInstance({ ci_id }) {
this.reload()
},
batchUpdateFromCreateInstance(values) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.ci.batchUpdateConfirm'),
onOk() {
that.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress')
const payload = {}
Object.keys(values).forEach((key) => {
if (values[key] || values[key] === 0) {
payload[key] = values[key]
}
// 字段值支持置空
// 目前存在字段值不支持置空由后端返回
if (values[key] === undefined || values[key] === null) {
payload[key] = null
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces' },
})
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
const arr1 = this.treeViewsLevels.map((item) => item.name)
const arr2 = Object.keys(values)
const arr3 = arr1.filter((item) => {
return arr2.includes(item)
})
if (arr3.length) {
this.reload()
return
}
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.handleLoadInstance()
},
onShowSizeChange(current, pageSize) {
this.pageSize = pageSize
this.currentPage = 1
this.handleLoadInstance()
},
getQAndSort() {
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
const expression = this.$refs['search'].expression || ''
this.$refs.preferenceSearch.savePreference({ fuzzySearch, expression })
},
setParamsFromPreferenceSearch(item) {
const { fuzzySearch, expression } = item.option
this.$refs.search.fuzzySearch = fuzzySearch
this.$refs.search.expression = expression
this.currentPage = 1
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.handleLoadInstance()
})
},
setPreferenceSearchCurrent(id = null) {
this.$refs.preferenceSearch.currentPreferenceSearch = id
},
copyExpression() {
const expression = this.$refs['search'].expression || ''
const fuzzySearch = this.$refs['search'].fuzzySearch
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const text = `q=_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
orderChange(e, subscribeTreeViewCiTypes) {
preferenceCitypeOrder({ type_ids: subscribeTreeViewCiTypes.map((type) => type.type_id), is_tree: true }).catch(
() => {
this.getTreeViews()
}
)
},
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
</script>
<style lang="less">
@import '../index.less';
.tree-views {
width: 100%;
height: calc(100% - 32px);
.tree-views-left {
float: left;
position: relative;
overflow: hidden;
width: 100%;
&:hover {
overflow: auto;
}
.custom-header {
width: 100%;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
padding: 8px 0 8px 12px;
cursor: move;
border-radius: 2px;
position: relative;
&:hover {
background-color: @primary-color_3;
> .actions,
> .move-icon {
display: inherit;
}
}
.move-icon {
width: 14px;
height: 20px;
cursor: move;
position: absolute;
display: none;
left: 0;
}
.tree-views-left-header-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 2px;
margin-right: 6px;
}
.tree-views-left-header-name {
flex: 1;
font-weight: bold;
margin-left: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: @text-color_1;
}
.actions {
display: none;
margin-left: auto;
cursor: pointer;
}
.action {
display: inline-block;
width: 22px;
text-align: center;
border-radius: 5px;
&:hover {
background-color: #cacaca;
}
}
}
.custom-header-selected {
background-color: @primary-color_3 !important;
}
.ant-tree li {
padding: 2px 0;
}
.ant-tree-switcher {
display: none;
}
.ant-tree-node-content-wrapper {
width: 100%;
padding: 4px 0;
display: inline-block;
height: 100%;
.ant-tree-title {
display: inline-block;
width: 100%;
padding: 0 6px;
}
}
.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected {
background-color: @primary-color_3;
}
}
.tree-views-right {
background-color: #fff;
display: flex;
flex-direction: column;
padding: 20px;
overflow: auto;
width: 100%;
border-radius: @border-radius-box;
}
}
</style>