diff --git a/cmdb-ui/src/modules/cmdb/utils/helper.js b/cmdb-ui/src/modules/cmdb/utils/helper.js index 1ddd76f..44de91b 100644 --- a/cmdb-ui/src/modules/cmdb/utils/helper.js +++ b/cmdb-ui/src/modules/cmdb/utils/helper.js @@ -1,5 +1,7 @@ /* eslint-disable */ import _ from 'lodash' +import XLSX from 'xlsx' +import XLSXS from 'xlsx-js-style' export function sum(arr) { if (!arr.length) { return 0 @@ -149,4 +151,26 @@ export const toThousands = (num = 0) => { return num.toString().replace(/\d+/, function (n) { return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') }) -} \ No newline at end of file +} + +export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => { + // STEP 1: Create a new workbook + const wb = XLSXS.utils.book_new() + // STEP 2: Create data rows and styles + const rowArray = data + // STEP 3: Create worksheet with rows; Add worksheet to workbook + const ws = XLSXS.utils.aoa_to_sheet(rowArray) + XLSXS.utils.book_append_sheet(wb, ws, fileName) + + let maxColumnNumber = 1 // 默认最大列数 + rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } }) + + // 添加列宽 + ws['!cols'] = (rowArray[0].map(item => { + return { width: 22 } + })) + // // 添加行高 + // ws['!rows'] = [{ 'hpt': 80 }] + // STEP 4: Write Excel file to browser #导出 + XLSXS.writeFile(wb, fileName + '.xlsx') +} diff --git a/cmdb-ui/src/modules/cmdb/views/batch/modules/CiTypeChoice.vue b/cmdb-ui/src/modules/cmdb/views/batch/modules/CiTypeChoice.vue index fe9af41..49686c7 100644 --- a/cmdb-ui/src/modules/cmdb/views/batch/modules/CiTypeChoice.vue +++ b/cmdb-ui/src/modules/cmdb/views/batch/modules/CiTypeChoice.vue @@ -14,20 +14,83 @@ }}</a-select-option> </a-select> <a-button - @click="downLoadExcel" + @click="openModal" :disabled="!selectNum" type="primary" class="ops-button-primary" icon="download" >下载模板</a-button > + <a-modal + :bodyStyle="{ paddingTop: 0 }" + width="800px" + :title="`${ciTypeName}`" + :visible="visible" + @cancel="handleCancel" + @ok="handleOk" + wrapClassName="ci-type-choice-modal" + > + <a-divider orientation="left">模型属性</a-divider> + <a-checkbox + @change="changeCheckAll" + :style="{ marginBottom: '20px' }" + :indeterminate="indeterminate" + :checked="checkAll" + > + 全选 + </a-checkbox> + <br /> + <a-checkbox-group v-model="checkedAttrs"> + <a-row> + <a-col :span="6" v-for="item in selectCiTypeAttrList.attributes" :key="item.alias || item.name"> + <a-checkbox :disabled="item.name === selectCiTypeAttrList.unique" :value="item.alias || item.name"> + {{ item.alias || item.name }} + <span style="color: red" v-if="item.name === selectCiTypeAttrList.unique">*</span> + </a-checkbox> + </a-col> + </a-row> + </a-checkbox-group> + <template v-if="parentsType && parentsType.length"> + <a-divider orientation="left">模型关联</a-divider> + <a-row :gutter="[24, 24]" align="top" type="flex"> + <a-col :style="{ display: 'inline-flex' }" :span="12" v-for="item in parentsType" :key="item.id"> + <a-checkbox @click="clickParent(item)" :checked="checkedParents.includes(item.alias || item.name)"> + </a-checkbox> + <span + :style="{ + display: 'inline-block', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + width: '80px', + margin: '0 5px', + textAlign: 'right', + }" + :title="item.alias || item.name" + >{{ item.alias || item.name }}</span + > + <a-select :style="{ flex: 1 }" size="small" v-model="parentsForm[item.alias || item.name].attr"> + <a-select-option + :title="attr.alias || attr.name" + v-for="attr in item.attributes" + :key="attr.alias || attr.name" + :value="attr.alias || attr.name" + > + {{ attr.alias || attr.name }} + </a-select-option> + </a-select> + </a-col> + </a-row> + </template> + </a-modal> </a-space> </template> <script> +import { downloadExcel } from '../../../utils/helper' import { getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' -import { writeExcel } from '@/modules/cmdb/api/batch' +import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation' export default { name: 'CiTypeChoice', @@ -37,6 +100,13 @@ export default { ciTypeName: '', selectNum: 0, selectCiTypeAttrList: [], + visible: false, + checkedAttrs: [], + indeterminate: false, + checkAll: true, + parentsType: [], + parentsForm: {}, + checkedParents: [], } }, created: function() { @@ -44,6 +114,18 @@ export default { this.ciTypeList = res.ci_types }) }, + watch: { + checkedAttrs() { + if (this.checkedAttrs.length < this.selectCiTypeAttrList.attributes.length) { + this.indeterminate = true + this.checkAll = false + } + if (this.checkedAttrs.length === this.selectCiTypeAttrList.attributes.length) { + this.indeterminate = false + this.checkAll = true + } + }, + }, methods: { selectCiType(el) { // 当选择好模板类型时的回调函数 @@ -60,24 +142,70 @@ export default { }) }, - downLoadExcel() { - const columns = [] - this.selectCiTypeAttrList.attributes.forEach((item) => { - columns.push(item.alias) + openModal() { + getCITypeParent(this.selectNum).then((res) => { + this.parentsType = res.parents + const _parentsForm = {} + res.parents.forEach((item) => { + const _find = item.attributes.find((attr) => attr.id === item.unique_id) + _parentsForm[item.alias || item.name] = { attr: _find?.alias || _find?.name, value: '' } + }) + this.parentsForm = _parentsForm + this.checkedParents = [] + this.visible = true + this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name) }) - const excel = writeExcel(columns, this.ciTypeName) - const tempLink = document.createElement('a') - tempLink.download = this.ciTypeName + '.xls' - tempLink.style.display = 'none' - const blob = new Blob([excel]) - tempLink.href = URL.createObjectURL(blob) - document.body.appendChild(tempLink) - tempLink.click() - document.body.removeChild(tempLink) }, filterOption(input, option) { return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0 }, + handleCancel() { + this.visible = false + }, + handleOk() { + const columns1 = this.checkedAttrs.map((item) => { + return { + v: item, + t: 's', + s: { + numFmt: 'string', + }, + } + }) + const columns2 = this.checkedParents.map((p) => { + return { + v: `$${p}.${this.parentsForm[p].attr}`, + t: 's', + s: { + font: { + color: { + rgb: 'FF0000', + }, + }, + }, + } + }) + downloadExcel([[...columns1, ...columns2]], this.ciTypeName) + this.handleCancel() + }, + changeCheckAll(e) { + if (e.target.checked) { + this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name) + } else { + const _find = this.selectCiTypeAttrList.attributes.find( + (item) => item.name === this.selectCiTypeAttrList.unique + ) + this.checkedAttrs = [_find?.alias || _find?.name] + } + }, + clickParent(item) { + const _idx = this.checkedParents.findIndex((p) => p === (item.alias || item.name)) + if (_idx > -1) { + this.checkedParents.splice(_idx, 1) + } else { + this.checkedParents.push(item.alias || item.name) + } + }, }, } </script> @@ -105,3 +233,15 @@ export default { } } </style> + +<style lang="less"> +.ci-type-choice-modal { + .ant-checkbox-disabled .ant-checkbox-inner { + border-color: #2f54eb !important; + background-color: #2f54eb; + } + .ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner::after { + border-color: #fff; + } +} +</style> diff --git a/cmdb-ui/src/modules/cmdb/views/batch/modules/CiUploadTable.vue b/cmdb-ui/src/modules/cmdb/views/batch/modules/CiUploadTable.vue index 0d3b8aa..4a97bb1 100644 --- a/cmdb-ui/src/modules/cmdb/views/batch/modules/CiUploadTable.vue +++ b/cmdb-ui/src/modules/cmdb/views/batch/modules/CiUploadTable.vue @@ -40,15 +40,25 @@ export default { }, computed: { columns() { + const _columns = [] if (this.ciTypeAttrs.attributes) { - return this.ciTypeAttrs.attributes.map((item) => { - return { - title: item.alias || item.name, - field: item.alias || item.name, + _columns.push( + ...this.ciTypeAttrs.attributes.map((item) => { + return { + title: item.alias || item.name, + field: item.alias || item.name, + } + }) + ) + } + if (this.uploadData && this.uploadData.length) { + Object.keys(this.uploadData[0]).forEach((key) => { + if (key.startsWith('$')) { + _columns.push({ title: key, field: key }) } }) } - return [] + return _columns }, dataSource() { return _.cloneDeep(this.uploadData) diff --git a/cmdb-ui/src/modules/cmdb/views/batch/modules/UploadFileForm.vue b/cmdb-ui/src/modules/cmdb/views/batch/modules/UploadFileForm.vue index a258f5b..1898ce0 100644 --- a/cmdb-ui/src/modules/cmdb/views/batch/modules/UploadFileForm.vue +++ b/cmdb-ui/src/modules/cmdb/views/batch/modules/UploadFileForm.vue @@ -4,13 +4,13 @@ ref="upload" :multiple="false" :customRequest="customRequest" - accept=".xls" + accept=".xls,.xlsx" :showUploadList="false" :fileList="fileList" > <img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" /> <p class="ant-upload-text">点击或拖拽文件至此上传!</p> - <p class="ant-upload-hint">支持文件类型:xls</p> + <p class="ant-upload-hint">支持文件类型:xls,xlsx</p> </a-upload-dragger> <div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file"> <span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span> diff --git a/cmdb-ui/src/modules/cmdb/views/ci/index.vue b/cmdb-ui/src/modules/cmdb/views/ci/index.vue index 20d7ac1..3479214 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci/index.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci/index.vue @@ -209,13 +209,19 @@ <EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs" /> </template> <template #default="{ row }"> - <a @click="$refs.detail.create(row.ci_id || row._id)"> - <a-icon type="unordered-list" /> - </a> - <a-divider type="vertical" /> - <a @click="deleteCI(row)" :style="{ color: 'red' }"> - <a-icon type="delete" /> - </a> + <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> + <a @click="deleteCI(row)" :style="{ color: 'red' }"> + <a-icon type="delete" /> + </a> + </a-space> </template> </vxe-column> <template #empty> diff --git a/cmdb-ui/src/modules/cmdb/views/ci/modules/CiDetail.vue b/cmdb-ui/src/modules/cmdb/views/ci/modules/CiDetail.vue index c31aa5d..303cbb4 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci/modules/CiDetail.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci/modules/CiDetail.vue @@ -38,7 +38,7 @@ <a-tab-pane key="tab_2"> <span slot="tab"><a-icon type="branches" />关系</span> <div :style="{ padding: '24px' }"> - <CiDetailRelation :ciId="ciId" :typeId="typeId" :ci="ci" /> + <CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" /> </div> </a-tab-pane> <a-tab-pane key="tab_3"> @@ -147,8 +147,14 @@ export default { }, inject: ['reload', 'handleSearch', 'attrList'], methods: { - create(ciId) { + create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') { this.visible = true + this.activeTabKey = activeTabKey + if (activeTabKey === 'tab_2') { + this.$nextTick(() => { + this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey + }) + } this.ciId = ciId this.getAttributes() this.getCI() diff --git a/cmdb-ui/src/modules/cmdb/views/ci/modules/CreateInstanceForm.vue b/cmdb-ui/src/modules/cmdb/views/ci/modules/CreateInstanceForm.vue index 63f02dc..bc9750d 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci/modules/CreateInstanceForm.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci/modules/CreateInstanceForm.vue @@ -23,6 +23,30 @@ :attributeList="attributeList" /> </template> + <template v-if="parentsType && parentsType.length"> + <a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider> + <a-form> + <a-row :gutter="24" align="top" type="flex"> + <a-col :span="12" v-for="item in parentsType" :key="item.id"> + <a-form-item :label="item.alias || item.name" :colon="false"> + <a-input-group compact style="width: 100%"> + <a-select v-model="parentsForm[item.name].attr"> + <a-select-option + :title="attr.alias || attr.name" + v-for="attr in item.attributes" + :key="attr.name" + :value="attr.name" + > + {{ attr.alias || attr.name }} + </a-select-option> + </a-select> + <a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" /> + </a-input-group> + </a-form-item> + </a-col> + </a-row> + </a-form> + </template> </template> <template v-if="action === 'update'"> <a-form :form="form"> @@ -110,6 +134,7 @@ import { addCI } from '@/modules/cmdb/api/ci' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import { valueTypeMap } from '../../../utils/const' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' +import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation' export default { name: 'CreateInstanceForm', @@ -138,6 +163,8 @@ export default { batchUpdateLists: [], editAttr: null, attributesByGroup: [], + parentsType: [], + parentsForm: {}, } }, computed: { @@ -231,6 +258,11 @@ export default { } }) values.ci_type = _this.typeId + Object.keys(this.parentsForm).forEach((type) => { + if (this.parentsForm[type].value) { + values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value + } + }) addCI(values).then((res) => { _this.$message.success('新增成功!') _this.visible = false @@ -249,6 +281,17 @@ export default { Promise.all([this.getCIType(), this.getAttributeList()]).then(() => { this.batchUpdateLists = [{ name: this.attributeList[0].name }] }) + if (action === 'create') { + getCITypeParent(this.typeId).then((res) => { + this.parentsType = res.parents + const _parentsForm = {} + res.parents.forEach((item) => { + const _find = item.attributes.find((attr) => attr.id === item.unique_id) + _parentsForm[item.name] = { attr: _find.name, value: '' } + }) + this.parentsForm = _parentsForm + }) + } }) }, getFieldType(name) { diff --git a/cmdb-ui/src/modules/cmdb/views/ci/modules/ciDetailRelation.vue b/cmdb-ui/src/modules/cmdb/views/ci/modules/ciDetailRelation.vue index a44d7c2..b862510 100644 --- a/cmdb-ui/src/modules/cmdb/views/ci/modules/ciDetailRelation.vue +++ b/cmdb-ui/src/modules/cmdb/views/ci/modules/ciDetailRelation.vue @@ -244,14 +244,13 @@ export default { }, }, mounted() { - console.log(this.ci) this.init(true) }, methods: { async init(isFirst) { await Promise.all([this.getParentCITypes(), this.getChildCITypes()]) Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => { - if (isFirst) { + if (isFirst && this.$refs.ciDetailRelationTopo) { this.$refs.ciDetailRelationTopo.setTopoData(this.topoData) } }) @@ -395,12 +394,6 @@ export default { margin-top: 20px; margin-bottom: 5px; color: #303133; - > a { - display: none; - } - &:hover > a { - display: inline-block; - } } } </style> diff --git a/cmdb-ui/src/modules/cmdb/views/relation_views/index.vue b/cmdb-ui/src/modules/cmdb/views/relation_views/index.vue index 8796295..1d17eac 100644 --- a/cmdb-ui/src/modules/cmdb/views/relation_views/index.vue +++ b/cmdb-ui/src/modules/cmdb/views/relation_views/index.vue @@ -243,17 +243,23 @@ /> </template> <template #default="{ row }"> - <a @click="$refs.detail.create(row.ci_id || row._id)"> - <a-icon type="unordered-list" /> - </a> - <template v-if="isLeaf"> - <a-divider type="vertical" /> - <a-tooltip title="删除实例"> - <a @click="deleteCI(row)" :style="{ color: 'red' }"> - <a-icon type="delete" /> + <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> + <template v-if="isLeaf"> + <a-tooltip title="删除实例"> + <a @click="deleteCI(row)" :style="{ color: 'red' }"> + <a-icon type="delete" /> + </a> + </a-tooltip> + </template> + </a-space> </template> </vxe-column> <template #empty> diff --git a/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue b/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue index 6da78c1..592b5d4 100644 --- a/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue +++ b/cmdb-ui/src/modules/cmdb/views/tree_views/index.vue @@ -297,17 +297,23 @@ <EditAttrsPopover :typeId="Number(typeId)" class="operation-icon" @refresh="refreshAfterEditAttrs" /> </template> <template #default="{ row }"> - <a @click="$refs.detail.create(row.ci_id || row._id)"> - <a-icon type="unordered-list" /> - </a> - <template> - <a-divider type="vertical" /> - <a-tooltip title="删除实例"> - <a @click="deleteCI(row)" :style="{ color: 'red' }"> - <a-icon type="delete" /> + <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> + <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>