cmdb/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue

1258 lines
43 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="请先到 我的订阅 页面完成订阅!" banner></a-alert>
</div>
<div class="tree-views" v-else>
<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" />
属性说明
</span>
</span>
<a-button
size="small"
icon="plus"
type="primary"
@click="$refs.create.handleOpen(true, 'create')"
>新建</a-button
>
</div>
<SplitPane
:min="200"
:max="500"
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-tree-views"
triggerColor="#F0F5FF"
:triggerLength="18"
>
<template #one>
<div class="tree-views-left" :style="{ height: `${windowHeight - 115}px` }">
<a-collapse
:activeKey="current"
accordion
@change="handleChangeCi"
:bordered="false"
:destroyInactivePanel="true"
>
<a-collapse-panel
v-for="ciType in subscribeTreeViewCiTypes"
:key="String(ciType.type_id)"
:showArrow="false"
:style="{
borderRadius: '4px',
marginBottom: '5px',
border: 0,
overflow: 'hidden',
width: '100%',
}"
>
<div
slot="header"
:class="{
'custom-header': true,
'custom-header-selected': Number(ciType.type_id) === Number(typeId),
}"
>
<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="取消订阅">
<div class="action" @click="(e) => cancelSubscribe(e, ciType)">
<a-icon type="star" />
</div>
</a-tooltip>
<a-tooltip title="订阅设置">
<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"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }">
<TreeViewsNode
:title="title"
:treeKey="treeKey"
:levels="levels"
:isLeaf="isLeaf"
@onNodeClick="onNodeClick"
/>
</template>
</a-tree>
</a-collapse-panel>
</a-collapse>
</div>
</template>
<template #two>
<div class="tree-views-right" id="tree-views-right" :style="{ height: `${windowHeight - 115}px` }">
<SearchForm
ref="search"
@refresh="reloadData"
:preferenceAttrList="currentAttrList"
:typeId="Number(typeId)"
@copyExpression="copyExpression"
/>
<div class="tree-views-right-bar">
<PreferenceSearch
ref="preferenceSearch"
@getQAndSort="getQAndSort"
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
/>
<div class="ops-list-batch-action">
<template v-if="selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">修改</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">下载</span>
<a-divider type="vertical" />
<span @click="batchDelete">删除</span>
<span>选取{{ selectedRowKeys.length }} </span>
</template>
</div>
</div>
<ops-table
:id="`cmdb-tree-${typeId}`"
border
ref="xTable"
size="small"
keep-source
:loading="loading"
:data="instanceList"
highlight-hover-row
show-overflow
show-header-overflow
row-id="_id"
resizable
:row-key="true"
:column-key="true"
:sort-config="{ remote: true, trigger: 'cell' }"
@sort-change="handleSortCol"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
:height="`${windowHeight - 252}px`"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
class="ops-unstripe-table"
:style="{ margin: '0 -12px' }"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 12px"
/>
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice" #edit="{ row }">
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
placeholder="请选择"
v-if="col.is_choice"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
>
<a-select-option
:value="choice[0]"
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<span
:style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
#default="{ row }"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:password="row[col.field]"
></PasswordField>
<template v-else-if="col.is_choice">
<template v-if="col.is_list">
<span
v-for="value in row[col.field]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}</span
>
</template>
<span
v-else-if="row[col.field]"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>{{ row[col.field] }}</span
>
</template>
</template>
</vxe-table-column>
<vxe-table-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>操作</span>
<EditAttrsPopover :typeId="Number(typeId)" class="operation-icon" @refresh="refreshAfterEditAttrs" />
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip title="添加关系">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<template>
<a-tooltip title="删除实例">
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-tooltip>
</template>
</a-space>
</template>
</vxe-table-column>
<template #empty>
<div v-if="loading" style="height: 200px; line-height: 200px">加载中...</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>暂无数据</div>
</div>
</template>
<template #loading>
<div style="height: 200px; line-height: 200px">{{ loadTip || '加载中...' }}</div>
</template>
</ops-table>
<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) => `当前${range[0]}-${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 }}/</span>
<span v-if="props.value === '100000'">全部</span>
</template>
</a-pagination>
</div>
</div>
</template>
</SplitPane>
</div>
<SubscribeSetting
ref="subscribeSetting"
@reload="
() => {
reload()
}
"
/>
<ci-detail ref="detail" :typeId="Number(typeId)" :treeViewsLevels="treeViewsLevels" />
<create-instance-form
ref="create"
:typeIdFromRelation="Number(typeId)"
@reload="sumbitFromCreateInstance"
@submit="batchUpdateFromCreateInstance"
/>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<MetadataDrawer ref="metadataDrawer" />
</div>
</template>
<script>
/* eslint-disable no-useless-escape */
import _ from 'lodash'
import Sortable from 'sortablejs'
import { getSubscribeTreeView, getSubscribeAttributes, subscribeTreeView } 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 PasswordField from '../../components/passwordField/index.vue'
import SplitPane from '@/components/SplitPane'
import TreeViewsNode from './modules/treeViewsNode.vue'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetail from '../ci/modules/CiDetail'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
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'
export default {
name: 'TreeViews',
components: {
SearchForm,
SubscribeSetting,
PasswordField,
SplitPane,
TreeViewsNode,
EditAttrsPopover,
CiDetail,
CreateInstanceForm,
JsonEditor,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
OpsMoveIcon,
},
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,
current: '', // 当前页面的type_id
currentAttrList: [],
trigger: false,
newLoad: true,
formatSearchFormData: '',
sortByTable: undefined,
paneLengthPixel: 205,
expandedKeys: [],
attrList: [],
attributes: {},
selectedRowKeys: [],
// 对照是否编辑
initialInstanceList: [],
citypes: [],
// 表格拖拽的参数
tableDragClassName: [],
// 已经设置过data的node
isSetDataNodes: [],
}
},
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
},
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
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() {
// const res = await getSubscribeTreeView()
// this.subscribeTreeViewCiTypes = res
// await this.initPage()
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() {
await getCITypeAttributesById(Number(this.typeId)).then((res) => {
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.current = `${this.typeId}`
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.current = String(this.typeId)
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)
}
} 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) {
// 切面
console.log('facet', facet)
const _treeData = Object.values(facet)[0].map((item) => {
return {
title: `${item[0]} (${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) },
})
} else {
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: '警告',
content: (h) => (
<div>
确认要取消订阅 <span style={{ fontWeight: 700, color: 'black' }}>{ciType.alias || ciType.name}</span>
</div>
),
onOk() {
subscribeTreeView(ciType.type_id, []).then(() => {
that.$message.success('取消订阅成功')
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 })
},
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.currentAttrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
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: '警告',
content: '确认删除?',
onOk() {
deleteCI(record.ci_id || record._id).then((res) => {
that.$message.success('删除成功!')
that.reload()
})
},
})
},
onSelectChange(e) {
/* const current = records.map((i) => i._id)
const cached = new Set(this.selectedRowKeys)
if (checked) {
current.forEach((i) => {
cached.add(i)
})
} else {
if (row) {
cached.delete(row._id)
} else {
this.instanceList.map((row) => {
cached.delete(row._id)
})
}
}
this.selectedRowKeys = Array.from(cached) */
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
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)
}
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
handleEditActived() {},
handleEditClose({ row, rowIndex, column }) {
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
data[item.field] = row[item.field] || null
}
})
if (JSON.stringify(data) !== '{}') {
updateCI(row._id, data)
.then(() => {
this.$message.success('保存成功!')
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)
})
}
},
jsonEditorOk(row, column, jsonData) {
// 后端写数据有快慢不拉接口直接修改table的数据
// this.reloadData()
this.instanceList.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.$refs.xTable.getVxetableRef().refreshColumn()
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.currentAttrList })
},
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: '警告',
content: '确认删除?',
onOk() {
that.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `正在删除...`
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await deleteCI(this.selectedRowKeys[i], false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = `正在删除${this.selectedRowKeys.length}成功${successNum}失败${errorNum}`
})
}
this.loading = false
this.loadTip = ''
this.reload()
},
sumbitFromCreateInstance({ ci_id }) {
this.reload()
},
batchUpdateFromCreateInstance(values) {
const that = this
this.$confirm({
title: '警告',
content: '确认要批量修改吗 ?',
onOk() {
that.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = `正在批量修改...`
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
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch(() => {
errorNum += 1
})
.finally(() => {
this.loadTip = `正在批量修改${this.selectedRowKeys.length}成功${successNum}失败${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('复制成功!')
})
.catch(() => {
this.$message.error('复制失败!')
})
},
},
}
</script>
<style lang="less">
@import '../index.less';
.tree-views {
width: 100%;
height: calc(100% - 32px);
.tree-views-left {
float: left;
position: relative;
background-color: #fff;
overflow: hidden;
width: 100%;
padding: 12px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
&:hover {
overflow: auto;
}
.ant-collapse-borderless {
background-color: #fff;
}
.ant-collapse-item:has(.custom-header-selected):not(:has(.ant-tree-treenode-selected)) > .ant-collapse-header,
.ant-collapse-item-active:not(:has(.ant-tree-treenode-selected)) > .ant-collapse-header {
background-color: #d6e4ff;
}
.ant-collapse-header {
padding: 8px 12px 4px;
&:hover {
background-color: #f0f5ff;
}
&:hover > .custom-header > .actions {
display: inherit;
}
.custom-header {
width: 100%;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
.tree-views-left-header-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 2px;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
margin-right: 6px;
background-color: #fff;
}
.tree-views-left-header-name {
flex: 1;
font-weight: bold;
margin-left: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
display: none;
margin-left: auto;
}
.action {
display: inline-block;
width: 22px;
text-align: center;
border-radius: 5px;
&:hover {
background-color: #cacaca;
}
}
}
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
white-space: nowrap;
}
.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;
}
}
}
.tree-views-right {
background-color: #fff;
display: flex;
flex-direction: column;
padding: 12px;
overflow: auto;
width: 100%;
border-radius: 15px;
.tree-views-right-bar {
display: inline-flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin-bottom: 10px;
height: 36px;
}
}
}
</style>