mirror of
https://github.com/veops/cmdb.git
synced 2025-08-07 11:28:06 +08:00
530 lines
18 KiB
Python
530 lines
18 KiB
Python
<template>
|
|
<div id="resource_search" :style="{ height: fromCronJob ? `${windowHeight - 48}px` : `${windowHeight - 90}px` }">
|
|
<div class="cmdb-views-header">
|
|
<span>
|
|
<span class="cmdb-views-header-title">资源搜索</span>
|
|
</span>
|
|
</div>
|
|
<div :style="{ backgroundColor: '#fff', padding: '12px', borderRadius: '15px' }">
|
|
<SearchForm
|
|
ref="search"
|
|
type="resourceSearch"
|
|
@refresh="handleSearch"
|
|
:preferenceAttrList="allAttributesList"
|
|
@updateAllAttributesList="updateAllAttributesList"
|
|
@copyExpression="copyExpression"
|
|
/>
|
|
<div
|
|
v-if="!fromCronJob"
|
|
:style="{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
height: '32px',
|
|
marginBottom: '5px',
|
|
alignItems: 'center',
|
|
}"
|
|
>
|
|
<a-button icon="download" type="primary" ghost size="small" @click="handleExport">导出</a-button>
|
|
<PreferenceSearch
|
|
ref="preferenceSearch"
|
|
@getQAndSort="getQAndSort"
|
|
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
|
|
/>
|
|
</div>
|
|
<vxe-table
|
|
:id="`cmdb-resource`"
|
|
border
|
|
keep-source
|
|
show-overflow
|
|
resizable
|
|
ref="xTable"
|
|
size="small"
|
|
row-id="_id"
|
|
:loading="loading"
|
|
:height="fromCronJob ? windowHeight - 180 : windowHeight - 250"
|
|
show-header-overflow
|
|
highlight-hover-row
|
|
:data="instanceList"
|
|
:sort-config="{ remote: true, trigger: 'cell' }"
|
|
@sort-change="handleSortCol"
|
|
:row-key="true"
|
|
:column-key="true"
|
|
:cell-style="getCellStyle"
|
|
:scroll-y="{ enabled: true, gt: 20 }"
|
|
:scroll-x="{ enabled: true, gt: 0 }"
|
|
:export-config="{
|
|
isColgroup: true,
|
|
type: 'xlsx',
|
|
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
|
|
mode: 'current',
|
|
modes: ['current'],
|
|
isFooter: false,
|
|
isHeader: true,
|
|
isColgroup: true,
|
|
}"
|
|
class="ops-unstripe-table"
|
|
:style="{ margin: '0 -12px' }"
|
|
:custom-config="{ storage: true }"
|
|
>
|
|
<vxe-column
|
|
v-if="instanceList.length"
|
|
title="模型"
|
|
field="ci_type_alias"
|
|
:width="100"
|
|
fixed="left"
|
|
></vxe-column>
|
|
<vxe-colgroup v-for="colGroup in columnsGroup" :key="colGroup.value" :title="colGroup.label">
|
|
<template #header>
|
|
<span :style="{ display: 'inline-flex', alignItems: 'center' }">
|
|
{{ colGroup.label }}
|
|
<EditAttrsPopover
|
|
:style="{ borderLeft: 'none', width: '30px', height: '38px', cursor: 'pointer' }"
|
|
v-if="colGroup.isCiType"
|
|
:typeId="Number(colGroup.id.split('-')[1])"
|
|
@refresh="loadInstance"
|
|
/>
|
|
</span>
|
|
</template>
|
|
<vxe-column
|
|
v-for="(col, index) in colGroup.children"
|
|
:key="`${col.field}_${index}`"
|
|
:title="col.title"
|
|
:field="col.field"
|
|
:width="col.width"
|
|
:minWidth="100"
|
|
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
|
|
>
|
|
<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]">{{ JSON.stringify(row[col.field]) }}</span>
|
|
<a
|
|
v-else-if="col.is_link && row[col.field]"
|
|
:href="
|
|
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
|
? `${row[col.field]}`
|
|
: `http://${row[col.field]}`
|
|
"
|
|
target="_blank"
|
|
>{{ row[col.field] }}</a
|
|
>
|
|
<PasswordField
|
|
v-else-if="col.is_password && row[col.field]"
|
|
:ci_id="row._id"
|
|
:attr_id="col.attr_id"
|
|
></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),
|
|
}"
|
|
><ops-icon
|
|
:style="{ color: getChoiceValueIcon(col, value).color }"
|
|
:type="getChoiceValueIcon(col, value).name"
|
|
/>{{ value }}</span
|
|
>
|
|
</template>
|
|
<span
|
|
v-else
|
|
:style="{
|
|
borderRadius: '4px',
|
|
padding: '1px 5px',
|
|
margin: '2px 0',
|
|
...getChoiceValueStyle(col, row[col.field]),
|
|
}"
|
|
>
|
|
<ops-icon
|
|
:style="{ color: getChoiceValueIcon(col, row[col.field]).color }"
|
|
:type="getChoiceValueIcon(col, row[col.field]).name"
|
|
/>
|
|
{{ row[col.field] }}</span
|
|
>
|
|
</template>
|
|
</template>
|
|
</vxe-column>
|
|
</vxe-colgroup>
|
|
|
|
<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>
|
|
</vxe-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"
|
|
@showSizeChange="onShowSizeChange"
|
|
:show-total="(total, range) => `当前${range[0]}-${range[1]} 共 ${total}条记录`"
|
|
@change="
|
|
(page) => {
|
|
currentPage = page
|
|
loadInstance(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>
|
|
|
|
<BatchDownload
|
|
:replaceFields="{ cildren: 'children', title: 'label', key: 'id' }"
|
|
ref="batchDownload"
|
|
@batchDownload="batchDownload"
|
|
treeType="tree"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import _ from 'lodash'
|
|
import SearchForm from '../../components/searchForm/SearchForm.vue'
|
|
import { searchCI } from '../../api/ci'
|
|
import { searchAttributes, getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
|
|
import { getCITypes } from '../../api/CIType'
|
|
import { getSubscribeAttributes } from '../../api/preference'
|
|
import { getCITableColumns } from '../../utils/helper'
|
|
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
|
|
import PasswordField from '../../components/passwordField/index.vue'
|
|
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
|
|
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
|
|
|
|
export default {
|
|
name: 'ResourceSearch',
|
|
components: { SearchForm, EditAttrsPopover, PasswordField, BatchDownload, PreferenceSearch },
|
|
props: {
|
|
fromCronJob: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
ciTypes: [],
|
|
allAttributesList: [], // 当前选择的模型的全部attributes 默认全部
|
|
currentPage: 1,
|
|
pageSizeOptions: ['50', '100', '200', '100000'],
|
|
pageSize: 50,
|
|
totalNumber: 0,
|
|
instanceList: [],
|
|
// columns: {},
|
|
sortByTable: undefined,
|
|
loading: false,
|
|
columnsGroup: [],
|
|
}
|
|
},
|
|
computed: {
|
|
windowHeight() {
|
|
return this.$store.state.windowHeight
|
|
},
|
|
},
|
|
provide() {
|
|
return {
|
|
setPreferenceSearchCurrent: this.setPreferenceSearchCurrent,
|
|
filterCompPreferenceSearch: () => {},
|
|
}
|
|
},
|
|
mounted() {
|
|
this.getAllAttr()
|
|
this.getAllCiTypes()
|
|
},
|
|
methods: {
|
|
getAllCiTypes() {
|
|
getCITypes().then((res) => {
|
|
this.ciTypes = res.ci_types
|
|
})
|
|
},
|
|
getAllAttr() {
|
|
searchAttributes({ page_size: 9999 }).then((res) => {
|
|
this.allAttributesList = res.attributes
|
|
})
|
|
},
|
|
updateAllAttributesList(value) {
|
|
if (value && value.length) {
|
|
getCITypeAttributesByTypeIds({ type_ids: value.join(',') }).then((res) => {
|
|
this.allAttributesList = res.attributes
|
|
})
|
|
} else {
|
|
this.getAllAttr()
|
|
}
|
|
},
|
|
async loadInstance(sortByTable = undefined) {
|
|
this.loading = true
|
|
// 若模糊搜索可以 queryParam相关后期可删除
|
|
// const queryParams = this.$refs['search'].queryParam || {}
|
|
const fuzzySearch = this.$refs['search'].fuzzySearch
|
|
const expression = this.$refs['search'].expression || ''
|
|
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
|
const regSort = /(?<=sort=).+/g
|
|
|
|
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
|
// 如果是表格点击的排序 以表格为准
|
|
let sort
|
|
if (sortByTable) {
|
|
sort = sortByTable
|
|
} else {
|
|
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
|
|
}
|
|
if (!sort) {
|
|
sort = '_type'
|
|
}
|
|
let currenCiType = this.$refs['search'].currenCiType
|
|
if (!currenCiType.length) {
|
|
const _currenCiType = []
|
|
this.$refs['search'].ciTypeGroup.forEach((item) => {
|
|
_currenCiType.push(...item.ci_types.map((type) => type.id))
|
|
})
|
|
currenCiType = _currenCiType
|
|
}
|
|
searchCI({
|
|
q: `${currenCiType && currenCiType.length ? `_type:(${currenCiType.join(';')})` : ''}${exp ? `,${exp}` : ''}${
|
|
fuzzySearch ? `,*${fuzzySearch}*` : ''
|
|
}`,
|
|
count: this.pageSize,
|
|
page: this.currentPage,
|
|
sort,
|
|
})
|
|
.then(async (res) => {
|
|
this.columnsGroup = []
|
|
this.instanceList = []
|
|
this.totalNumber = res['numfound']
|
|
|
|
const oldData = res.result
|
|
|
|
function allKeys(data) {
|
|
const keys = {}
|
|
const ignoreAttr = ['_id', '_type', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias']
|
|
data.forEach((item) => {
|
|
Object.keys(item).forEach((key) => {
|
|
if (!ignoreAttr.includes(key)) {
|
|
keys[key] = ''
|
|
}
|
|
})
|
|
})
|
|
return keys
|
|
}
|
|
|
|
function tidy(data) {
|
|
const outputKeys = allKeys(data)
|
|
const common = {}
|
|
data.forEach((item) => {
|
|
const tmp = {}
|
|
Object.keys(outputKeys).forEach((j) => {
|
|
if (j in item) {
|
|
tmp[j] = item[j]
|
|
// 提取common
|
|
{
|
|
const key = item['ci_type_alias']
|
|
if (j in common) {
|
|
common[j][[key]] = ''
|
|
} else {
|
|
common[j] = { [key]: '' }
|
|
}
|
|
}
|
|
} else {
|
|
tmp[j] = null
|
|
}
|
|
})
|
|
})
|
|
const commonObject = {}
|
|
const commonKeys = []
|
|
// 整理common
|
|
Object.keys(common).forEach((key) => {
|
|
if (Object.keys(common[key]).length > 1) {
|
|
commonKeys.push(key)
|
|
const reverseKey = Object.keys(common[key]).join('&')
|
|
if (!commonObject[reverseKey]) {
|
|
commonObject[reverseKey] = [key]
|
|
} else {
|
|
commonObject[reverseKey].push(key)
|
|
}
|
|
}
|
|
})
|
|
return { commonObject, commonKeys }
|
|
}
|
|
|
|
const { commonObject, commonKeys } = tidy(oldData)
|
|
const _commonColumnsGroup = Object.keys(commonObject).map((key) => {
|
|
return {
|
|
id: `parent-${key}`,
|
|
value: key,
|
|
label: key,
|
|
children: this.getColumns(
|
|
res.result,
|
|
commonObject[key].map((item) => {
|
|
const _find = this.allAttributesList.find((attr) => attr.name === item)
|
|
return _find
|
|
})
|
|
),
|
|
}
|
|
})
|
|
|
|
const _columnsGroup = Object.keys(res.counter).map((key) => {
|
|
const _find = this.ciTypes.find((item) => item.name === key)
|
|
return {
|
|
id: `parent-${_find.id}`,
|
|
value: key,
|
|
label: _find?.alias || _find?.name,
|
|
isCiType: true,
|
|
}
|
|
})
|
|
|
|
const promises = _columnsGroup.map((item) => {
|
|
return getSubscribeAttributes(item.id.split('-')[1]).then((res1) => {
|
|
item.children = this.getColumns(res.result, res1.attributes).filter(
|
|
(col) => !commonKeys.includes(col.field)
|
|
)
|
|
})
|
|
})
|
|
await Promise.all(promises).then(() => {
|
|
this.columnsGroup = [..._commonColumnsGroup, ..._columnsGroup]
|
|
this.instanceList = res['result']
|
|
})
|
|
})
|
|
.finally(() => {
|
|
this.loading = false
|
|
})
|
|
},
|
|
getColumns(data, attrList) {
|
|
const width = document.getElementById('resource_search').clientWidth - 50
|
|
return getCITableColumns(data, attrList, width).map((item) => {
|
|
return { ...item, id: item.field, label: item.title }
|
|
})
|
|
},
|
|
handleSearch() {
|
|
this.currentPage = 1
|
|
this.loadInstance()
|
|
},
|
|
onShowSizeChange(current, pageSize) {
|
|
this.pageSize = pageSize
|
|
this.currentPage = 1
|
|
this.loadInstance()
|
|
},
|
|
handleSortCol() {},
|
|
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
|
|
const { property } = column
|
|
const _find = this.allAttributesList.find((attr) => attr.name === property)
|
|
if (
|
|
_find &&
|
|
_find.option &&
|
|
_find.option.fontOptions &&
|
|
row[`${property}`] !== undefined &&
|
|
row[`${property}`] !== null
|
|
) {
|
|
return { ..._find.option.fontOptions }
|
|
}
|
|
},
|
|
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 {}
|
|
},
|
|
handleExport() {
|
|
// this.$refs.xTable.openExport()
|
|
this.$refs.batchDownload.open({
|
|
preferenceAttrList: [{ id: `ci_type_alias`, value: 'ci_type_alias', label: '模型' }, ...this.columnsGroup],
|
|
})
|
|
},
|
|
batchDownload({ filename, type, checkedKeys }) {
|
|
const jsonAttrList = []
|
|
checkedKeys.forEach((key) => {
|
|
const _find = this.allAttributesList.find((attr) => attr.name === key)
|
|
if (_find && _find.value_type === '6') {
|
|
jsonAttrList.push(key)
|
|
}
|
|
})
|
|
const data = _.cloneDeep(this.instanceList)
|
|
this.$refs.xTable.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 }
|
|
}),
|
|
],
|
|
download: false,
|
|
})
|
|
this.selectedRowKeys = []
|
|
this.$refs.xTable.clearCheckboxRow()
|
|
this.$refs.xTable.clearCheckboxReserve()
|
|
},
|
|
getQAndSort() {
|
|
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
|
|
const expression = this.$refs['search'].expression || ''
|
|
const currenCiType = this.$refs['search'].currenCiType || undefined
|
|
this.$refs.preferenceSearch.savePreference({ fuzzySearch, expression, currenCiType })
|
|
},
|
|
setParamsFromPreferenceSearch(item) {
|
|
const { fuzzySearch, expression, currenCiType } = item.option
|
|
this.$refs.search.fuzzySearch = fuzzySearch
|
|
this.$refs.search.expression = expression
|
|
this.$refs.search.currenCiType = currenCiType
|
|
this.currentPage = 1
|
|
this.$nextTick(() => {
|
|
this.loadInstance()
|
|
})
|
|
},
|
|
setPreferenceSearchCurrent(id = null) {
|
|
if (this.$refs.preferenceSearch) {
|
|
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
|
|
let currenCiType = this.$refs['search'].currenCiType
|
|
if (!currenCiType.length) {
|
|
const _currenCiType = []
|
|
this.$refs['search'].ciTypeGroup.forEach((item) => {
|
|
_currenCiType.push(...item.ci_types.map((type) => type.id))
|
|
})
|
|
currenCiType = _currenCiType
|
|
}
|
|
const text = `q=${currenCiType && currenCiType.length ? `_type:(${currenCiType.join(';')})` : ''}${
|
|
exp ? `,${exp}` : ''
|
|
}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
|
|
this.$copyText(text)
|
|
.then(() => {
|
|
this.$message.success('复制成功!')
|
|
this.$emit('copySuccess', text)
|
|
})
|
|
.catch(() => {
|
|
this.$message.error('复制失败!')
|
|
})
|
|
},
|
|
},
|
|
}
|
|
</script>
|