mirror of
https://github.com/veops/cmdb.git
synced 2025-11-07 23:46:11 +08:00
Modify code organization
This commit is contained in:
15
cmdb-ui/src/views/404.vue
Normal file
15
cmdb-ui/src/views/404.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
404 page
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: '404'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
288
cmdb-ui/src/views/account/center/Index.vue
Normal file
288
cmdb-ui/src/views/account/center/Index.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="page-header-index-wide page-header-wrapper-grid-content-main">
|
||||
<a-row :gutter="24">
|
||||
<a-col :md="24" :lg="7">
|
||||
<a-card :bordered="false">
|
||||
<div class="account-center-avatarHolder">
|
||||
<div class="avatar">
|
||||
<img :src="avatar()">
|
||||
</div>
|
||||
<div class="username">{{ nickname() }}</div>
|
||||
<div class="bio">海纳百川,有容乃大</div>
|
||||
</div>
|
||||
<div class="account-center-detail">
|
||||
<p>
|
||||
<i class="title"></i>交互专家
|
||||
</p>
|
||||
<p>
|
||||
<i class="group"></i>蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED
|
||||
</p>
|
||||
<p>
|
||||
<i class="address"></i>
|
||||
<span>浙江省</span>
|
||||
<span>杭州市</span>
|
||||
</p>
|
||||
</div>
|
||||
<a-divider/>
|
||||
|
||||
<div class="account-center-tags">
|
||||
<div class="tagsTitle">标签</div>
|
||||
<div>
|
||||
<template v-for="(tag, index) in tags">
|
||||
<a-tooltip v-if="tag.length > 20" :key="tag" :title="tag">
|
||||
<a-tag
|
||||
:key="tag"
|
||||
:closable="index !== 0"
|
||||
:afterClose="() => handleTagClose(tag)"
|
||||
>{{ `${tag.slice(0, 20)}...` }}</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tag
|
||||
v-else
|
||||
:key="tag"
|
||||
:closable="index !== 0"
|
||||
:afterClose="() => handleTagClose(tag)"
|
||||
>{{ tag }}</a-tag>
|
||||
</template>
|
||||
<a-input
|
||||
v-if="tagInputVisible"
|
||||
ref="tagInput"
|
||||
type="text"
|
||||
size="small"
|
||||
:style="{ width: '78px' }"
|
||||
:value="tagInputValue"
|
||||
@change="handleInputChange"
|
||||
@blur="handleTagInputConfirm"
|
||||
@keyup.enter="handleTagInputConfirm"
|
||||
/>
|
||||
<a-tag v-else @click="showTagInput" style="background: #fff; borderStyle: dashed;">
|
||||
<a-icon type="plus"/>New Tag
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider :dashed="true"/>
|
||||
|
||||
<div class="account-center-team">
|
||||
<div class="teamTitle">团队</div>
|
||||
<a-spin :spinning="teamSpinning">
|
||||
<div class="members">
|
||||
<a-row>
|
||||
<a-col :span="12" v-for="(item, index) in teams" :key="index">
|
||||
<a>
|
||||
<a-avatar size="small" :src="item.avatar"/>
|
||||
<span class="member">{{ item.name }}</span>
|
||||
</a>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :md="24" :lg="17">
|
||||
<a-card
|
||||
style="width:100%"
|
||||
:bordered="false"
|
||||
:tabList="tabListNoTitle"
|
||||
:activeTabKey="noTitleKey"
|
||||
@tabChange="key => handleTabChange(key, 'noTitleKey')"
|
||||
>
|
||||
<article-page v-if="noTitleKey === 'article'"></article-page>
|
||||
<app-page v-else-if="noTitleKey === 'app'"></app-page>
|
||||
<project-page v-else-if="noTitleKey === 'project'"></project-page>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PageView, RouteView } from '@/layouts'
|
||||
import { AppPage, ArticlePage, ProjectPage } from './page'
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RouteView,
|
||||
PageView,
|
||||
AppPage,
|
||||
ArticlePage,
|
||||
ProjectPage
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tags: ['很有想法的', '专注设计', '辣~', '大长腿', '川妹子', '海纳百川'],
|
||||
|
||||
tagInputVisible: false,
|
||||
tagInputValue: '',
|
||||
|
||||
teams: [],
|
||||
teamSpinning: true,
|
||||
|
||||
tabListNoTitle: [
|
||||
{
|
||||
key: 'article',
|
||||
tab: '文章(8)'
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
tab: '应用(8)'
|
||||
},
|
||||
{
|
||||
key: 'project',
|
||||
tab: '项目(8)'
|
||||
}
|
||||
],
|
||||
noTitleKey: 'app'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getTeams()
|
||||
},
|
||||
methods: {
|
||||
...mapGetters(['nickname', 'avatar']),
|
||||
|
||||
getTeams () {
|
||||
this.$http.get('/workplace/teams').then(res => {
|
||||
this.teams = res.result
|
||||
this.teamSpinning = false
|
||||
})
|
||||
},
|
||||
|
||||
handleTabChange (key, type) {
|
||||
this[type] = key
|
||||
},
|
||||
|
||||
handleTagClose (removeTag) {
|
||||
const tags = this.tags.filter(tag => tag !== removeTag)
|
||||
this.tags = tags
|
||||
},
|
||||
|
||||
showTagInput () {
|
||||
this.tagInputVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.tagInput.focus()
|
||||
})
|
||||
},
|
||||
|
||||
handleInputChange (e) {
|
||||
this.tagInputValue = e.target.value
|
||||
},
|
||||
|
||||
handleTagInputConfirm () {
|
||||
const inputValue = this.tagInputValue
|
||||
let tags = this.tags
|
||||
if (inputValue && !tags.includes(inputValue)) {
|
||||
tags = [...tags, inputValue]
|
||||
}
|
||||
|
||||
Object.assign(this, {
|
||||
tags,
|
||||
tagInputVisible: false,
|
||||
tagInputValue: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-header-wrapper-grid-content-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
transition: 0.3s;
|
||||
|
||||
.account-center-avatarHolder {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
& > .avatar {
|
||||
margin: 0 auto;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-center-detail {
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 26px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
background: url(https://gw.alipayobjects.com/zos/rmsportal/pBjWzVAHnOOtAUvZmZfy.svg);
|
||||
}
|
||||
|
||||
.title {
|
||||
background-position: 0 0;
|
||||
}
|
||||
.group {
|
||||
background-position: 0 -22px;
|
||||
}
|
||||
.address {
|
||||
background-position: 0 -44px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-center-tags {
|
||||
.ant-tag {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-center-team {
|
||||
.members {
|
||||
a {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
.member {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 24px;
|
||||
max-width: 100px;
|
||||
vertical-align: top;
|
||||
margin-left: 12px;
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
&:hover {
|
||||
span {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tagsTitle,
|
||||
.teamTitle {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
113
cmdb-ui/src/views/account/center/page/App.vue
Normal file
113
cmdb-ui/src/views/account/center/page/App.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="app-list">
|
||||
<a-list
|
||||
:grid="{ gutter: 24, lg: 3, md: 2, sm: 1, xs: 1 }"
|
||||
:dataSource="dataSource">
|
||||
<a-list-item slot="renderItem" slot-scope="item">
|
||||
<a-card :hoverable="true">
|
||||
<a-card-meta>
|
||||
<div style="margin-bottom: 3px" slot="title">{{ item.title }}</div>
|
||||
<a-avatar class="card-avatar" slot="avatar" :src="item.avatar" size="small"/>
|
||||
<div class="meta-cardInfo" slot="description">
|
||||
<div>
|
||||
<p>活跃用户</p>
|
||||
<p>
|
||||
<span>{{ item.activeUser }}<span>万</span></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>新增用户</p>
|
||||
<p>{{ item.newUser | NumberFormat }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-card-meta>
|
||||
<template class="ant-card-actions" slot="actions">
|
||||
<a>
|
||||
<a-icon type="download"/>
|
||||
</a>
|
||||
<a>
|
||||
<a-icon type="edit"/>
|
||||
</a>
|
||||
<a>
|
||||
<a-icon type="share-alt"/>
|
||||
</a>
|
||||
<a>
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link" href="javascript:;">
|
||||
<a-icon type="ellipsis"/>
|
||||
</a>
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item>
|
||||
<a href="javascript:;">1st menu item</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a href="javascript:;">2nd menu item</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a href="javascript:;">3rd menu item</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const dataSource = []
|
||||
for (let i = 0; i < 11; i++) {
|
||||
dataSource.push({
|
||||
title: 'Alipay',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
|
||||
activeUser: 17,
|
||||
newUser: 1700
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Article',
|
||||
components: {},
|
||||
data () {
|
||||
return {
|
||||
dataSource
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.app-list {
|
||||
|
||||
.meta-cardInfo {
|
||||
zoom: 1;
|
||||
margin-top: 16px;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
float: left;
|
||||
width: 50%;
|
||||
|
||||
p {
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
|
||||
&:first-child {
|
||||
color: rgba(0, 0, 0, .45);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
75
cmdb-ui/src/views/account/center/page/Article.vue
Normal file
75
cmdb-ui/src/views/account/center/page/Article.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<a-list
|
||||
size="large"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
itemLayout="vertical"
|
||||
:dataSource="data"
|
||||
>
|
||||
<a-list-item :key="item.id" slot="renderItem" slot-scope="item">
|
||||
<template slot="actions">
|
||||
<icon-text type="star-o" :text="item.star" />
|
||||
<icon-text type="like-o" :text="item.like" />
|
||||
<icon-text type="message" :text="item.message" />
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<a slot="title" href="https://vue.ant.design/">{{ item.title }}</a>
|
||||
<template slot="description">
|
||||
<span>
|
||||
<a-tag>Ant Design</a-tag>
|
||||
<a-tag>设计语言</a-tag>
|
||||
<a-tag>蚂蚁金服</a-tag>
|
||||
</span>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<article-list-content :description="item.description" :owner="item.owner" :avatar="item.avatar" :href="item.href" :updateAt="item.updatedAt" />
|
||||
</a-list-item>
|
||||
<div slot="footer" v-if="data.length > 0" style="text-align: center; margin-top: 16px;">
|
||||
<a-button @click="loadMore" :loading="loadingMore">加载更多</a-button>
|
||||
</div>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ArticleListContent } from '@/components'
|
||||
import IconText from '@/views/list/search/components/IconText'
|
||||
|
||||
export default {
|
||||
name: 'Article',
|
||||
components: {
|
||||
IconText,
|
||||
ArticleListContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
data: []
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList () {
|
||||
this.$http.get('/list/article').then(res => {
|
||||
console.log('res', res)
|
||||
this.data = res.result
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
loadMore () {
|
||||
this.loadingMore = true
|
||||
this.$http.get('/list/article').then(res => {
|
||||
this.data = this.data.concat(res.result)
|
||||
}).finally(() => {
|
||||
this.loadingMore = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
109
cmdb-ui/src/views/account/center/page/Project.vue
Normal file
109
cmdb-ui/src/views/account/center/page/Project.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="ant-pro-pages-account-projects-cardList">
|
||||
<a-list :loading="loading" :data-source="data" :grid="{ gutter: 24, xxl: 3, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }">
|
||||
<a-list-item slot="renderItem" slot-scope="item">
|
||||
<a-card class="ant-pro-pages-account-projects-card" hoverable>
|
||||
<img slot="cover" :src="item.cover" :alt="item.title" />
|
||||
<a-card-meta :title="item.title">
|
||||
<template slot="description">
|
||||
<ellipsis :length="50">{{ item.description }}</ellipsis>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
<div class="cardItemContent">
|
||||
<span>{{ item.updatedAt | fromNow }}</span>
|
||||
<div class="avatarList">
|
||||
<avatar-list size="mini">
|
||||
<avatar-list-item
|
||||
v-for="(member, i) in item.members"
|
||||
:key="`${item.id}-avatar-${i}`"
|
||||
:src="member.avatar"
|
||||
:tips="member.name"
|
||||
/>
|
||||
</avatar-list>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import { TagSelect, StandardFormRow, Ellipsis, AvatarList } from '@/components'
|
||||
const TagSelectOption = TagSelect.Option
|
||||
const AvatarListItem = AvatarList.AvatarItem
|
||||
|
||||
export default {
|
||||
name: 'Project',
|
||||
components: {
|
||||
AvatarList,
|
||||
AvatarListItem,
|
||||
Ellipsis,
|
||||
TagSelect,
|
||||
TagSelectOption,
|
||||
StandardFormRow
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
data: [],
|
||||
form: this.$form.createForm(this),
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
fromNow (date) {
|
||||
return moment(date).fromNow()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
handleChange (value) {
|
||||
console.log(`selected ${value}`)
|
||||
},
|
||||
getList () {
|
||||
this.$http.get('/list/article', { params: { count: 8 } }).then(res => {
|
||||
console.log('res', res)
|
||||
this.data = res.result
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ant-pro-pages-account-projects-cardList {
|
||||
margin-top: 24px;
|
||||
|
||||
/deep/ .ant-card-meta-title {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/deep/ .ant-card-meta-description {
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.cardItemContent {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: -4px;
|
||||
line-height: 20px;
|
||||
|
||||
> span {
|
||||
flex: 1 1;
|
||||
color: rgba(0,0,0,.45);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/deep/ .ant-pro-avatar-list {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
cmdb-ui/src/views/account/center/page/index.js
Normal file
5
cmdb-ui/src/views/account/center/page/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import AppPage from './App'
|
||||
import ArticlePage from './Article'
|
||||
import ProjectPage from './Project'
|
||||
|
||||
export { AppPage, ArticlePage, ProjectPage }
|
||||
109
cmdb-ui/src/views/account/settings/AvatarModal.vue
Normal file
109
cmdb-ui/src/views/account/settings/AvatarModal.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<a-modal
|
||||
title="修改头像"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:confirmLoading="confirmLoading"
|
||||
:width="800"
|
||||
@cancel="cancelHandel">
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="12" :style="{height: '350px'}">
|
||||
<vue-cropper
|
||||
ref="cropper"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:autoCrop="options.autoCrop"
|
||||
:autoCropWidth="options.autoCropWidth"
|
||||
:autoCropHeight="options.autoCropHeight"
|
||||
:fixedBox="options.fixedBox"
|
||||
@realTime="realTime"
|
||||
>
|
||||
</vue-cropper>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12" :style="{height: '350px'}">
|
||||
<div class="avatar-upload-preview">
|
||||
<img :src="previews.url" :style="previews.img"/>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<template slot="footer">
|
||||
<a-button key="back" @click="cancelHandel">取消</a-button>
|
||||
<a-button key="submit" type="primary" :loading="confirmLoading" @click="okHandel">保存</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
// import { VueCropper } from 'vue-cropper'
|
||||
|
||||
export default {
|
||||
/*
|
||||
components: {
|
||||
VueCropper
|
||||
},
|
||||
*/
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
id: null,
|
||||
confirmLoading: false,
|
||||
|
||||
options: {
|
||||
img: '/avatar2.jpg',
|
||||
autoCrop: true,
|
||||
autoCropWidth: 200,
|
||||
autoCropHeight: 200,
|
||||
fixedBox: true
|
||||
},
|
||||
previews: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit (id) {
|
||||
this.visible = true
|
||||
this.id = id
|
||||
/* 获取原始头像 */
|
||||
},
|
||||
close () {
|
||||
this.id = null
|
||||
this.visible = false
|
||||
},
|
||||
cancelHandel () {
|
||||
this.close()
|
||||
},
|
||||
okHandel () {
|
||||
const vm = this
|
||||
|
||||
vm.confirmLoading = true
|
||||
setTimeout(() => {
|
||||
vm.confirmLoading = false
|
||||
vm.close()
|
||||
vm.$message.success('上传头像成功')
|
||||
}, 2000)
|
||||
},
|
||||
|
||||
realTime (data) {
|
||||
this.previews = data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.avatar-upload-preview {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(50%, -50%);
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
cmdb-ui/src/views/account/settings/BaseSetting.vue
Normal file
161
cmdb-ui/src/views/account/settings/BaseSetting.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="account-settings-info-view">
|
||||
<a-row :gutter="16">
|
||||
<a-col :md="24" :lg="16">
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item
|
||||
label="昵称"
|
||||
>
|
||||
<a-input placeholder="给自己起个名字" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="Bio"
|
||||
>
|
||||
<a-textarea rows="4" placeholder="You are not alone."/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="电子邮件"
|
||||
:required="false"
|
||||
>
|
||||
<a-input placeholder="exp@admin.com"/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="加密方式"
|
||||
:required="false"
|
||||
>
|
||||
<a-select defaultValue="aes-256-cfb">
|
||||
<a-select-option value="aes-256-cfb">aes-256-cfb</a-select-option>
|
||||
<a-select-option value="aes-128-cfb">aes-128-cfb</a-select-option>
|
||||
<a-select-option value="chacha20">chacha20</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="连接密码"
|
||||
:required="false"
|
||||
>
|
||||
<a-input placeholder="h3gSbecd"/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="登录密码"
|
||||
:required="false"
|
||||
>
|
||||
<a-input placeholder="密码"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary">提交</a-button>
|
||||
<a-button style="margin-left: 8px">保存</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
</a-col>
|
||||
<a-col :md="24" :lg="8" :style="{ minHeight: '180px' }">
|
||||
<div class="ant-upload-preview" @click="$refs.modal.edit(1)" >
|
||||
<a-icon type="cloud-upload-o" class="upload-icon"/>
|
||||
<div class="mask">
|
||||
<a-icon type="plus" />
|
||||
</div>
|
||||
<img :src="option.img"/>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
</a-row>
|
||||
|
||||
<avatar-modal ref="modal">
|
||||
|
||||
</avatar-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AvatarModal from './AvatarModal'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AvatarModal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// cropper
|
||||
preview: {},
|
||||
option: {
|
||||
img: '/avatar2.jpg',
|
||||
info: true,
|
||||
size: 1,
|
||||
outputType: 'jpeg',
|
||||
canScale: false,
|
||||
autoCrop: true,
|
||||
// 只有自动截图开启 宽度高度才生效
|
||||
autoCropWidth: 180,
|
||||
autoCropHeight: 180,
|
||||
fixedBox: true,
|
||||
// 开启宽度和高度比例
|
||||
fixed: true,
|
||||
fixedNumber: [1, 1]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.avatar-upload-wrapper {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-upload-preview {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
|
||||
.upload-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(222, 221, 221, 0.7);
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.mask {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.4);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 2rem;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -1rem;
|
||||
margin-top: -1rem;
|
||||
color: #d6d6d6;
|
||||
}
|
||||
}
|
||||
|
||||
img, .mask {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
cmdb-ui/src/views/account/settings/Binding.vue
Normal file
25
cmdb-ui/src/views/account/settings/Binding.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<a-list
|
||||
itemLayout="horizontal"
|
||||
:dataSource="data"
|
||||
>
|
||||
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
75
cmdb-ui/src/views/account/settings/Custom.vue
Normal file
75
cmdb-ui/src/views/account/settings/Custom.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import { colorList } from '@/components/SettingDrawer/settingConfig'
|
||||
import ASwitch from 'ant-design-vue/es/switch'
|
||||
import AList from 'ant-design-vue/es/list'
|
||||
import AListItem from 'ant-design-vue/es/list/Item'
|
||||
import { mixin } from '@/utils/mixin'
|
||||
|
||||
const Meta = AListItem.Meta
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AListItem,
|
||||
AList,
|
||||
ASwitch,
|
||||
Meta
|
||||
},
|
||||
mixins: [mixin],
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
themeFilter (theme) {
|
||||
const themeMap = {
|
||||
'dark': '暗色',
|
||||
'light': '白色'
|
||||
}
|
||||
return themeMap[theme]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
colorFilter (color) {
|
||||
const c = colorList.filter(o => o.color === color)[0]
|
||||
return c && c.key
|
||||
},
|
||||
|
||||
onChange (checked) {
|
||||
if (checked) {
|
||||
this.$store.dispatch('ToggleTheme', 'dark')
|
||||
} else {
|
||||
this.$store.dispatch('ToggleTheme', 'light')
|
||||
}
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return (
|
||||
<AList itemLayout="horizontal">
|
||||
<AListItem>
|
||||
<Meta>
|
||||
<a slot="title">风格配色</a>
|
||||
<span slot="description">
|
||||
整体风格配色设置
|
||||
</span>
|
||||
</Meta>
|
||||
<div slot="actions">
|
||||
<ASwitch checkedChildren="暗色" unCheckedChildren="白色" defaultChecked={this.navTheme === 'dark' && true || false} onChange={this.onChange} />
|
||||
</div>
|
||||
</AListItem>
|
||||
<AListItem>
|
||||
<Meta>
|
||||
<a slot="title">主题色</a>
|
||||
<span slot="description">
|
||||
页面风格配色: <a domPropsInnerHTML={ this.colorFilter(this.primaryColor) }/>
|
||||
</span>
|
||||
</Meta>
|
||||
</AListItem>
|
||||
</AList>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
155
cmdb-ui/src/views/account/settings/Index.vue
Normal file
155
cmdb-ui/src/views/account/settings/Index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="page-header-index-wide">
|
||||
<a-card :bordered="false" :bodyStyle="{ padding: '16px 0', height: '100%' }" :style="{ height: '100%' }">
|
||||
<div class="account-settings-info-main" :class="device">
|
||||
<div class="account-settings-info-left">
|
||||
<a-menu
|
||||
:mode="device == 'mobile' ? 'horizontal' : 'inline'"
|
||||
:style="{ border: '0', width: device == 'mobile' ? '560px' : 'auto'}"
|
||||
:selectedKeys="selectedKeys"
|
||||
type="inner"
|
||||
@openChange="onOpenChange"
|
||||
>
|
||||
<a-menu-item key="/account/settings/base">
|
||||
<router-link :to="{ name: 'BaseSettings' }">
|
||||
基本设置
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/account/settings/security">
|
||||
<router-link :to="{ name: 'SecuritySettings' }">
|
||||
安全设置
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/account/settings/custom">
|
||||
<router-link :to="{ name: 'CustomSettings' }">
|
||||
个性化
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/account/settings/binding">
|
||||
<router-link :to="{ name: 'BindingSettings' }">
|
||||
账户绑定
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/account/settings/notification">
|
||||
<router-link :to="{ name: 'NotificationSettings' }">
|
||||
新消息通知
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</div>
|
||||
<div class="account-settings-info-right">
|
||||
<div class="account-settings-info-title">
|
||||
<span>{{ $route.meta.title }}</span>
|
||||
</div>
|
||||
<route-view></route-view>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PageView, RouteView } from '@/layouts'
|
||||
import { mixinDevice } from '@/utils/mixin.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RouteView,
|
||||
PageView
|
||||
},
|
||||
mixins: [mixinDevice],
|
||||
data () {
|
||||
return {
|
||||
// horizontal inline
|
||||
mode: 'inline',
|
||||
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
|
||||
// cropper
|
||||
preview: {},
|
||||
option: {
|
||||
img: '/avatar2.jpg',
|
||||
info: true,
|
||||
size: 1,
|
||||
outputType: 'jpeg',
|
||||
canScale: false,
|
||||
autoCrop: true,
|
||||
// 只有自动截图开启 宽度高度才生效
|
||||
autoCropWidth: 180,
|
||||
autoCropHeight: 180,
|
||||
fixedBox: true,
|
||||
// 开启宽度和高度比例
|
||||
fixed: true,
|
||||
fixedNumber: [1, 1]
|
||||
},
|
||||
|
||||
pageTitle: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.updateMenu()
|
||||
},
|
||||
methods: {
|
||||
onOpenChange (openKeys) {
|
||||
this.openKeys = openKeys
|
||||
},
|
||||
updateMenu () {
|
||||
const routes = this.$route.matched.concat()
|
||||
this.selectedKeys = [ routes.pop().path ]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (val) {
|
||||
this.updateMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.account-settings-info-main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&.mobile {
|
||||
display: block;
|
||||
|
||||
.account-settings-info-left {
|
||||
border-right: unset;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.account-settings-info-right {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-settings-info-left {
|
||||
border-right: 1px solid #e8e8e8;
|
||||
width: 224px;
|
||||
}
|
||||
|
||||
.account-settings-info-right {
|
||||
flex: 1 1;
|
||||
padding: 8px 40px;
|
||||
|
||||
.account-settings-info-title {
|
||||
color: rgba(0,0,0,.85);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.account-settings-info-view {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
25
cmdb-ui/src/views/account/settings/Notification.vue
Normal file
25
cmdb-ui/src/views/account/settings/Notification.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<a-list
|
||||
itemLayout="horizontal"
|
||||
:dataSource="data"
|
||||
>
|
||||
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
41
cmdb-ui/src/views/account/settings/Security.vue
Normal file
41
cmdb-ui/src/views/account/settings/Security.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a-list
|
||||
itemLayout="horizontal"
|
||||
:dataSource="data"
|
||||
>
|
||||
<a-list-item slot="renderItem" slot-scope="item, index" :key="index">
|
||||
<a-list-item-meta>
|
||||
<a slot="title">{{ item.title }}</a>
|
||||
<span slot="description">
|
||||
<span class="security-list-description">{{ item.description }}</span>
|
||||
<span v-if="item.value"> : </span>
|
||||
<span class="security-list-value">{{ item.value }}</span>
|
||||
</span>
|
||||
</a-list-item-meta>
|
||||
<template v-if="item.actions">
|
||||
<a slot="actions" @click="item.actions.callback">{{ item.actions.title }}</a>
|
||||
</template>
|
||||
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: [
|
||||
{ title: '账户密码', description: '当前密码强度', value: '强', actions: { title: '修改', callback: () => { this.$message.info('This is a normal message') } } },
|
||||
{ title: '密保手机', description: '已绑定手机', value: '138****8293', actions: { title: '修改', callback: () => { this.$message.success('This is a message of success') } } },
|
||||
{ title: '密保问题', description: '未设置密保问题,密保问题可有效保护账户安全', value: '', actions: { title: '设置', callback: () => { this.$message.error('This is a message of error') } } },
|
||||
{ title: '备用邮箱', description: '已绑定邮箱', value: 'ant***sign.com', actions: { title: '修改', callback: () => { this.$message.warning('This is message of warning') } } },
|
||||
{ title: 'MFA 设备', description: '未绑定 MFA 设备,绑定后,可以进行二次确认', value: '', actions: { title: '绑定', callback: () => { this.$message.info('This is a normal message') } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
202
cmdb-ui/src/views/acl/module/addRoleRelationForm.vue
Normal file
202
cmdb-ui/src/views/acl/module/addRoleRelationForm.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleAddParent">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="角色列表"
|
||||
>
|
||||
<a-select name="otherID" :filterOption="filterOption" v-decorator="['otherID', {rules: [{ required: true, message: '请选择另一个角色'}]} ]">
|
||||
<template v-for="role in allRoles">
|
||||
<a-select-option v-if="role.id != current_record.id" :key="role.id">{{ role.name }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleAddParent" type="primary" style="margin-right: 1rem">关联父角色</a-button>
|
||||
<a-button @click="handleAddChild" type="primary" style="margin-right: 1rem">关联子角色</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { searchRole, addParentRole, addChildRole } from '@/api/acl/role'
|
||||
|
||||
export default {
|
||||
name: 'AddRoleRelationForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '角色关联',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
allRoles: [],
|
||||
current_record: null
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
handleAddRoleRelation (record) {
|
||||
this.current_record = record
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.getAllRoles()
|
||||
this.form.setFieldsValue({
|
||||
id: record.id
|
||||
})
|
||||
})
|
||||
},
|
||||
handleAddParent (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.addParent(values.id, values.otherID)
|
||||
}
|
||||
})
|
||||
},
|
||||
handleAddChild (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.addChild(values.id, values.otherID)
|
||||
}
|
||||
})
|
||||
},
|
||||
getAllRoles () {
|
||||
searchRole({ page_size: 9999, app_id: this.$store.state.app.name }).then(res => {
|
||||
this.allRoles = res.roles
|
||||
})
|
||||
},
|
||||
addParent (id, otherID) {
|
||||
addParentRole(id, otherID)
|
||||
.then(res => {
|
||||
this.$message.success(`关联父角色成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
addChild (id, otherID) {
|
||||
addChildRole(id, otherID)
|
||||
.then(res => {
|
||||
this.$message.success(`关联子角色成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
235
cmdb-ui/src/views/acl/module/permissionForm.vue
Normal file
235
cmdb-ui/src/views/acl/module/permissionForm.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="类型名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder=""
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入资源名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="描述"
|
||||
>
|
||||
<a-textarea placeholder="请输入描述信息..." name="description" :rows="4" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="权限"
|
||||
>
|
||||
<div :style="{ borderBottom: '1px solid #E9E9E9' }">
|
||||
<a-checkbox :indeterminate="indeterminate" @change="onCheckAllChange" :checked="checkAll">
|
||||
全选
|
||||
</a-checkbox>
|
||||
</div>
|
||||
<br />
|
||||
<a-checkbox-group :options="plainOptions" v-model="perms" @change="onPermChange" />
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { addResourceType, updateResourceTypeById } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'ResourceForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增资源类型',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
perms: ['1'],
|
||||
indeterminate: true,
|
||||
checkAll: false,
|
||||
plainOptions: ['1', '2']
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
onPermChange (perms) {
|
||||
this.indeterminate = !!perms.length && perms.length < this.plainOptions.length
|
||||
this.checkAll = perms.length === this.plainOptions.length
|
||||
},
|
||||
onCheckAllChange (e) {
|
||||
Object.assign(this, {
|
||||
perms: e.target.checked ? this.plainOptions : [],
|
||||
indeterminate: false,
|
||||
checkAll: e.target.checked
|
||||
})
|
||||
},
|
||||
handleCreate () {
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
description: record.description
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
values.app_id = this.$route.name.split('_')[0]
|
||||
values.perms = this.perms
|
||||
if (values.id) {
|
||||
this.updateResourceType(values.id, values)
|
||||
} else {
|
||||
this.createResourceType(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateResourceType (id, data) {
|
||||
updateResourceTypeById(id, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createResourceType (data) {
|
||||
addResourceType(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
191
cmdb-ui/src/views/acl/module/resourceForm.vue
Normal file
191
cmdb-ui/src/views/acl/module/resourceForm.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="资源名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder=""
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入资源名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="资源类型"
|
||||
>
|
||||
<a-select name="type_id" v-decorator="['type_id', {rules: []} ]">
|
||||
<a-select-option v-for="type in allTypes" :key="type.id">{{ type.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { addResource, searchResourceType } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'ResourceForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增资源',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
allTypes: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
created () {
|
||||
this.getAllResourceTypes()
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
getAllResourceTypes () {
|
||||
searchResourceType({ page_size: 9999, app_id: this.$route.name.split('_')[0] }).then(res => {
|
||||
this.allTypes = res.groups
|
||||
})
|
||||
},
|
||||
handleCreate (defaultType) {
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({ type_id: defaultType.id })
|
||||
})
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
this.$emit('fresh')
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
values.app_id = this.$route.name.split('_')[0]
|
||||
if (values.id) {
|
||||
this.updateResource(values.id, values)
|
||||
} else {
|
||||
this.createResource(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
createResource (data) {
|
||||
addResource(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
123
cmdb-ui/src/views/acl/module/resourcePermForm.vue
Normal file
123
cmdb-ui/src/views/acl/module/resourcePermForm.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="drawerTitle"
|
||||
v-model="drawerVisible"
|
||||
width="50%"
|
||||
>
|
||||
<template slot="footer">
|
||||
<a-button key="back" @click="handleCancel">关闭</a-button>
|
||||
</template>
|
||||
|
||||
<template>
|
||||
<a-list itemLayout="horizontal">
|
||||
<a-list-item v-for="item in resPerms" :key="item[0]">
|
||||
<span>{{ item[0] }}: </span>
|
||||
<div>
|
||||
<a-tag
|
||||
closable
|
||||
color="cyan"
|
||||
v-for="perm in item[1]"
|
||||
:key="perm.name"
|
||||
@close="deletePerm(perm.rid, perm.name)">
|
||||
{{ perm.name }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { getResourceTypePerms, getResourcePerms, deleteRoleResourcePerm } from '@/api/acl/permission'
|
||||
|
||||
export default {
|
||||
name: 'ResourceForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '权限列表',
|
||||
drawerVisible: false,
|
||||
record: null,
|
||||
allPerms: [],
|
||||
resPerms: [],
|
||||
roleID: null,
|
||||
childrenDrawer: false,
|
||||
allRoles: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
methods: {
|
||||
handlePerm (record) {
|
||||
this.drawerVisible = true
|
||||
this.record = record
|
||||
this.getResPerms(record.id)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.getAllPerms(record.resource_type_id)
|
||||
})
|
||||
},
|
||||
getResPerms (resId) {
|
||||
getResourcePerms(resId).then(res => {
|
||||
var perms = []
|
||||
for (var key in res) {
|
||||
perms.push([key, res[key]])
|
||||
}
|
||||
this.resPerms = perms
|
||||
})
|
||||
},
|
||||
getAllPerms (resTypeId) {
|
||||
getResourceTypePerms(resTypeId).then(res => {
|
||||
this.allPerms = res
|
||||
})
|
||||
},
|
||||
deletePerm (roleID, permName) {
|
||||
deleteRoleResourcePerm(roleID, this.record.id, { perms: [permName] }).then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleCancel (e) {
|
||||
this.drawerVisible = false
|
||||
},
|
||||
requestFailed (err) {
|
||||
console.log(err)
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
120
cmdb-ui/src/views/acl/module/resourcePermManageForm.vue
Normal file
120
cmdb-ui/src/views/acl/module/resourcePermManageForm.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:title="'添加授权:'+instance.name"
|
||||
width="30%"
|
||||
:closable="true"
|
||||
:visible="visible"
|
||||
@close="closeForm"
|
||||
>
|
||||
<a-form :form="form">
|
||||
<a-form-item
|
||||
label="角色列表"
|
||||
>
|
||||
<a-select
|
||||
showSearch
|
||||
name="roleIdList"
|
||||
v-decorator="['roleIdList', {rules: []} ]"
|
||||
mode="multiple"
|
||||
placeholder="请选择角色名称,可多选!"
|
||||
:filterOption="filterOption">
|
||||
<a-select-option v-for="role in allRoles" :key="role.id">{{ role.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="权限列表"
|
||||
>
|
||||
<a-select name="permName" v-decorator="['permName', {rules: []} ]" mode="multiple" placeholder="请选择权限,可多选!">
|
||||
<a-select-option v-for="perm in allPerms" :key="perm.name">{{ perm.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div class="btn-group">
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">添加</a-button>
|
||||
<a-button @click="closeForm">取消</a-button>
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script>
|
||||
import { searchRole } from '@/api/acl/role'
|
||||
import { getResourceTypePerms, setRoleResourcePerm } from '@/api/acl/permission'
|
||||
|
||||
export default {
|
||||
name: 'ResourcePermManageForm',
|
||||
data () {
|
||||
return {
|
||||
allRoles: [],
|
||||
allPerms: [],
|
||||
visible: false,
|
||||
instance: {} // 当前对象
|
||||
}
|
||||
},
|
||||
props: {
|
||||
groupTypeMessage: {
|
||||
required: true,
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadRoles()
|
||||
},
|
||||
methods: {
|
||||
loadRoles () {
|
||||
searchRole({ page_size: 9999, app_id: this.$route.name.split('_')[0], user_role: 1 }).then(res => {
|
||||
this.allRoles = res.roles
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
loadPerm (resourceTypeId) {
|
||||
getResourceTypePerms(resourceTypeId).then(res => {
|
||||
this.allPerms = res
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
closeForm () {
|
||||
this.visible = false
|
||||
this.form.resetFields()
|
||||
},
|
||||
editPerm (record) {
|
||||
this.visible = true
|
||||
this.instance = record
|
||||
this.loadPerm(record['resource_type_id'])
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
values.roleIdList.forEach(roleId => {
|
||||
setRoleResourcePerm(roleId, this.instance.id, { perms: values.permName }).then(
|
||||
res => { this.$message.info('添加授权成功') }).catch(
|
||||
err => this.requestFailed(err))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.btn-group {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e9e9e9;
|
||||
padding: 0.8rem 1rem;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
217
cmdb-ui/src/views/acl/module/resourceTypeForm.vue
Normal file
217
cmdb-ui/src/views/acl/module/resourceTypeForm.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="类型名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder="类型名称"
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入类型名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="描述"
|
||||
>
|
||||
<a-textarea placeholder="请输入描述信息..." name="description" :rows="4" v-decorator="['description', {rules: []} ]"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="权限"
|
||||
>
|
||||
<a-select mode="tags" v-model="perms" style="width: 100%" placeholder="请输入权限名...">
|
||||
</a-select>
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { addResourceType, updateResourceTypeById } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'ResourceForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增资源类型',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
perms: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
handleCreate () {
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.perms = []
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerVisible = true
|
||||
this.perms = record.perms
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
description: record.description
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
values.app_id = this.$route.name.split('_')[0]
|
||||
values.perms = this.perms
|
||||
if (values.id) {
|
||||
this.updateResourceType(values.id, values)
|
||||
} else {
|
||||
this.createResourceType(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateResourceType (id, data) {
|
||||
updateResourceTypeById(id, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createResourceType (data) {
|
||||
addResourceType(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
258
cmdb-ui/src/views/acl/module/roleForm.vue
Normal file
258
cmdb-ui/src/views/acl/module/roleForm.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="{span:6}"
|
||||
:wrapper-col="{span:12}"
|
||||
label="角色名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder=""
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入角色名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="{span:6}"
|
||||
:wrapper-col="{span:12}"
|
||||
label="继承自"
|
||||
>
|
||||
<a-select
|
||||
v-model="selectedParents"
|
||||
placeholder="可选择继承角色"
|
||||
mode="multiple"
|
||||
:filterOption="filterOption">
|
||||
<template v-for="role in allRoles">
|
||||
<a-select-option v-if="current_id !== role.id" :key="role.id">{{ role.name }}</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="{span:8}"
|
||||
:wrapper-col="{span:10}"
|
||||
label="是否应用管理员"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_app_admin"
|
||||
v-decorator="['is_app_admin', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { addRole, updateRoleById, addParentRole, delParentRole } from '@/api/acl/role'
|
||||
|
||||
export default {
|
||||
name: 'RoleForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增角色',
|
||||
current_id: 0,
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
selectedParents: [],
|
||||
oldParents: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 8 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
},
|
||||
handleCreate () {
|
||||
this.drawerTitle = '新增'
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.selectedParents = []
|
||||
this.oldParents = []
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerTitle = '编辑'
|
||||
this.drawerVisible = true
|
||||
this.current_id = record.id
|
||||
const _parents = this.id2parents[record.id]
|
||||
if (_parents) {
|
||||
_parents.forEach(item => {
|
||||
this.selectedParents.push(item.id)
|
||||
this.oldParents.push(item.id)
|
||||
})
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
is_app_admin: record.is_app_admin
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
values.app_id = this.$route.name.split('_')[0]
|
||||
if (values.id) {
|
||||
this.updateRole(values.id, values)
|
||||
} else {
|
||||
this.createRole(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRole (id, data) {
|
||||
this.updateParents(id)
|
||||
updateRoleById(id, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createRole (data) {
|
||||
addRole(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.updateParents(res.id)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
updateParents (id) {
|
||||
this.oldParents.forEach(item => {
|
||||
if (!this.selectedParents.includes(item)) {
|
||||
delParentRole(id, item).catch(err => this.requestFailed(err))
|
||||
}
|
||||
})
|
||||
this.selectedParents.forEach(item => {
|
||||
if (!this.oldParents.includes(item)) {
|
||||
addParentRole(id, item).catch(err => this.requestFailed(err))
|
||||
}
|
||||
})
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
allRoles: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
id2parents: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
293
cmdb-ui/src/views/acl/module/userForm.vue
Normal file
293
cmdb-ui/src/views/acl/module/userForm.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="用户名(英文)"
|
||||
>
|
||||
<a-input
|
||||
name="username"
|
||||
placeholder="英文名"
|
||||
v-decorator="['username', {rules: [{ required: true, message: '请输入用户名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="中文名"
|
||||
>
|
||||
<a-input
|
||||
name="nickname"
|
||||
v-decorator="['nickname', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="密码"
|
||||
>
|
||||
<a-input
|
||||
type="password"
|
||||
name="password"
|
||||
v-decorator="['password', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="部门"
|
||||
>
|
||||
<a-input
|
||||
name="department"
|
||||
v-decorator="['department', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="小组"
|
||||
>
|
||||
<a-input
|
||||
name="catalog"
|
||||
v-decorator="['catalog', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="邮箱"
|
||||
>
|
||||
<a-input
|
||||
name="email"
|
||||
v-decorator="[
|
||||
'email',
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱!',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
message: '请输入邮箱',
|
||||
},
|
||||
],
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="手机号码"
|
||||
>
|
||||
<a-input
|
||||
name="mobile"
|
||||
v-decorator="['mobile', {rules: [{message: '请输入正确的手机号码', pattern: /^1\d{10}$/ }]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否锁定"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="block"
|
||||
v-decorator="['block', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { addUser, updateUserById } from '@/api/acl/user'
|
||||
|
||||
export default {
|
||||
name: 'AttributeForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增用户',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical'
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
|
||||
handleCreate () {
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.uid,
|
||||
username: record.username,
|
||||
nickname: record.nickname,
|
||||
password: record.password,
|
||||
department: record.department,
|
||||
catalog: record.catalog,
|
||||
email: record.email,
|
||||
mobile: record.mobile,
|
||||
block: record.block
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
if (values.id) {
|
||||
this.updateUser(values.id, values)
|
||||
} else {
|
||||
this.createUser(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateUser (attrId, data) {
|
||||
updateUserById(attrId, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createUser (data) {
|
||||
addUser(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
309
cmdb-ui/src/views/acl/permissions.vue
Normal file
309
cmdb-ui/src/views/acl/permissions.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="description" slot-scope="text">{{ text }}</template>
|
||||
|
||||
<span slot="id" slot-scope="key">
|
||||
<a-tag color="cyan" v-for="perm in id2perms[key]" :key="perm.id">{{ perm.name }}</a-tag>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<resourceTypeForm ref="resourceTypeForm" :handleOk="handleOk"> </resourceTypeForm>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import resourceTypeForm from './module/resourceTypeForm'
|
||||
import { deleteResourceTypeById, searchResourceType } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
resourceTypeForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
id2perms: {},
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增资源类型',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '类型名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
width: 250,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'description' }
|
||||
},
|
||||
{
|
||||
title: '权限',
|
||||
dataIndex: 'id',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'id' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter.app_id = this.$route.name.split('_')[0]
|
||||
parameter.page = parameter.pageNo
|
||||
parameter.page_size = parameter.pageSize
|
||||
delete parameter.pageNo
|
||||
delete parameter.pageSize
|
||||
Object.assign(parameter, this.queryParam)
|
||||
|
||||
return searchResourceType(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.groups
|
||||
this.id2perms = res.id2perms
|
||||
return res
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.$refs.resourceTypeForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteResourceType(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.resourceTypeForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteResourceType (id) {
|
||||
deleteResourceTypeById(id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
cancel () {
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
317
cmdb-ui/src/views/acl/resource_types.vue
Normal file
317
cmdb-ui/src/views/acl/resource_types.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="description" slot-scope="text">{{ text }}</template>
|
||||
|
||||
<span slot="id" slot-scope="key">
|
||||
<a-tag color="cyan" v-for="perm in id2perms[key]" :key="perm.id">{{ perm.name }}</a-tag>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<resourceTypeForm ref="resourceTypeForm" :handleOk="handleOk"> </resourceTypeForm>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import resourceTypeForm from './module/resourceTypeForm'
|
||||
import { deleteResourceTypeById, searchResourceType } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
resourceTypeForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
id2perms: {},
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增资源类型',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '类型名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 150,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
width: 200,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'description' }
|
||||
},
|
||||
{
|
||||
title: '权限',
|
||||
dataIndex: 'id',
|
||||
width: 300,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'id' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter.app_id = this.$route.name.split('_')[0]
|
||||
parameter.page = parameter.pageNo
|
||||
parameter.page_size = parameter.pageSize
|
||||
delete parameter.pageNo
|
||||
delete parameter.pageSize
|
||||
Object.assign(parameter, this.queryParam)
|
||||
|
||||
return searchResourceType(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.groups
|
||||
this.id2perms = res.id2perms
|
||||
return res
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
var perms = []
|
||||
var permList = this.id2perms[record.id]
|
||||
if (permList) {
|
||||
for (var i = 0; i < permList.length; i++) {
|
||||
perms.push(permList[i].name)
|
||||
}
|
||||
}
|
||||
record.perms = perms
|
||||
this.$refs.resourceTypeForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteResourceType(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.resourceTypeForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteResourceType (id) {
|
||||
deleteResourceTypeById(id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
cancel () {
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
339
cmdb-ui/src/views/acl/resources.vue
Normal file
339
cmdb-ui/src/views/acl/resources.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<div>
|
||||
<a-list :grid="{ gutter: 12, column: 12 }" style="height: 40px;clear: both">
|
||||
<a-list-item
|
||||
v-for="rtype in allResourceTypes"
|
||||
:key="rtype.id"
|
||||
:class="{'bottom-border':currentType.name===rtype.name}"
|
||||
style="text-align: center;height: 30px; margin:0 30px"
|
||||
>
|
||||
<a
|
||||
@click="loadCurrentType(rtype)"
|
||||
:style="currentType.name === rtype.name?'color:#108ee9':'color:grey'">
|
||||
<span style="font-size: 18px">{{ rtype.name }}</span>
|
||||
</a>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
<a-divider style="margin-top: -16px" />
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handlePerm(record)">查看授权</a>
|
||||
<a-divider type="vertical"/>
|
||||
<a @click="handlePermManage(record)">授权</a>
|
||||
<a-divider type="vertical"/>
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<resourceForm ref="resourceForm" @fresh="handleOk"> </resourceForm>
|
||||
<resourcePermForm ref="resourcePermForm"> </resourcePermForm>
|
||||
<ResourcePermManageForm ref="resourcePermManageForm" :groupTypeMessage="currentType"></ResourcePermManageForm>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import resourceForm from './module/resourceForm'
|
||||
import resourcePermForm from './module/resourcePermForm'
|
||||
import ResourcePermManageForm from './module/resourcePermManageForm'
|
||||
import { deleteResourceById, searchResource, searchResourceType } from '@/api/acl/resource'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
resourceForm,
|
||||
resourcePermForm,
|
||||
ResourcePermManageForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增资源',
|
||||
allResourceTypes: [],
|
||||
currentType: { id: 0 },
|
||||
formLayout: 'vertical',
|
||||
|
||||
allResources: [],
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '资源名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 250,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
width: 200,
|
||||
dataIndex: 'created_at'
|
||||
},
|
||||
{
|
||||
title: '最后修改时间',
|
||||
width: 200,
|
||||
dataIndex: 'updated_at'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter.app_id = this.$route.name.split('_')[0]
|
||||
parameter.page = parameter.pageNo
|
||||
parameter.page_size = parameter.pageSize
|
||||
parameter.resource_type_id = this.currentType.id
|
||||
delete parameter.pageNo
|
||||
delete parameter.pageSize
|
||||
Object.assign(parameter, this.queryParam)
|
||||
return searchResource(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.resources
|
||||
this.allResources = res.resources
|
||||
return res
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
this.getAllResourceTypes()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
getAllResourceTypes () {
|
||||
searchResourceType({ page_size: 9999, app_id: this.$route.name.split('_')[0] }).then(res => {
|
||||
this.allResourceTypes = res.groups
|
||||
this.loadCurrentType(res.groups[0])
|
||||
})
|
||||
},
|
||||
handlePermManage (record) {
|
||||
this.$refs.resourcePermManageForm.editPerm(record)
|
||||
},
|
||||
loadCurrentType (rtype) {
|
||||
if (rtype) {
|
||||
this.currentType = rtype
|
||||
}
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
handlePerm (record) {
|
||||
this.$refs.resourcePermForm.handlePerm(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteResource(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.resourceForm.handleCreate(this.currentType)
|
||||
},
|
||||
|
||||
deleteResource (id) {
|
||||
deleteResourceById(id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
cancel () {
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
.bottom-border {
|
||||
border-bottom: cornflowerblue 2px solid;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
310
cmdb-ui/src/views/acl/roles.vue
Normal file
310
cmdb-ui/src/views/acl/roles.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<span slot="is_app_admin" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="inherit" slot-scope="key">
|
||||
<a-tag color="cyan" v-for="role in id2parents[key]" :key="role.id">{{ role.name }}</a-tag>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">修改</a>
|
||||
<a-divider type="vertical"/>
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
</s-table>
|
||||
<roleForm ref="roleForm" :allRoles="allRoles" :id2parents="id2parents" :handleOk="handleOk"></roleForm>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import roleForm from './module/roleForm'
|
||||
import { deleteRoleById, searchRole } from '@/api/acl/role'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
roleForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增角色',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
allRoles: [],
|
||||
id2parents: {},
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '角色名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 150,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '是否管理员',
|
||||
dataIndex: 'is_app_admin',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_app_admin' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '继承自',
|
||||
dataIndex: 'id',
|
||||
sorter: false,
|
||||
width: 250,
|
||||
scopedSlots: { customRender: 'inherit' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter.app_id = this.$route.name.split('_')[0]
|
||||
parameter.page = parameter.pageNo
|
||||
parameter.page_size = parameter.pageSize
|
||||
delete parameter.pageNo
|
||||
delete parameter.pageSize
|
||||
Object.assign(parameter, this.queryParam)
|
||||
|
||||
return searchRole(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.roles
|
||||
|
||||
this.allRoles = res.roles
|
||||
this.id2parents = res.id2parents
|
||||
return res
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleAddRoleRelation (record) {
|
||||
this.$refs.AddRoleRelationForm.handleAddRoleRelation(record)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.$refs.roleForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteRole(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.roleForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteRole (id) {
|
||||
deleteRoleById(id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
cancel (e) {
|
||||
return false
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
361
cmdb-ui/src/views/acl/users.vue
Normal file
361
cmdb-ui/src/views/acl/users.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.uid"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="usernameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.username})|(?=${columnSearchText.username})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="nicknameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.alias">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.nickname})|(?=${columnSearchText.nickname})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.alias.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
<span slot="block" slot-scope="text">
|
||||
<a-icon type="lock" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import userForm from './module/userForm'
|
||||
import { deleteUserById, searchUser } from '@/api/acl/user'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
userForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scroll: { x: 1300, y: 500 },
|
||||
btnName: '新增用户',
|
||||
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
allUsers: [],
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
sorter: false,
|
||||
width: 150,
|
||||
scopedSlots: {
|
||||
customRender: 'usernameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.username && record.username.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '中文名',
|
||||
dataIndex: 'nickname',
|
||||
sorter: false,
|
||||
width: 150,
|
||||
scopedSlots: {
|
||||
customRender: 'nicknameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.nickname && record.nickname.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'department',
|
||||
width: 100,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'department' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '小组',
|
||||
dataIndex: 'catalog',
|
||||
sorter: false,
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'catalog' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'email' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '手机',
|
||||
dataIndex: 'mobile',
|
||||
sorter: false,
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'mobile' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '加入时间',
|
||||
dataIndex: 'date_joined',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'date_joined' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '锁定',
|
||||
dataIndex: 'block',
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'block' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter.page = parameter.pageNo
|
||||
parameter.page_size = parameter.pageSize
|
||||
delete parameter.pageNo
|
||||
delete parameter.pageSize
|
||||
Object.assign(parameter, this.queryParam)
|
||||
|
||||
return searchUser(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.users
|
||||
|
||||
this.allUsers = res.users
|
||||
return res
|
||||
})
|
||||
},
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
},
|
||||
cancel () {
|
||||
return false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.$refs.userForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteUser(record.uid)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.userForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteUser (attrId) {
|
||||
deleteUserById(attrId)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
overflow: auto;
|
||||
}
|
||||
.ant-table-content{
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
85
cmdb-ui/src/views/cmdb/batch/index.vue
Normal file
85
cmdb-ui/src/views/cmdb/batch/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="title">
|
||||
<ci-type-choice @getCiTypeAttr="showCiType">
|
||||
</ci-type-choice>
|
||||
</div>
|
||||
<a-row>
|
||||
<a-col :span="18">
|
||||
<a-card style="height: 605px">
|
||||
<a-button class="ant-btn-primary" style="margin-left: 10px;" :disabled="uploadFlag" id="upload-button" @click="uploadData">上传</a-button>
|
||||
<upload-file-form v-if="displayUpload" ref="fileEditor"></upload-file-form>
|
||||
<ci-table v-if="editorOnline" :ciTypeAttrs="ciTypeAttrs" ref="onlineEditor"></ci-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div style="min-height: 604px; background: white">
|
||||
<a-card title="上传结果">
|
||||
<upload-result v-if="beginLoad" :upLoadData="needDataList" :ciType="ciType" :unique-field="uniqueField"></upload-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CiTypeChoice from './modules/CiTypeChoice'
|
||||
import CiTable from './modules/CiTable'
|
||||
import UploadFileForm from './modules/UploadFileForm'
|
||||
import UploadResult from './modules/UploadResult'
|
||||
import { filterNull } from '@/api/cmdb/batch'
|
||||
|
||||
export default {
|
||||
name: 'Batch',
|
||||
components: {
|
||||
CiTypeChoice,
|
||||
CiTable,
|
||||
UploadFileForm,
|
||||
UploadResult
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editorOnline: false,
|
||||
uploadFlag: true,
|
||||
ciTypeAttrs: [],
|
||||
needDataList: [],
|
||||
ciType: -1,
|
||||
uniqueField: '',
|
||||
uniqueId: 0,
|
||||
beginLoad: false,
|
||||
displayUpload: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showCiType (message) {
|
||||
this.ciTypeAttrs = message
|
||||
this.ciType = message.type_id
|
||||
this.uniqueField = message.unique
|
||||
this.uniqueId = message.unique_id
|
||||
this.editorOnline = false
|
||||
this.$nextTick(() => {
|
||||
this.editorOnline = true
|
||||
})
|
||||
},
|
||||
uploadData () {
|
||||
if (this.ciType < 0) {
|
||||
alert('尚未选择模板类型!')
|
||||
return
|
||||
}
|
||||
this.beginLoad = false
|
||||
const fileData = this.$refs.fileEditor.dataList
|
||||
if (fileData.length > 0) {
|
||||
this.needDataList = filterNull(fileData)
|
||||
} else {
|
||||
this.needDataList = filterNull(this.$refs.onlineEditor.getDataList())
|
||||
}
|
||||
this.displayUpload = false
|
||||
this.$nextTick(() => {
|
||||
this.beginLoad = true
|
||||
this.displayUpload = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
95
cmdb-ui/src/views/cmdb/batch/modules/CiTable.vue
Normal file
95
cmdb-ui/src/views/cmdb/batch/modules/CiTable.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="hotTable" class="hotTable" style="overflow: hidden; height:275px">
|
||||
<HotTable :root="root" ref="HTable" :settings="hotSettings"></HotTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { HotTable } from '@handsontable-pro/vue'
|
||||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
HotTable
|
||||
},
|
||||
props: { ciTypeAttrs: { type: Object, required: true } },
|
||||
data: function () {
|
||||
return {
|
||||
root: 'test-hot'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hotSettings () {
|
||||
const whiteColumn = []
|
||||
const aliasList = []
|
||||
this.$props.ciTypeAttrs.attributes.forEach(item => {
|
||||
aliasList.push(item.alias)
|
||||
whiteColumn.push('')
|
||||
})
|
||||
const dt = {
|
||||
data: [whiteColumn],
|
||||
startRows: 11,
|
||||
startCols: 6,
|
||||
minRows: 5,
|
||||
minCols: 1,
|
||||
maxRows: 90,
|
||||
maxCols: 90,
|
||||
rowHeaders: true,
|
||||
// minSpareCols: 2, //列留白
|
||||
colHeaders: aliasList,
|
||||
minSpareRows: 2, // 行留白
|
||||
// autoWrapRow: true, // 自动换行
|
||||
// 自定义右键菜单,可汉化,默认布尔值
|
||||
contextMenu: {
|
||||
items: {
|
||||
row_above: {
|
||||
name: '上方插入一行'
|
||||
},
|
||||
row_below: {
|
||||
name: '下方插入一行'
|
||||
},
|
||||
moverow: {
|
||||
name: '删除行'
|
||||
},
|
||||
unfreeze_column: {
|
||||
name: '取消列固定'
|
||||
},
|
||||
hsep1: '---------', // 提供分隔线
|
||||
hsep2: '---------'
|
||||
}
|
||||
},
|
||||
// width: '100%',
|
||||
// fillHandle: true, // 选中拖拽复制 possible values: true, false, "horizontal", "vertical"
|
||||
fixedColumnsLeft: 0, // 固定左边列数
|
||||
fixedRowsTop: 0, // 固定上边列数
|
||||
manualColumnFreeze: true, // 手动固定列
|
||||
// manualColumnMove: true, // 手动移动列
|
||||
// manualRowMove: true, // 手动移动行
|
||||
// manualColumnResize: true, // 手工更改列距
|
||||
// manualRowResize: true, // 手动更改行距
|
||||
comments: true, // 添加注释
|
||||
customBorders: [], // 添加边框
|
||||
columnSorting: true, // 排序
|
||||
stretchH: 'all', // 根据宽度横向扩展,last:只扩展最后一列,none:默认不扩展
|
||||
afterChange: function (changes, source) {
|
||||
if (changes !== null) {
|
||||
document.getElementById('upload-button').disabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return dt
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getDataList () {
|
||||
const data = this.$refs.HTable.$data.hotInstance.getData()
|
||||
data.unshift(this.$refs.HTable.$data.hotInstance.getColHeader())
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import '~handsontable/dist/handsontable.full.css';
|
||||
</style>
|
||||
113
cmdb-ui/src/views/cmdb/batch/modules/CiTypeChoice.vue
Normal file
113
cmdb-ui/src/views/cmdb/batch/modules/CiTypeChoice.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-form :form="form" style="max-width: 500px; margin: 30px auto 0;">
|
||||
<a-row>
|
||||
<a-col :span="18">
|
||||
<a-form-item label="模板类型" :labelCol="labelCol" :wrapperCol="wrapperCol">
|
||||
<a-select
|
||||
placeholder="--请选择模板类型--"
|
||||
v-decorator="['ciTypes', { rules: [{required: true, message: '模板类型必须选择'}] }]"
|
||||
@change="selectCiType"
|
||||
>
|
||||
<a-select-option v-for="ciType in ciTypeList" :key="ciType.name" :value="ciType.id">{{ ciType.alias }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item>
|
||||
<a-button
|
||||
style="margin-left: 20px"
|
||||
:disabled="downLoadButtonDis"
|
||||
@click="downLoadExcel"
|
||||
>下载模板</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<a-divider />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCITypes } from '@/api/cmdb/CIType'
|
||||
import { getCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
|
||||
import { writeExcel } from '@/api/cmdb/batch'
|
||||
|
||||
export default {
|
||||
name: 'CiTypeChoice',
|
||||
data () {
|
||||
return {
|
||||
labelCol: { lg: { span: 5 }, sm: { span: 5 } },
|
||||
wrapperCol: { lg: { span: 19 }, sm: { span: 19 } },
|
||||
form: this.$form.createForm(this),
|
||||
ciTypeList: [],
|
||||
ciTypeName: '',
|
||||
downLoadButtonDis: true,
|
||||
selectNum: 0,
|
||||
selectCiTypeAttrList: []
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
getCITypes().then(res => {
|
||||
this.ciTypeList = res.ci_types
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
selectCiType (el) {
|
||||
// 当选择好模板类型时的回调函数
|
||||
this.downLoadButtonDis = false
|
||||
this.selectNum = el
|
||||
getCITypeAttributesById(el).then(res => {
|
||||
this.$emit('getCiTypeAttr', res)
|
||||
this.selectCiTypeAttrList = res
|
||||
})
|
||||
|
||||
this.ciTypeList.forEach(item => {
|
||||
if (this.selectNum === item.id) {
|
||||
this.ciTypeName = item.alias || item.name
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
downLoadExcel () {
|
||||
const columns = []
|
||||
this.selectCiTypeAttrList.attributes.forEach(item => {
|
||||
columns.push(item.alias)
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.step-form-style-desc {
|
||||
padding: 0 56px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
h3 {
|
||||
margin: 0 0 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0 0 4px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
cmdb-ui/src/views/cmdb/batch/modules/UploadFileForm.vue
Normal file
61
cmdb-ui/src/views/cmdb/batch/modules/UploadFileForm.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-form :form="form" style="max-width: 500px; margin: 40px auto 0;">
|
||||
<a-upload-dragger ref="upload" :multiple="true" :customRequest="customRequest" accept=".xls">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<a-icon type="inbox" />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽文件至此上传!</p>
|
||||
<p class="ant-upload-hint">支持文件类型:xls</p>
|
||||
</a-upload-dragger>
|
||||
</a-form>
|
||||
<a-divider>or</a-divider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { processFile } from '@/api/cmdb/batch'
|
||||
|
||||
export default {
|
||||
name: 'Step2',
|
||||
data () {
|
||||
return {
|
||||
labelCol: { lg: { span: 5 }, sm: { span: 5 } },
|
||||
wrapperCol: { lg: { span: 19 }, sm: { span: 19 } },
|
||||
form: this.$form.createForm(this),
|
||||
loading: false,
|
||||
timer: 0,
|
||||
ciItemNum: 0,
|
||||
dataList: []
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
customRequest (data) {
|
||||
processFile(data.file).then(res => {
|
||||
this.ciItemNum = res.length - 1
|
||||
document.getElementById('upload-button').disabled = false
|
||||
this.dataList = res
|
||||
})
|
||||
},
|
||||
|
||||
handleChange (info) {
|
||||
document.getElementById('load-button').disabled = false
|
||||
console.log(info)
|
||||
},
|
||||
clear () {
|
||||
console.log(this.$refs.upload.$children[0].onSuccess('', ''))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.stepFormText {
|
||||
margin-bottom: 24px;
|
||||
.ant-form-item-label,
|
||||
.ant-form-item-control {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
cmdb-ui/src/views/cmdb/batch/modules/UploadResult.vue
Normal file
95
cmdb-ui/src/views/cmdb/batch/modules/UploadResult.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4>共 <span style="color: blue">{{ total }}</span> 条,已完成 <span style="color: lightgreen">{{ complete }}</span> 条
|
||||
,失败 <span style="color: red">{{ errorNum }} </span>条</h4>
|
||||
|
||||
<a-progress :percent="mPercent"/>
|
||||
<div class="my-box">
|
||||
<span>错误信息:</span>
|
||||
<ol>
|
||||
<li :key="item" v-for="item in errorItems">{{ item }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uploadData } from '@/api/cmdb/batch'
|
||||
|
||||
export default {
|
||||
name: 'Result',
|
||||
props: {
|
||||
upLoadData: {
|
||||
required: true,
|
||||
type: Array
|
||||
},
|
||||
ciType: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
uniqueField: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
total: 0,
|
||||
complete: 0,
|
||||
errorNum: 0,
|
||||
errorItems: []
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
document.getElementById('upload-button').disabled = true
|
||||
this.upload2Server()
|
||||
},
|
||||
computed: {
|
||||
mpercent () {
|
||||
return Math.round(this.complete / this.total * 10000) / 100
|
||||
},
|
||||
progressStatus () {
|
||||
if (this.complete === this.total) {
|
||||
return null
|
||||
} else {
|
||||
return 'active'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
upload2Server () {
|
||||
this.total = this.$props.upLoadData.length - 1
|
||||
for (let i = 0; i < this.total; i++) {
|
||||
const item = {}
|
||||
let itemUniqueName = 'unknown'
|
||||
for (let j = 0; j < this.$props.upLoadData[0].length; j++) {
|
||||
item[this.$props.upLoadData[0][j]] = this.$props.upLoadData[i + 1][j]
|
||||
if (this.$props.upLoadData[0][j] === this.$props.uniqueField) {
|
||||
itemUniqueName = this.$props.upLoadData[i + 1][j] || 'unknown'
|
||||
}
|
||||
}
|
||||
uploadData(this.$props.ciType, item).then(res => {
|
||||
console.log(res)
|
||||
}).catch(err => {
|
||||
this.errorNum += 1
|
||||
console.log(err)
|
||||
this.errorItems.push(itemUniqueName + ': ' + (((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'))
|
||||
})
|
||||
this.complete += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.my-box {
|
||||
margin-top: 20px;
|
||||
color: red;
|
||||
border: 1px red dashed;
|
||||
padding: 8px;
|
||||
border-radius:5px;
|
||||
height: 429px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
461
cmdb-ui/src/views/cmdb/ci/index.vue
Normal file
461
cmdb-ui/src/views/cmdb/ci/index.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-spin :tip="loadTip" :spinning="loading">
|
||||
<search-form ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
|
||||
|
||||
<ci-detail ref="detail" :typeId="typeId" />
|
||||
|
||||
<div class="table-operator">
|
||||
<a-button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
@click="$refs.create.visible = true; $refs.create.action='create'"
|
||||
>新建</a-button>
|
||||
<a-dropdown v-action:edit v-if="selectedRowKeys.length > 0">
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item
|
||||
key="batchUpdate"
|
||||
@click="$refs.create.visible = true; $refs.create.action='update'"
|
||||
>
|
||||
<span @click="$refs.create.visible = true">
|
||||
<a-icon type="edit" /> 修改
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="batchDownload" @click="batchDownload">
|
||||
<json-excel :fetch="batchDownload" name="cmdb.xls">
|
||||
<a-icon type="download" /> 下载
|
||||
</json-excel>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="batchDelete" @click="batchDelete">
|
||||
<a-icon type="delete" />删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-button style="margin-left: 8px">
|
||||
批量操作
|
||||
<a-icon type="down" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
<s-table
|
||||
bordered
|
||||
ref="table"
|
||||
size="middle"
|
||||
rowKey="ci_id"
|
||||
:columns="columns"
|
||||
:data="loadInstances"
|
||||
:alert="options.alert"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="{ x: scrollX, y: scrollY }"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
>
|
||||
<template :slot="col.dataIndex" slot-scope="text, record" v-for="col in columns">
|
||||
<editable-cell
|
||||
:key="'edit_' + col.dataIndex"
|
||||
:text="text"
|
||||
@change="onCellChange(record.key, col.dataIndex, $event, record[col.dataIndex])"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a
|
||||
@click="$refs.detail.visible = true; $refs.detail.ciId = record.key; $refs.detail.create()"
|
||||
>详情</a>
|
||||
|
||||
<a-divider type="vertical" />
|
||||
<a @click="deleteCI(record)">删除</a>
|
||||
</template>
|
||||
</span>
|
||||
</s-table>
|
||||
|
||||
<create-instance-form @refresh="refreshTable" ref="create" @submit="batchUpdate" />
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setTimeout } from 'timers'
|
||||
import { STable } from '@/components'
|
||||
import JsonExcel from 'vue-json-excel'
|
||||
|
||||
import SearchForm from './modules/SearchForm'
|
||||
import CreateInstanceForm from './modules/CreateInstanceForm'
|
||||
import EditableCell from './modules/EditableCell'
|
||||
import CiDetail from './modules/CiDetail'
|
||||
import { searchCI, updateCI, deleteCI } from '@/api/cmdb/ci'
|
||||
import { getSubscribeAttributes } from '@/api/cmdb/preference'
|
||||
import { notification } from 'ant-design-vue'
|
||||
|
||||
var valueTypeMap = {
|
||||
'0': 'int',
|
||||
'1': 'float',
|
||||
'2': 'text',
|
||||
'3': 'datetime',
|
||||
'4': 'date',
|
||||
'5': 'time'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'InstanceList',
|
||||
components: {
|
||||
STable,
|
||||
EditableCell,
|
||||
JsonExcel,
|
||||
SearchForm,
|
||||
CreateInstanceForm,
|
||||
CiDetail
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
loadTip: '',
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
form: this.$form.createForm(this),
|
||||
valueTypeMap: valueTypeMap,
|
||||
mdl: {},
|
||||
typeId: this.$router.currentRoute.meta.typeId,
|
||||
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
|
||||
preferenceAttrList: [],
|
||||
|
||||
instanceList: [],
|
||||
// 表头
|
||||
columns: [],
|
||||
// 加载数据方法 必须为 Promise 对象
|
||||
loadInstances: parameter => {
|
||||
const params = Object.assign(parameter, this.$refs.search.queryParam)
|
||||
let q = `q=_type:${this.$router.currentRoute.meta.typeId}`
|
||||
Object.keys(params).forEach(key => {
|
||||
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
|
||||
if (typeof params[key] === 'object' && params[key].length > 1) {
|
||||
q += `,${key}:(${params[key].join(';')})`
|
||||
} else if (params[key]) {
|
||||
q += `,${key}:${params[key]}`
|
||||
}
|
||||
|
||||
if (typeof params[key] === 'string') {
|
||||
q += '*'
|
||||
}
|
||||
}
|
||||
})
|
||||
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
|
||||
if ('sortField' in params) {
|
||||
let order = ''
|
||||
if (params['sortOrder'] !== 'ascend') {
|
||||
order = '-'
|
||||
}
|
||||
q += `&sort=${order}${params['sortField']}`
|
||||
}
|
||||
|
||||
return searchCI(q).then(res => {
|
||||
const result = {}
|
||||
result.pageNo = res.page
|
||||
result.pageSize = res.total
|
||||
result.totalCount = res.numfound
|
||||
result.totalPage = Math.ceil(res.numfound / params.pageSize)
|
||||
result.data = Object.assign([], res.result)
|
||||
result.data.forEach((item, index) => (item.key = item.ci_id))
|
||||
if (res.numfound) {
|
||||
setTimeout(() => {
|
||||
this.setColumnWidth()
|
||||
}, 200)
|
||||
}
|
||||
this.instanceList = result.data
|
||||
return result
|
||||
})
|
||||
},
|
||||
// custom table alert & rowSelection
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
options: {
|
||||
alert: {
|
||||
show: true,
|
||||
clear: () => {
|
||||
this.selectedRowKeys = []
|
||||
}
|
||||
},
|
||||
rowSelection: {
|
||||
selectedRowKeys: this.selectedRowKeys,
|
||||
onChange: this.onSelectChange,
|
||||
columnWidth: 62,
|
||||
fixed: true
|
||||
}
|
||||
},
|
||||
optionAlertShow: false,
|
||||
watch: {
|
||||
'$route.path': function (newPath, oldPath) {
|
||||
this.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.tableOption()
|
||||
this.loadColumns()
|
||||
},
|
||||
watch: {
|
||||
'$route.path': function (newPath, oldPath) {
|
||||
this.reload()
|
||||
}
|
||||
},
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
setColumnWidth () {
|
||||
let rows = []
|
||||
try {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
|
||||
} catch (e) {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
|
||||
}
|
||||
let scrollX = 0
|
||||
|
||||
const columns = Object.assign([], this.columns)
|
||||
for (let i = 1; i < rows.length - 1; i++) {
|
||||
columns[i - 1].width = rows[i].offsetWidth < 100 ? 100 : rows[i].offsetWidth
|
||||
scrollX += columns[i - 1].width
|
||||
}
|
||||
this.columns = columns
|
||||
|
||||
this.scrollX =
|
||||
scrollX +
|
||||
document.querySelector('.ant-table-fixed-left').offsetWidth +
|
||||
document.querySelector('.ant-table-fixed-right').offsetWidth
|
||||
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
|
||||
},
|
||||
tableOption () {
|
||||
if (!this.optionAlertShow) {
|
||||
this.options = {
|
||||
alert: {
|
||||
show: true,
|
||||
clear: () => {
|
||||
this.selectedRowKeys = []
|
||||
}
|
||||
},
|
||||
rowSelection: {
|
||||
selectedRowKeys: this.selectedRowKeys,
|
||||
onChange: this.onSelectChange,
|
||||
getCheckboxProps: record => ({
|
||||
props: {
|
||||
disabled: record.no === 'No 2', // Column configuration not to be checked
|
||||
name: record.no
|
||||
}
|
||||
}),
|
||||
columnWidth: 62,
|
||||
fixed: true
|
||||
}
|
||||
}
|
||||
this.optionAlertShow = true
|
||||
} else {
|
||||
alert('no alert')
|
||||
this.options = {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
}
|
||||
this.optionAlertShow = false
|
||||
}
|
||||
},
|
||||
|
||||
loadColumns () {
|
||||
getSubscribeAttributes(this.$router.currentRoute.meta.typeId).then(res => {
|
||||
const prefAttrList = res.attributes
|
||||
this.preferenceAttrList = prefAttrList
|
||||
|
||||
const columns = []
|
||||
prefAttrList.forEach((item, index) => {
|
||||
const col = {}
|
||||
col.title = item.alias
|
||||
col.dataIndex = item.name
|
||||
if (index !== prefAttrList.length - 1) {
|
||||
col.width = 100
|
||||
}
|
||||
if (item.is_sortable) {
|
||||
col.sorter = true
|
||||
}
|
||||
if (item.is_choice) {
|
||||
const filters = []
|
||||
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
|
||||
col.filters = filters
|
||||
}
|
||||
col.scopedSlots = { customRender: item.name }
|
||||
columns.push(col)
|
||||
})
|
||||
|
||||
columns.push({
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
})
|
||||
this.columns = columns
|
||||
})
|
||||
},
|
||||
|
||||
onSelectChange (selectedRowKeys, selectedRows) {
|
||||
this.selectedRowKeys = selectedRowKeys
|
||||
this.selectedRows = selectedRows
|
||||
},
|
||||
|
||||
refreshTable (bool = false) {
|
||||
this.$refs.table.refresh(bool)
|
||||
},
|
||||
|
||||
onCellChange (key, dataIndex, event, oldValue) {
|
||||
const value = event[0]
|
||||
const payload = {}
|
||||
payload[dataIndex] = value
|
||||
updateCI(key, payload)
|
||||
.then(res => {
|
||||
event[1].x = false
|
||||
})
|
||||
.catch(err => {
|
||||
notification.error({
|
||||
message: err.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
async batchDownload () {
|
||||
this.loading = true
|
||||
this.loadTip = '正在下载 ...'
|
||||
const promises = this.selectedRowKeys.map(ciId => {
|
||||
return searchCI(`q=_id:${ciId}`).then(res => {
|
||||
const ciMap = {}
|
||||
Object.keys(res.result[0]).forEach(k => {
|
||||
if (!['ci_type', 'ci_id', 'ci_type_alias', 'type_id'].includes(k)) {
|
||||
ciMap[k] = res.result[0][k]
|
||||
}
|
||||
})
|
||||
return ciMap
|
||||
})
|
||||
})
|
||||
const results = await Promise.all(promises)
|
||||
this.loading = false
|
||||
this.$refs.table.clearSelected()
|
||||
|
||||
return results
|
||||
},
|
||||
batchUpdate (values) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: '确认要批量修改吗 ?',
|
||||
onOk () {
|
||||
that.loading = true
|
||||
that.loadTip = '正在批量修改 ...'
|
||||
const payload = {}
|
||||
Object.keys(values).forEach(key => {
|
||||
if (values[key]) {
|
||||
payload[key] = values[key]
|
||||
}
|
||||
})
|
||||
const promises = that.selectedRowKeys.map(ciId => {
|
||||
return updateCI(ciId, payload).then(res => {
|
||||
return 'ok'
|
||||
})
|
||||
})
|
||||
Promise.all(promises)
|
||||
.then(res => {
|
||||
that.loading = false
|
||||
notification.success({
|
||||
message: '批量修改成功'
|
||||
})
|
||||
that.$refs.create.visible = false
|
||||
|
||||
that.$refs.table.clearSelected()
|
||||
setTimeout(() => {
|
||||
that.$refs.table.refresh(true)
|
||||
}, 1000)
|
||||
that.reload()
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
that.loading = false
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
setTimeout(() => {
|
||||
that.$refs.table.refresh(true)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
batchDelete () {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: '真的要删除吗 ?',
|
||||
onOk () {
|
||||
that.loading = true
|
||||
that.loadTip = '正在删除 ...'
|
||||
const promises = that.selectedRowKeys.map(ciId => {
|
||||
return deleteCI(ciId).then(res => {
|
||||
return 'ok'
|
||||
})
|
||||
})
|
||||
Promise.all(promises)
|
||||
.then(res => {
|
||||
that.loading = false
|
||||
notification.success({
|
||||
message: '删除成功'
|
||||
})
|
||||
that.$refs.table.clearSelected()
|
||||
setTimeout(() => {
|
||||
that.$refs.table.refresh(true)
|
||||
}, 1500)
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
that.loading = false
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
setTimeout(() => {
|
||||
that.$refs.table.refresh(true)
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteCI (record) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: '真的要删除吗 ?',
|
||||
onOk () {
|
||||
deleteCI(record.key)
|
||||
.then(res => {
|
||||
setTimeout(() => {
|
||||
that.$refs.table.refresh(true)
|
||||
}, 1000)
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ant-table-thead > tr > th,
|
||||
.ant-table-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spin-content {
|
||||
border: 1px solid #91d5ff;
|
||||
background-color: #e6f7ff;
|
||||
padding: 30px;
|
||||
}
|
||||
</style>
|
||||
369
cmdb-ui/src/views/cmdb/ci/modules/CiDetail.vue
Normal file
369
cmdb-ui/src/views/cmdb/ci/modules/CiDetail.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
width="80%"
|
||||
placement="left"
|
||||
@close="() => { visible = false }"
|
||||
:visible="visible"
|
||||
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
|
||||
>
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:tabList="tabList"
|
||||
:activeTabKey="activeTabKey"
|
||||
:selectedKeys="[typeId]"
|
||||
@tabChange="(key) => {this.activeTabKey = key}"
|
||||
>
|
||||
<div v-if="activeTabKey === 'tab_1'">
|
||||
<a-card
|
||||
type="inner"
|
||||
:title="group.name || '其他'"
|
||||
:key="group.name"
|
||||
v-for="group in attributeGroups"
|
||||
>
|
||||
<description-list title size="small">
|
||||
<div :key="attr.name" v-for="(attr, index) in group.attributes">
|
||||
<div v-if="index % 3 === 0" style="clear: both"></div>
|
||||
<description-list-item :term="attr.alias || attr.name">{{ ci[attr.name] }}</description-list-item>
|
||||
</div>
|
||||
</description-list>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTabKey === 'tab_2'">
|
||||
<div v-for="parent in parentCITypes" :key="'ctr_' + parent.ctr_id">
|
||||
<a-card type="inner" :title="parent.alias || parent.name" v-if="firstCIs[parent.name]">
|
||||
<a-table
|
||||
rowKey="ci_id"
|
||||
size="middle"
|
||||
:columns="firstCIColumns[parent.id]"
|
||||
:dataSource="firstCIs[parent.name]"
|
||||
:pagination="false"
|
||||
:scroll="{x: '100%'}"
|
||||
></a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
<div v-for="child in childCITypes" :key="'ctr_' + child.ctr_id">
|
||||
<a-card type="inner" :title="child.alias || child.name" v-if="secondCIs[child.name]">
|
||||
<a-table
|
||||
rowKey="ci_id"
|
||||
size="middle"
|
||||
:columns="secondCIColumns[child.id]"
|
||||
:dataSource="secondCIs[child.name]"
|
||||
:pagination="false"
|
||||
:scroll="{x: '100%'}"
|
||||
></a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTabKey === 'tab_3'">
|
||||
<a-card type="inner" :bordered="false">
|
||||
<a-table
|
||||
bordered
|
||||
rowKey="hid"
|
||||
size="middle"
|
||||
:columns="historyColumns"
|
||||
:dataSource="ciHistory"
|
||||
:pagination="false"
|
||||
:scroll="{x: '100%'}"
|
||||
>
|
||||
<template
|
||||
slot="operate_type"
|
||||
slot-scope="operate_type"
|
||||
>{{ operate_type | operateTypeFilter }}</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DescriptionList from '@/components/DescriptionList'
|
||||
|
||||
import { getCITypeGroupById } from '@/api/cmdb/CIType'
|
||||
import { getCITypeChildren, getCITypeParent } from '@/api/cmdb/CITypeRelation'
|
||||
import { getFirstCIs, getSecondCIs } from '@/api/cmdb/CIRelation'
|
||||
import { getCIHistory } from '@/api/cmdb/history'
|
||||
import { getCIById } from '@/api/cmdb/ci'
|
||||
import { notification } from 'ant-design-vue'
|
||||
|
||||
const DescriptionListItem = DescriptionList.Item
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DescriptionList,
|
||||
DescriptionListItem
|
||||
},
|
||||
props: {
|
||||
typeId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
parentCITypes: [],
|
||||
childCITypes: [],
|
||||
firstCIs: {},
|
||||
firstCIColumns: {},
|
||||
secondCIs: {},
|
||||
secondCIColumns: {},
|
||||
ci: {},
|
||||
|
||||
attributeGroups: [],
|
||||
tabList: [
|
||||
{
|
||||
key: 'tab_1',
|
||||
tab: '属性'
|
||||
},
|
||||
{
|
||||
key: 'tab_2',
|
||||
tab: '关系'
|
||||
},
|
||||
{
|
||||
key: 'tab_3',
|
||||
tab: '操作历史'
|
||||
}
|
||||
],
|
||||
activeTabKey: 'tab_1',
|
||||
rowSpanMap: {},
|
||||
historyColumns: [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
customRender: (value, row, index) => {
|
||||
const obj = {
|
||||
children: value,
|
||||
attrs: {}
|
||||
}
|
||||
obj.attrs.rowSpan = this.rowSpanMap[index]
|
||||
return obj
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
customRender: (value, row, index) => {
|
||||
const obj = {
|
||||
children: value,
|
||||
attrs: {}
|
||||
}
|
||||
obj.attrs.rowSpan = this.rowSpanMap[index]
|
||||
return obj
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operate_type',
|
||||
key: 'operate_type',
|
||||
scopedSlots: { customRender: 'operate_type' }
|
||||
},
|
||||
{
|
||||
title: '属性',
|
||||
dataIndex: 'attr_alias',
|
||||
key: 'attr_name'
|
||||
},
|
||||
{
|
||||
title: 'Old',
|
||||
dataIndex: 'old',
|
||||
key: 'old'
|
||||
},
|
||||
{
|
||||
title: 'New',
|
||||
dataIndex: 'new',
|
||||
key: 'new'
|
||||
}
|
||||
],
|
||||
ciHistory: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// this.getAttributes()
|
||||
// this.getCI()
|
||||
// this.getFirstCIs()
|
||||
// this.getSecondCIs()
|
||||
// this.getParentCITypes()
|
||||
// this.getChildCITypes()
|
||||
// this.getCIHistory()
|
||||
},
|
||||
filters: {
|
||||
operateTypeFilter (operateType) {
|
||||
const operateTypeMap = {
|
||||
'0': '新增',
|
||||
'1': '删除',
|
||||
'2': '修改'
|
||||
}
|
||||
return operateTypeMap[operateType]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
create () {
|
||||
this.getAttributes()
|
||||
this.getCI()
|
||||
this.getFirstCIs()
|
||||
this.getSecondCIs()
|
||||
this.getParentCITypes()
|
||||
this.getChildCITypes()
|
||||
this.getCIHistory()
|
||||
},
|
||||
getAttributes () {
|
||||
getCITypeGroupById(this.typeId, { need_other: 1 })
|
||||
.then(res => {
|
||||
this.attributeGroups = res
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getCI () {
|
||||
getCIById(this.ciId)
|
||||
.then(res => {
|
||||
this.ci = res.ci
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
getFirstCIs () {
|
||||
getFirstCIs(this.ciId)
|
||||
.then(res => {
|
||||
const firstCIs = {}
|
||||
res.first_cis.forEach(item => {
|
||||
if (item.ci_type in firstCIs) {
|
||||
firstCIs[item.ci_type].push(item)
|
||||
} else {
|
||||
firstCIs[item.ci_type] = [item]
|
||||
}
|
||||
})
|
||||
this.firstCIs = firstCIs
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getSecondCIs () {
|
||||
getSecondCIs(this.ciId)
|
||||
.then(res => {
|
||||
const secondCIs = {}
|
||||
res.second_cis.forEach(item => {
|
||||
if (item.ci_type in secondCIs) {
|
||||
secondCIs[item.ci_type].push(item)
|
||||
} else {
|
||||
secondCIs[item.ci_type] = [item]
|
||||
}
|
||||
})
|
||||
this.secondCIs = secondCIs
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getParentCITypes () {
|
||||
getCITypeParent(this.typeId)
|
||||
.then(res => {
|
||||
this.parentCITypes = res.parents
|
||||
|
||||
const firstCIColumns = {}
|
||||
res.parents.forEach(item => {
|
||||
const columns = []
|
||||
item.attributes.forEach(attr => {
|
||||
columns.push({ key: 'p_' + attr.id, dataIndex: attr.name, title: attr.alias })
|
||||
})
|
||||
firstCIColumns[item.id] = columns
|
||||
})
|
||||
this.firstCIColumns = firstCIColumns
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getChildCITypes () {
|
||||
getCITypeChildren(this.typeId)
|
||||
.then(res => {
|
||||
this.childCITypes = res.children
|
||||
|
||||
const secondCIColumns = {}
|
||||
res.children.forEach(item => {
|
||||
const columns = []
|
||||
item.attributes.forEach(attr => {
|
||||
columns.push({ key: 'c_' + attr.id, dataIndex: attr.name, title: attr.alias })
|
||||
})
|
||||
secondCIColumns[item.id] = columns
|
||||
})
|
||||
this.secondCIColumns = secondCIColumns
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getCIHistory () {
|
||||
getCIHistory(this.ciId)
|
||||
.then(res => {
|
||||
this.ciHistory = res
|
||||
const rowSpanMap = {}
|
||||
let startIndex = 0
|
||||
let startCount = 1
|
||||
res.forEach((item, index) => {
|
||||
if (index === 0) {
|
||||
return
|
||||
}
|
||||
if (res[index].record_id === res[startIndex].record_id) {
|
||||
startCount += 1
|
||||
rowSpanMap[index] = 0
|
||||
if (index === res.length - 1) {
|
||||
rowSpanMap[startIndex] = startCount
|
||||
}
|
||||
} else {
|
||||
rowSpanMap[startIndex] = startCount
|
||||
startIndex = index
|
||||
startCount = 1
|
||||
if (index === res.length - 1) {
|
||||
rowSpanMap[index] = 1
|
||||
}
|
||||
}
|
||||
})
|
||||
this.rowSpanMap = rowSpanMap
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lange="less">
|
||||
div.term {
|
||||
background-color: rgb(225, 238, 246);
|
||||
font-weight: 400;
|
||||
min-width: 120px !important;
|
||||
padding-left: 10px;
|
||||
}
|
||||
div.content {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-weight: bold;
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
163
cmdb-ui/src/views/cmdb/ci/modules/CreateInstanceForm.vue
Normal file
163
cmdb-ui/src/views/cmdb/ci/modules/CreateInstanceForm.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<!-- v-decorator="[ item.name, { rules: [ { required: item.is_required ? true: false } ] } ]" -->
|
||||
<a-drawer
|
||||
:title="title + CIType.alias"
|
||||
width="500"
|
||||
@close="() => { visible = false; $emit('refresh', true) }"
|
||||
:visible="visible"
|
||||
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
|
||||
>
|
||||
<p v-if="action === 'update'">只需要填写需要修改的字段即可!</p>
|
||||
<a-form :form="form" :layout="formLayout" @submit="createInstance">
|
||||
<a-button type="primary" @click="createInstance">Submit</a-button>
|
||||
<a-form-item
|
||||
v-bind="formItemLayout"
|
||||
:label="attr.alias || attr.name"
|
||||
v-for="(attr, attr_idx) in attributeList"
|
||||
:key="attr.name + attr_idx"
|
||||
>
|
||||
<a-select
|
||||
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
|
||||
placeholder="请选择"
|
||||
v-if="attr.is_choice"
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice"
|
||||
:key="'New_' + attr.name + choice_idx"
|
||||
v-for="(choice, choice_idx) in attr.choice_value"
|
||||
>{{ choice }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
|
||||
style="width: 100%"
|
||||
v-else-if="valueTypeMap[attr.value_type] == 'int' || valueTypeMap[attr.value_type] == 'float'"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
|
||||
style="width: 100%"
|
||||
:format="valueTypeMap[attr.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
|
||||
/>
|
||||
<a-input
|
||||
v-decorator="[attr.name, {validateTrigger: ['submit'], rules: [{ required: attr.is_required && action === 'create' ? true: false}]}]"
|
||||
style="width: 100%"
|
||||
v-else
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCIType } from '@/api/cmdb/CIType'
|
||||
import { getCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
|
||||
import { addCI } from '@/api/cmdb/ci'
|
||||
|
||||
import { notification } from 'ant-design-vue'
|
||||
|
||||
var valueTypeMap = {
|
||||
'0': 'int',
|
||||
'1': 'float',
|
||||
'2': 'text',
|
||||
'3': 'datetime',
|
||||
'4': 'date',
|
||||
'5': 'time'
|
||||
}
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
action: '',
|
||||
form: this.$form.createForm(this),
|
||||
|
||||
visible: false,
|
||||
attributeList: [],
|
||||
typeId: this.$router.currentRoute.meta.typeId,
|
||||
CIType: {},
|
||||
valueTypeMap: valueTypeMap,
|
||||
|
||||
formItemLayout: {
|
||||
labelCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 8 }
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 16 }
|
||||
}
|
||||
},
|
||||
formLayout: 'horizontal'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
return this.action === 'create' ? '创建 ' : '批量修改 '
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.path': function (oldValue, newValue) {
|
||||
this.typeId = this.$router.currentRoute.meta.typeId
|
||||
this.getCIType()
|
||||
this.getAttributeList()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getCIType()
|
||||
this.getAttributeList()
|
||||
},
|
||||
methods: {
|
||||
getCIType () {
|
||||
getCIType(this.typeId).then(res => {
|
||||
this.CIType = res.ci_types[0]
|
||||
})
|
||||
},
|
||||
getAttributeList () {
|
||||
getCITypeAttributesById(this.typeId).then(res => {
|
||||
const attrList = res.attributes
|
||||
// res.attributes.forEach(item =>
|
||||
// attrList.push({
|
||||
// name: item.name,
|
||||
// alias: item.alias,
|
||||
// value_type: item.value_type,
|
||||
// is_required: item.is_required
|
||||
// })
|
||||
// )
|
||||
this.attributeList = attrList.sort((x, y) => y.is_required - x.is_required)
|
||||
})
|
||||
},
|
||||
createInstance (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
Object.keys(values).forEach(k => {
|
||||
if (typeof values[k] === 'object') {
|
||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
})
|
||||
if (!err) {
|
||||
if (this.action === 'update') {
|
||||
this.$emit('submit', values)
|
||||
return
|
||||
}
|
||||
|
||||
values.ci_type = this.typeId
|
||||
addCI(values)
|
||||
.then(res => {
|
||||
notification.success({
|
||||
message: '新增成功'
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
} else {
|
||||
notification.error({
|
||||
message: err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
78
cmdb-ui/src/views/cmdb/ci/modules/EditableCell.vue
Normal file
78
cmdb-ui/src/views/cmdb/ci/modules/EditableCell.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="editable-cell">
|
||||
<div v-if="editable.x" class="editable-cell-input-wrapper">
|
||||
<a-input :value="value" @change="handleChange" @pressEnter="check" />
|
||||
<a-icon type="check" class="editable-cell-icon-check" @click="check" />
|
||||
</div>
|
||||
<div v-else class="editable-cell-text-wrapper">
|
||||
{{ value | joinList }}
|
||||
<a-icon type="edit" class="editable-cell-icon" @click="edit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
value: this.text,
|
||||
editable: { x: false }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange (e) {
|
||||
const value = e.target.value
|
||||
this.value = value
|
||||
},
|
||||
check () {
|
||||
// this.editable.x = false
|
||||
this.$emit('change', [this.value, this.editable])
|
||||
},
|
||||
edit () {
|
||||
this.editable.x = true
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
joinList: function (itemValue) {
|
||||
if (typeof itemValue === 'object' && itemValue) {
|
||||
return itemValue.join(',')
|
||||
} else if (itemValue !== null) {
|
||||
return itemValue + ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editable-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editable-cell-icon,
|
||||
.editable-cell-icon-check {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editable-cell-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:hover > .editable-cell .editable-cell-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editable-cell-icon:hover,
|
||||
.editable-cell-icon-check:hover {
|
||||
color: #108ee9;
|
||||
}
|
||||
</style>
|
||||
131
cmdb-ui/src/views/cmdb/ci/modules/SearchForm.vue
Normal file
131
cmdb-ui/src/views/cmdb/ci/modules/SearchForm.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="table-page-search-wrapper">
|
||||
<a-form layout="inline">
|
||||
<a-row :gutter="48">
|
||||
<a-col
|
||||
:lg="6"
|
||||
:md="8"
|
||||
:sm="24"
|
||||
:key="prefAttr.name"
|
||||
v-for="prefAttr in preferenceAttrList.slice(0, 4)"
|
||||
>
|
||||
<a-form-item :label="prefAttr.alias || prefAttr.name">
|
||||
<a-select
|
||||
v-model="queryParam[prefAttr.name]"
|
||||
placeholder="请选择"
|
||||
v-if="prefAttr.is_choice"
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice"
|
||||
:key="'Search_' + prefAttr.name + index"
|
||||
v-for="(choice, index) in prefAttr.choice_value"
|
||||
>{{ choice }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
v-model="queryParam[prefAttr.name]"
|
||||
style="width: 100%"
|
||||
v-else-if="valueTypeMap[prefAttr.value_type] == 'int' || valueTypeMap[prefAttr.value_type] == 'float'"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-model="queryParam[prefAttr.name]"
|
||||
style="width: 100%"
|
||||
:format="valueTypeMap[prefAttr.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-else-if="valueTypeMap[prefAttr.value_type] == 'date' || valueTypeMap[prefAttr.value_type] == 'datetime'"
|
||||
/>
|
||||
<a-input v-model="queryParam[prefAttr.name]" style="width: 100%" v-else />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<template v-if="advanced && preferenceAttrList.length > 4">
|
||||
<a-col
|
||||
:lg="6"
|
||||
:md="8"
|
||||
:sm="24"
|
||||
:key="'advanced_' + item.name"
|
||||
v-for="item in preferenceAttrList.slice(4)"
|
||||
>
|
||||
<a-form-item :label="item.alias || item.name">
|
||||
<a-select v-model="queryParam[item.name]" placeholder="请选择" v-if="item.is_choice">
|
||||
<a-select-option
|
||||
:value="choice"
|
||||
:key="'advanced_' + item.name + index"
|
||||
v-for="(choice, index) in item.choice_value"
|
||||
>{{ choice }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
v-model="queryParam[item.name]"
|
||||
style="width: 100%"
|
||||
v-else-if="valueTypeMap[item.value_type] == 'int' || valueTypeMap[item.value_type] == 'float'"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-model="queryParam[item.name]"
|
||||
style="width: 100%"
|
||||
:format="valueTypeMap[item.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
|
||||
/>
|
||||
<a-input v-model="queryParam[item.name]" style="width: 100%" v-else />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
|
||||
<a-col :lg="!advanced && 6 || 24" :md="!advanced && 8 || 24" :sm="24" style="float: right; padding-left: 0">
|
||||
<span
|
||||
class="table-page-search-submitButtons"
|
||||
:style="advanced && { float: 'right', overflow: 'hidden' } || {} "
|
||||
>
|
||||
<a-button type="primary" @click="$emit('refresh', true)" v-if="preferenceAttrList.length">查询</a-button>
|
||||
<a-button style="margin-left: 8px" @click="() => queryParam = {}" v-if="preferenceAttrList.length">重置</a-button>
|
||||
<a
|
||||
@click="toggleAdvanced"
|
||||
style="margin-left: 8px"
|
||||
v-if="preferenceAttrList.length > 4"
|
||||
>
|
||||
{{ advanced ? '收起' : '展开' }}
|
||||
<a-icon :type="advanced ? 'up' : 'down'" />
|
||||
</a>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
var valueTypeMap = {
|
||||
'0': 'int',
|
||||
'1': 'float',
|
||||
'2': 'text',
|
||||
'3': 'datetime',
|
||||
'4': 'date',
|
||||
'5': 'time'
|
||||
}
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
queryParam: {},
|
||||
valueTypeMap: valueTypeMap
|
||||
}
|
||||
},
|
||||
props: {
|
||||
preferenceAttrList: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.path': function (oldValue, newValue) {
|
||||
this.queryParam = {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleAdvanced () {
|
||||
this.advanced = !this.advanced
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
391
cmdb-ui/src/views/cmdb/modeling/attributes/index.vue
Normal file
391
cmdb-ui/src/views/cmdb/modeling/attributes/index.vue
Normal file
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
showPagination="auto"
|
||||
:pageSize="25"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="aliasSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.alias">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.alias})|(?=${columnSearchText.alias})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.alias.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<AttributeForm ref="attributeForm" :handleOk="handleOk"> </AttributeForm>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import AttributeForm from './module/attributeForm'
|
||||
import { valueTypeMap } from './module/const'
|
||||
import { deleteAttributesById, searchAttributes } from '@/api/cmdb/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
AttributeForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增属性',
|
||||
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
attributes: [],
|
||||
allAttributes: [],
|
||||
transferData: [],
|
||||
transferTargetKeys: [],
|
||||
transferSelectedKeys: [],
|
||||
originTargetKeys: [],
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'alias',
|
||||
sorter: false,
|
||||
width: 250,
|
||||
scopedSlots: {
|
||||
customRender: 'aliasSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.alias && record.alias.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '英文名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 250,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'value_type',
|
||||
sorter: false,
|
||||
width: 80,
|
||||
scopedSlots: { customRender: 'value_type' },
|
||||
customRender: (text) => valueTypeMap[text]
|
||||
|
||||
},
|
||||
{
|
||||
title: '唯一',
|
||||
dataIndex: 'is_unique',
|
||||
width: 50,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '索引',
|
||||
dataIndex: 'is_index',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'is_sortable',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '链接',
|
||||
dataIndex: 'is_link',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '密码',
|
||||
dataIndex: 'is_password',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '列表',
|
||||
dataIndex: 'is_list',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
parameter['page_size'] = parameter['pageSize']
|
||||
parameter['page'] = parameter['pageNo']
|
||||
Object.assign(parameter, this.queryParam)
|
||||
console.log('loadData.parameter', parameter)
|
||||
|
||||
return searchAttributes(parameter)
|
||||
.then(res => {
|
||||
res.pageNo = res.page
|
||||
res.pageSize = res.total
|
||||
res.totalCount = res.numfound
|
||||
res.totalPage = Math.ceil(res.numfound / parameter.pageSize)
|
||||
res.data = res.attributes
|
||||
|
||||
console.log('loadData.res', res)
|
||||
this.allAttributes = res.attributes
|
||||
return res
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.getAttributes()
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
getAttributes () {
|
||||
searchAttributes().then(res => {
|
||||
this.allAttributes = res.attributes
|
||||
})
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.$refs.attributeForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteAttribute(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.attributeForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteAttribute (attrId) {
|
||||
deleteAttributesById(attrId)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="属性名(英文)"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder="英文"
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入属性名'},{message: '不能以数字开头,可以是英文 数字以及下划线 (_)', pattern: RegExp('^(?!\\d)[a-zA-Z_0-9]+$')}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="别名"
|
||||
>
|
||||
<a-input
|
||||
name="alias"
|
||||
v-decorator="['alias', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="数据类型"
|
||||
>
|
||||
|
||||
<a-select
|
||||
name="value_type"
|
||||
style="width: 120px"
|
||||
v-decorator="['value_type', {rules: [{required: true}], } ]"
|
||||
>
|
||||
<a-select-option :value="key" :key="key" v-for="(value, key) in ValueTypeMap">{{ value }}</a-select-option>
|
||||
</a-select>
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否唯一"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_unique"
|
||||
v-decorator="['is_unique', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否索引"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_index"
|
||||
v-decorator="['is_index', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否可排序"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_sortable"
|
||||
v-decorator="['is_sortable', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否是链接"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_link"
|
||||
v-decorator="['is_link', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否是密码"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_password"
|
||||
v-decorator="['is_password', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="是否列表"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
name="is_list"
|
||||
v-decorator="['is_list', {rules: [], valuePropName: 'checked',} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="预定义值"
|
||||
>
|
||||
<a-textarea
|
||||
:rows="5"
|
||||
name="choice_value"
|
||||
placeholder="多个值使用换行分隔"
|
||||
v-decorator="['choice_value', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import { createAttribute, createCITypeAttributes, updateAttributeById } from '@/api/cmdb/CITypeAttr'
|
||||
import { valueTypeMap } from './const'
|
||||
|
||||
export default {
|
||||
name: 'AttributeForm',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
drawerTitle: '新增属性',
|
||||
drawerVisible: false,
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
attributes: [],
|
||||
allAttributes: [],
|
||||
|
||||
ValueTypeMap: valueTypeMap
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
|
||||
handleCreate () {
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerVisible = true
|
||||
console.log(record)
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
|
||||
id: record.id,
|
||||
alias: record.alias,
|
||||
name: record.name,
|
||||
value_type: record.value_type,
|
||||
is_list: record.is_list,
|
||||
is_unique: record.is_unique,
|
||||
is_index: record.is_index,
|
||||
is_password: record.is_password,
|
||||
is_link: record.is_link,
|
||||
is_sortable: record.is_sortable,
|
||||
choice_value: (record.choice_value || []).join('\n')
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
console.log('Received values of form: ', values)
|
||||
if (values.choice_value) {
|
||||
values.choice_value = values.choice_value.split('\n')
|
||||
}
|
||||
|
||||
if (values.id) {
|
||||
this.updateAttribute(values.id, values)
|
||||
} else {
|
||||
this.createAttribute(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateAttribute (attrId, data) {
|
||||
updateAttributeById(attrId, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createAttribute (data) {
|
||||
createAttribute(data)
|
||||
.then(res => {
|
||||
if (this.CITypeId) {
|
||||
createCITypeAttributes(this.CITypeId, { attr_id: [res.attr_id] })
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
} else {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
export const valueTypeMap = {
|
||||
'0': '整数',
|
||||
'1': '浮点数',
|
||||
'2': '文本',
|
||||
'3': 'datetime',
|
||||
'4': 'date',
|
||||
'5': 'time'
|
||||
}
|
||||
561
cmdb-ui/src/views/cmdb/modeling/ci_type/attributesTable.vue
Normal file
561
cmdb-ui/src/views/cmdb/modeling/ci_type/attributesTable.vue
Normal file
@@ -0,0 +1,561 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ singleAttrAction.btnName }}</a-button>
|
||||
<a-button @click="handleUpdate" type="primary">{{ batchBindAttrAction.btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
:showPagination="showPagination"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="`Search ${column.dataIndex}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>Search</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>Reset</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="aliasSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.alias">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.alias})|(?=${columnSearchText.alias})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.alias.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<AttributeForm ref="attributeForm" :handleOk="handleOk"> </AttributeForm>
|
||||
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="batchBindAttrAction.drawerTitle"
|
||||
:visible="batchBindAttrAction.drawerVisible"
|
||||
@close="onBatchBindAttrActionClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleBatchUpdateSubmit" style="margin-bottom: 5rem">
|
||||
|
||||
<a-transfer
|
||||
:dataSource="transferData"
|
||||
:render="item=>item.title"
|
||||
:selectedKeys="transferSelectedKeys"
|
||||
:targetKeys="transferTargetKeys"
|
||||
:titles="['当前项', '已选项']"
|
||||
:listStyle="{
|
||||
height: '600px',
|
||||
width: '40%',
|
||||
}"
|
||||
showSearch
|
||||
@change="handleTransferChange"
|
||||
@scroll="handleTransferScroll"
|
||||
@selectChange="handleTransferSelectChange"
|
||||
|
||||
/>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleBatchUpdateSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onBatchBindAttrActionClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
createCITypeAttributes,
|
||||
deleteCITypeAttributesById,
|
||||
getCITypeAttributesByName,
|
||||
searchAttributes
|
||||
} from '@/api/cmdb/CITypeAttr'
|
||||
import { STable } from '@/components'
|
||||
import { mixin, mixinDevice } from '@/utils/mixin'
|
||||
import AttributeForm from '@/views/cmdb/modeling/attributes/module/attributeForm'
|
||||
import { valueTypeMap } from '@/views/cmdb/modeling/attributes/module/const'
|
||||
|
||||
export default {
|
||||
name: 'AttributesTable',
|
||||
mixins: [mixin, mixinDevice],
|
||||
components: {
|
||||
STable,
|
||||
AttributeForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
form: this.$form.createForm(this),
|
||||
scroll: { x: 1030, y: 600 },
|
||||
singleAttrAction: {
|
||||
btnName: '新增属性',
|
||||
drawerTitle: '新增属性',
|
||||
drawerVisible: false
|
||||
},
|
||||
batchBindAttrAction: {
|
||||
btnName: '绑定属性',
|
||||
drawerTitle: '绑定属性',
|
||||
drawerVisible: false
|
||||
},
|
||||
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
attributes: [],
|
||||
allAttributes: [],
|
||||
transferData: [],
|
||||
transferTargetKeys: [],
|
||||
transferSelectedKeys: [],
|
||||
originTargetKeys: [],
|
||||
|
||||
ValueTypeMap: valueTypeMap,
|
||||
pagination: {
|
||||
defaultPageSize: 20
|
||||
},
|
||||
showPagination: false,
|
||||
columnSearchText: {
|
||||
alias: '',
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'alias',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: {
|
||||
customRender: 'aliasSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.alias.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '英文名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'value_type',
|
||||
sorter: false,
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'value_type' },
|
||||
customRender: (text) => valueTypeMap[text]
|
||||
|
||||
},
|
||||
{
|
||||
title: '唯一',
|
||||
dataIndex: 'is_unique',
|
||||
width: 50,
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '索引',
|
||||
dataIndex: 'is_index',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'is_sortable',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '链接',
|
||||
dataIndex: 'is_link',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '密码',
|
||||
dataIndex: 'is_password',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '列表',
|
||||
dataIndex: 'is_list',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '必须',
|
||||
dataIndex: 'is_required',
|
||||
sorter: false,
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '默认显示',
|
||||
dataIndex: 'default_show',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
console.log('loadData.parameter', parameter)
|
||||
return getCITypeAttributesByName(this.CITypeName)
|
||||
.then(res => {
|
||||
this.attributes = res.attributes
|
||||
this.setTransferData()
|
||||
|
||||
return {
|
||||
data: res.attributes
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
},
|
||||
inject: ['reload'],
|
||||
computed: {
|
||||
|
||||
removeTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return originTargetKeys.filter(v => !originTargetKeys.includes(v) || !transferTargetKeys.includes(v))
|
||||
},
|
||||
|
||||
addTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return transferTargetKeys.filter(v => !transferTargetKeys.includes(v) || !originTargetKeys.includes(v))
|
||||
},
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getAttributes()
|
||||
this.setScrollY()
|
||||
},
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
getAttributes () {
|
||||
searchAttributes({ page_size: 10000 }).then(res => {
|
||||
this.allAttributes = res.attributes
|
||||
})
|
||||
},
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 250
|
||||
},
|
||||
|
||||
setTransferData () {
|
||||
const data = []
|
||||
const target = []
|
||||
|
||||
this.attributes.forEach(i => target.push(i.id.toString()))
|
||||
|
||||
this.allAttributes.forEach(i => data.push({
|
||||
key: i.id.toString(),
|
||||
title: i.alias,
|
||||
description: ''
|
||||
}))
|
||||
|
||||
this.transferData = data
|
||||
this.transferTargetKeys = target
|
||||
this.originTargetKeys = target
|
||||
},
|
||||
|
||||
handleTransferChange (nextTargetKeys, direction, moveKeys) {
|
||||
this.transferTargetKeys = nextTargetKeys
|
||||
|
||||
console.log('targetKeys: ', nextTargetKeys)
|
||||
console.log('direction: ', direction)
|
||||
console.log('moveKeys: ', moveKeys)
|
||||
console.log('addTransferKeys: ', this.addTransferKeys)
|
||||
console.log('removeTransferKeys: ', this.removeTransferKeys)
|
||||
},
|
||||
|
||||
handleTransferSelectChange (sourceSelectedKeys, targetSelectedKeys) {
|
||||
this.transferSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
|
||||
console.log('sourceSelectedKeys: ', sourceSelectedKeys)
|
||||
console.log('targetSelectedKeys: ', targetSelectedKeys)
|
||||
},
|
||||
handleTransferScroll (direction, e) {
|
||||
console.log('direction:', direction)
|
||||
console.log('target:', e.target)
|
||||
},
|
||||
|
||||
callback (key) {
|
||||
console.log(key)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.$refs.attributeForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.unbindAttribute([record.id])
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
this.reload()
|
||||
},
|
||||
handleCreate () {
|
||||
this.$refs.attributeForm.handleCreate()
|
||||
},
|
||||
|
||||
handleUpdate () {
|
||||
this.setTransferData()
|
||||
this.batchBindAttrAction.drawerVisible = true
|
||||
},
|
||||
|
||||
onBatchBindAttrActionClose () {
|
||||
this.batchBindAttrAction.drawerVisible = false
|
||||
},
|
||||
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleBatchUpdateSubmit (e) {
|
||||
e.preventDefault()
|
||||
const p = []
|
||||
if (this.addTransferKeys && this.addTransferKeys.length) {
|
||||
p.push(this.bindAttribute(this.addTransferKeys))
|
||||
}
|
||||
|
||||
if (this.removeTransferKeys && this.removeTransferKeys.length) {
|
||||
p.push(this.unbindAttribute(this.removeTransferKeys))
|
||||
}
|
||||
const that = this
|
||||
Promise.all(p).then(function (values) {
|
||||
console.log(values)
|
||||
that.$message.success(`修改成功`)
|
||||
that.handleOk()
|
||||
that.onBatchBindAttrActionClose()
|
||||
}).catch(err => that.requestFailed(err))
|
||||
},
|
||||
|
||||
bindAttribute (attrIds) {
|
||||
return createCITypeAttributes(this.CITypeId, { attr_id: attrIds })
|
||||
},
|
||||
unbindAttribute (attrIds) {
|
||||
return deleteCITypeAttributesById(this.CITypeId, { attr_id: attrIds })
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
props: {
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ant-transfer {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.fixedWidthTable table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.fixedWidthTable .ant-table-tbody > tr > td {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
349
cmdb-ui/src/views/cmdb/modeling/ci_type/checkTable.vue
Normal file
349
cmdb-ui/src/views/cmdb/modeling/ci_type/checkTable.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-button class="action-btn" @click="handleCreate" type="primary">批量修改</a-button>
|
||||
<s-table
|
||||
v-once
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:pagination="pagination"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:showPagination="showPagination"
|
||||
ref="table"
|
||||
size="middle"
|
||||
:scroll="scroll"
|
||||
>
|
||||
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleDelete(record)">删除</a>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="visible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-transfer
|
||||
:dataSource="transferData"
|
||||
:render="item=>item.title"
|
||||
:selectedKeys="transferSelectedKeys"
|
||||
:targetKeys="transferTargetKeys"
|
||||
:titles="['当前项', '已选项']"
|
||||
:listStyle="{
|
||||
height: '600px',
|
||||
width: '40%',
|
||||
|
||||
}"
|
||||
showSearch
|
||||
@change="handleTransferChange"
|
||||
@scroll="handleTransferScroll"
|
||||
@selectChange="handleTransferSelectChange"
|
||||
/>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { STable } from '@/components'
|
||||
import { getCITypeAttributesByName, updateCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'CheckTable',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
|
||||
form: this.$form.createForm(this),
|
||||
scroll: { x: 900, y: 600 },
|
||||
|
||||
visible: false,
|
||||
|
||||
drawerTitle: '',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
transferData: [],
|
||||
transferTargetKeys: [],
|
||||
transferSelectedKeys: [],
|
||||
originTargetKeys: [],
|
||||
|
||||
attributes: [],
|
||||
|
||||
pagination: {
|
||||
defaultPageSize: 20
|
||||
},
|
||||
showPagination: false,
|
||||
columns: [
|
||||
{
|
||||
title: '属性名',
|
||||
dataIndex: 'alias',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'alias' }
|
||||
},
|
||||
{
|
||||
title: '属性英文名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: '必须',
|
||||
dataIndex: 'is_required',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
console.log('loadData.parameter', parameter)
|
||||
return getCITypeAttributesByName(this.CITypeName)
|
||||
.then(res => {
|
||||
this.attributes = res.attributes
|
||||
this.setTransferData()
|
||||
|
||||
return {
|
||||
data: this.attributes.filter(o => o.is_required)
|
||||
}
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
},
|
||||
computed: {
|
||||
|
||||
removeTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return originTargetKeys.filter(v => !originTargetKeys.includes(v) || !transferTargetKeys.includes(v))
|
||||
},
|
||||
|
||||
addTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return transferTargetKeys.filter(v => !transferTargetKeys.includes(v) || !originTargetKeys.includes(v))
|
||||
},
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
setTransferData () {
|
||||
const data = []
|
||||
const target = []
|
||||
this.attributes.forEach(
|
||||
function (i) {
|
||||
data.push({
|
||||
key: i.id.toString(),
|
||||
title: i.alias,
|
||||
description: ''
|
||||
})
|
||||
if (i.is_required) {
|
||||
target.push(i.id.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
this.transferData = data
|
||||
this.transferTargetKeys = target
|
||||
this.originTargetKeys = target
|
||||
},
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleTransferChange (nextTargetKeys, direction, moveKeys) {
|
||||
this.transferTargetKeys = nextTargetKeys
|
||||
|
||||
console.log('targetKeys: ', nextTargetKeys)
|
||||
console.log('direction: ', direction)
|
||||
console.log('moveKeys: ', moveKeys)
|
||||
console.log('addTransferKeys: ', this.addTransferKeys)
|
||||
console.log('removeTransferKeys: ', this.removeTransferKeys)
|
||||
},
|
||||
|
||||
handleTransferSelectChange (sourceSelectedKeys, targetSelectedKeys) {
|
||||
this.transferSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
|
||||
|
||||
console.log('sourceSelectedKeys: ', sourceSelectedKeys)
|
||||
console.log('targetSelectedKeys: ', targetSelectedKeys)
|
||||
},
|
||||
handleTransferScroll (direction, e) {
|
||||
console.log('direction:', direction)
|
||||
console.log('target:', e.target)
|
||||
},
|
||||
|
||||
handleDelete (record) {
|
||||
console.log(record)
|
||||
|
||||
updateCITypeAttributesById(this.CITypeId, { attributes: [{ attr_id: record.id, is_required: false }] })
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.drawerTitle = '批量修改'
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e.target.checked}`)
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.addTransferKeys || this.removeTransferKeys) {
|
||||
const requestData = []
|
||||
this.addTransferKeys.forEach(function (k) {
|
||||
const data = { attr_id: k, is_required: true }
|
||||
requestData.push(data)
|
||||
})
|
||||
|
||||
this.removeTransferKeys.forEach(function (k) {
|
||||
const data = { attr_id: k, is_required: false }
|
||||
requestData.push(data)
|
||||
})
|
||||
|
||||
const CITypeId = this.CITypeId
|
||||
|
||||
updateCITypeAttributesById(CITypeId, { attributes: requestData }).then(
|
||||
res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.visible = false
|
||||
this.handleOk()
|
||||
}
|
||||
).catch(err => {
|
||||
this.requestFailed(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ant-transfer {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
355
cmdb-ui/src/views/cmdb/modeling/ci_type/defaultShowTable.vue
Normal file
355
cmdb-ui/src/views/cmdb/modeling/ci_type/defaultShowTable.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-button class="action-btn" @click="handleCreate" type="primary">批量修改</a-button>
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:pagination="pagination"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:showPagination="showPagination"
|
||||
ref="table"
|
||||
size="middle"
|
||||
:scroll="scroll"
|
||||
|
||||
>
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleDelete(record)">删除</a>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="visible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-transfer
|
||||
:dataSource="transferData"
|
||||
:render="item=>item.title"
|
||||
:selectedKeys="transferSelectedKeys"
|
||||
:targetKeys="transferTargetKeys"
|
||||
:titles="['当前项', '已选项']"
|
||||
:listStyle="{
|
||||
height: '600px',
|
||||
width: '42%'
|
||||
|
||||
}"
|
||||
showSearch
|
||||
@change="handleTransferChange"
|
||||
@scroll="handleTransferScroll"
|
||||
@selectChange="handleTransferSelectChange"
|
||||
/>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { STable } from '@/components'
|
||||
import { getCITypeAttributesByName, updateCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'DefaultShowTable',
|
||||
components: {
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
|
||||
form: this.$form.createForm(this),
|
||||
scroll: { x: 1000, y: 600 },
|
||||
|
||||
visible: false,
|
||||
|
||||
drawerTitle: '',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
transferData: [],
|
||||
transferTargetKeys: [],
|
||||
transferSelectedKeys: [],
|
||||
originTargetKeys: [],
|
||||
|
||||
attributes: [],
|
||||
|
||||
pagination: {
|
||||
defaultPageSize: 20
|
||||
},
|
||||
showPagination: false,
|
||||
columns: [
|
||||
{
|
||||
title: '属性名',
|
||||
dataIndex: 'alias',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'alias' }
|
||||
},
|
||||
{
|
||||
title: '属性英文名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: '默认显示',
|
||||
dataIndex: 'default_show',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'is_check' }
|
||||
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
console.log('loadData.parameter', parameter)
|
||||
return getCITypeAttributesByName(this.CITypeName)
|
||||
.then(res => {
|
||||
this.attributes = res.attributes
|
||||
this.setTransferData()
|
||||
|
||||
return {
|
||||
data: this.attributes.filter(o => o.default_show)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
},
|
||||
computed: {
|
||||
|
||||
removeTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return originTargetKeys.filter(v => !originTargetKeys.includes(v) || !transferTargetKeys.includes(v))
|
||||
},
|
||||
|
||||
addTransferKeys () {
|
||||
const { originTargetKeys, transferTargetKeys } = this
|
||||
return transferTargetKeys.filter(v => !transferTargetKeys.includes(v) || !originTargetKeys.includes(v))
|
||||
},
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
setTransferData () {
|
||||
const data = []
|
||||
const target = []
|
||||
this.attributes.forEach(
|
||||
function (i) {
|
||||
data.push({
|
||||
key: i.id.toString(),
|
||||
title: i.alias,
|
||||
description: ''
|
||||
})
|
||||
if (i.default_show) {
|
||||
target.push(i.id.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
this.transferData = data
|
||||
this.transferTargetKeys = target
|
||||
this.originTargetKeys = target
|
||||
},
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 250
|
||||
},
|
||||
|
||||
handleTransferChange (nextTargetKeys, direction, moveKeys) {
|
||||
this.transferTargetKeys = nextTargetKeys
|
||||
|
||||
console.log('targetKeys: ', nextTargetKeys)
|
||||
console.log('direction: ', direction)
|
||||
console.log('moveKeys: ', moveKeys)
|
||||
console.log('addTransferKeys: ', this.addTransferKeys)
|
||||
console.log('removeTransferKeys: ', this.removeTransferKeys)
|
||||
},
|
||||
|
||||
handleTransferSelectChange (sourceSelectedKeys, targetSelectedKeys) {
|
||||
this.transferSelectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
|
||||
console.log('sourceSelectedKeys: ', sourceSelectedKeys)
|
||||
console.log('targetSelectedKeys: ', targetSelectedKeys)
|
||||
},
|
||||
handleTransferScroll (direction, e) {
|
||||
console.log('direction:', direction)
|
||||
console.log('target:', e.target)
|
||||
},
|
||||
|
||||
handleDelete (record) {
|
||||
console.log(record)
|
||||
|
||||
updateCITypeAttributesById(this.CITypeId, { attributes: [{ attr_id: record.id, default_show: false }] })
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
onSelectChange (selectedRowKeys, selectedRows) {
|
||||
this.selectedRowKeys = selectedRowKeys
|
||||
this.selectedRows = selectedRows
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.drawerTitle = '批量修改'
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e.target.checked}`)
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.addTransferKeys || this.removeTransferKeys) {
|
||||
const requestData = []
|
||||
this.addTransferKeys.forEach(function (k) {
|
||||
const data = { 'attr_id': k, 'default_show': true }
|
||||
requestData.push(data)
|
||||
})
|
||||
|
||||
this.removeTransferKeys.forEach(function (k) {
|
||||
const data = { 'attr_id': k, 'default_show': false }
|
||||
requestData.push(data)
|
||||
})
|
||||
|
||||
const CITypeId = this.CITypeId
|
||||
|
||||
updateCITypeAttributesById(CITypeId, { attributes: requestData }).then(
|
||||
res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.visible = false
|
||||
this.handleOk()
|
||||
}
|
||||
).catch(err => {
|
||||
this.requestFailed(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.ant-transfer {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
cmdb-ui/src/views/cmdb/modeling/ci_type/detail.vue
Normal file
89
cmdb-ui/src/views/cmdb/modeling/ci_type/detail.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<a-tabs defaultActiveKey="1">
|
||||
<a-tab-pane key="1" tab="模型属性">
|
||||
|
||||
<AttributesTable></AttributesTable>
|
||||
|
||||
</a-tab-pane>
|
||||
<a-tab-pane forceRender key="2" tab="模型关联">
|
||||
<RelationTable :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
|
||||
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane forceRender key="3" tab="必须校验">
|
||||
<CheckTable></CheckTable>
|
||||
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane forceRender key="4" tab="默认显示属性">
|
||||
<DefaultShowTable></DefaultShowTable>
|
||||
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane forceRender key="5" tab="属性分组 & 排序">
|
||||
<Group></Group>
|
||||
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import AttributesTable from './attributesTable'
|
||||
import RelationTable from './relationTable'
|
||||
import CheckTable from './checkTable'
|
||||
import DefaultShowTable from './defaultShowTable'
|
||||
import Group from './group'
|
||||
|
||||
export default {
|
||||
name: 'CITypeDetail',
|
||||
components: {
|
||||
STable,
|
||||
AttributesTable,
|
||||
RelationTable,
|
||||
CheckTable,
|
||||
DefaultShowTable,
|
||||
Group
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
CITypeName: this.$route.params.CITypeName
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
},
|
||||
mounted () {
|
||||
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
591
cmdb-ui/src/views/cmdb/modeling/ci_type/group.vue
Normal file
591
cmdb-ui/src/views/cmdb/modeling/ci_type/group.vue
Normal file
@@ -0,0 +1,591 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom: 2rem">
|
||||
<a-button type="primary" v-if="addGroupBtnVisible" @click="handleAddGroup">添加分组</a-button>
|
||||
|
||||
<template v-else>
|
||||
<span>
|
||||
<a-input
|
||||
size="small"
|
||||
type="text"
|
||||
style="width: 10rem;margin-right: 0.5rem"
|
||||
ref="addGroupInput"
|
||||
v-model.trim="newGroupName" />
|
||||
<a @click="handleCreateGroup" style="margin-right: 0.5rem">保存</a>
|
||||
<a @click="handleCancelCreateGroup">取消</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div :key="index" v-for="(CITypeGroup, index) in CITypeGroups">
|
||||
<div class="group-header">
|
||||
|
||||
<template style="margin-bottom: 2rem;" v-if="!CITypeGroup.editable">
|
||||
|
||||
<span style="margin-right: 0.2rem">{{ CITypeGroup.name }}</span>
|
||||
<span style="color: #c3cdd7; margin-right: 0.5rem">({{ CITypeGroup.attributes.length }})</span>
|
||||
|
||||
<a-button type="link" size="small" @click="handleEditGroupName(index, CITypeGroup)"><a-icon type="edit" /></a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span style="font-size: 1rem">
|
||||
<a-input
|
||||
size="small"
|
||||
type="text"
|
||||
style="width: 15%;margin-right: 0.5rem"
|
||||
ref="editGroupInput"
|
||||
v-model.trim="CITypeGroup.name" />
|
||||
<a @click="handleSaveGroupName(index, CITypeGroup)" style="margin-right: 0.5rem">保存</a>
|
||||
<a @click="handleCancelGroupName(index, CITypeGroup)">取消</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div style="float: right">
|
||||
|
||||
<a-button-group size="small">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
上移
|
||||
</template>
|
||||
<a-button icon="arrow-up" size="small" @click="handleMoveGroup(index, index-1)" :disabled="index===0"/>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
下移
|
||||
</template>
|
||||
<a-button icon="arrow-down" size="small" @click="handleMoveGroup(index, index+1)" :disabled="index===CITypeGroups.length-1" />
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
添加属性
|
||||
</template>
|
||||
<a-button icon="plus" size="small" @click="handleAddExistGroupAttr(index)"/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
删除分组
|
||||
</template>
|
||||
<a-button icon="delete" size="small" @click="handleDeleteGroup(CITypeGroup.id)" :disabled="CITypeGroup.attributes.length!==0" />
|
||||
|
||||
</a-tooltip>
|
||||
|
||||
</a-button-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="box"
|
||||
style="min-height: 2rem; margin-bottom: 1.5rem;"
|
||||
>
|
||||
|
||||
<draggable
|
||||
v-model="CITypeGroup.attributes"
|
||||
group="properties"
|
||||
@start="drag=true"
|
||||
@end="handleEnd"
|
||||
@change="handleChange"
|
||||
:filter="'.filter-empty'"
|
||||
:animation="100"
|
||||
tag="div"
|
||||
style="width: 100%; display: flex;flex-flow: wrap"
|
||||
>
|
||||
|
||||
<li
|
||||
class="property-item"
|
||||
v-for="item in CITypeGroup.attributes"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.alias }}
|
||||
</li>
|
||||
|
||||
<template
|
||||
v-if="!CITypeGroup.attributes.length"
|
||||
style="height: 2rem"
|
||||
>
|
||||
<li
|
||||
class="property-item-empty"
|
||||
@click="handleAddExistGroupAttr(index)"
|
||||
style="">添加属性</li>
|
||||
|
||||
</template>
|
||||
|
||||
</draggable>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="group-header">
|
||||
|
||||
<template>
|
||||
|
||||
<span style="margin-right: 0.2rem">更多属性</span>
|
||||
<span style="color: #c3cdd7; margin-right: 0.5rem">({{ otherGroupAttributes.length }})</span>
|
||||
</template>
|
||||
<div style="float: right">
|
||||
<a-button-group size="small">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
上移
|
||||
</template>
|
||||
<a-button icon="arrow-up" size="small" disabled/>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
下移
|
||||
</template>
|
||||
<a-button icon="arrow-down" size="small" disabled />
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
添加属性
|
||||
</template>
|
||||
<a-button icon="plus" size="small" @click="handleAddOtherGroupAttr"/>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
删除分组
|
||||
</template>
|
||||
<a-button icon="delete" size="small" disabled />
|
||||
|
||||
</a-tooltip>
|
||||
|
||||
</a-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<draggable
|
||||
v-model="otherGroupAttributes"
|
||||
group="properties"
|
||||
@start="drag=true"
|
||||
@end="handleEnd"
|
||||
@change="handleChange"
|
||||
:animation="0"
|
||||
style="min-height: 2rem; width: 100%; display: flex; flex-flow: wrap">
|
||||
|
||||
<li
|
||||
class="property-item"
|
||||
v-for="item in otherGroupAttributes"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.alias }}
|
||||
</li>
|
||||
|
||||
<template
|
||||
v-if="!otherGroupAttributes.length"
|
||||
style="display: block"
|
||||
>
|
||||
<li
|
||||
class="property-item-empty"
|
||||
@click="handleAddOtherGroupAttr"
|
||||
style="">添加属性</li>
|
||||
|
||||
</template>
|
||||
|
||||
</draggable>
|
||||
</div>
|
||||
<a-modal
|
||||
title="添加字段"
|
||||
:width="'80%'"
|
||||
v-model="modalVisible"
|
||||
@ok="handleSubmit"
|
||||
@cancel="modalVisible = false"
|
||||
|
||||
>
|
||||
<a-form :form="form" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
>
|
||||
<a-checkbox-group
|
||||
v-decorator="['checkedAttributes']"
|
||||
style="width: 90%"
|
||||
>
|
||||
|
||||
<a-row :gutter="{ xs: 8, sm: 16, md: 24}" type="flex" justify="start">
|
||||
<a-col
|
||||
v-for="attribute in attributes"
|
||||
:key="attribute.id"
|
||||
:sm="8"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xxl="3"
|
||||
style="line-height: 1.8rem"
|
||||
>
|
||||
<a-checkbox
|
||||
:value="attribute.id">
|
||||
{{ attribute.alias }}
|
||||
</a-checkbox>
|
||||
|
||||
</a-col>
|
||||
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="groupId"
|
||||
type="hidden"
|
||||
v-decorator="['groupId']"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="groupIndex"
|
||||
type="hidden"
|
||||
v-decorator="['groupIndex']"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
</div >
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {
|
||||
deleteCITypeGroupById,
|
||||
getCITypeGroupById,
|
||||
createCITypeGroupById,
|
||||
updateCITypeGroupById
|
||||
} from '@/api/cmdb/CIType'
|
||||
import { getCITypeAttributesById, updateCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
name: 'Group',
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
form: this.$form.createForm(this),
|
||||
CITypeId: this.$route.params.CITypeId,
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
CITypeGroups: [],
|
||||
attributes: [],
|
||||
otherGroupAttributes: [],
|
||||
addGroupBtnVisible: true,
|
||||
newGroupName: '',
|
||||
modalVisible: false
|
||||
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
},
|
||||
created () {
|
||||
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.getCITypeGroupData()
|
||||
},
|
||||
methods: {
|
||||
setOtherGroupAttributes () {
|
||||
const orderMap = this.attributes.reduce(function (map, obj) {
|
||||
map[obj.id] = obj.order
|
||||
return map
|
||||
}, {})
|
||||
|
||||
const inGroupAttrKeys = this.CITypeGroups
|
||||
.filter(x => x.attributes && x.attributes.length > 0)
|
||||
.map(x => x.attributes).flat().map(x => x.id)
|
||||
|
||||
this.CITypeGroups.forEach(group => {
|
||||
group.attributes.forEach(attribute => {
|
||||
attribute.order = orderMap[attribute.id]
|
||||
attribute.originOrder = attribute.order
|
||||
attribute.originGroupName = group.name
|
||||
})
|
||||
group.originCount = group.attributes.length
|
||||
group.editable = false
|
||||
group.originOrder = group.order
|
||||
group.originName = group.name
|
||||
group.attributes = group.attributes.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
this.otherGroupAttributes = this.attributes.filter(x => !inGroupAttrKeys.includes(x.id)).sort((a, b) => a.order - b.order)
|
||||
this.attributes = this.attributes.sort((a, b) => a.order - b.order)
|
||||
this.CITypeGroups = this.CITypeGroups.sort((a, b) => a.order - b.order)
|
||||
this.otherGroupAttributes.forEach(attribute => {
|
||||
attribute.originOrder = attribute.order
|
||||
})
|
||||
|
||||
console.log('setOtherGroupAttributes', this.CITypeGroups, this.otherGroupAttributes)
|
||||
},
|
||||
getCITypeGroupData () {
|
||||
const promises = [
|
||||
getCITypeAttributesById(this.CITypeId),
|
||||
getCITypeGroupById(this.CITypeId)
|
||||
]
|
||||
Promise.all(promises)
|
||||
.then(values => {
|
||||
this.attributes = values[0].attributes
|
||||
this.CITypeGroups = values[1]
|
||||
this.setOtherGroupAttributes()
|
||||
})
|
||||
},
|
||||
|
||||
handleEditGroupName (index, CITypeGroup) {
|
||||
CITypeGroup.editable = true
|
||||
this.$set(this.CITypeGroups, index, CITypeGroup)
|
||||
},
|
||||
handleSaveGroupName (index, CITypeGroup) {
|
||||
if (CITypeGroup.name === CITypeGroup.originName) {
|
||||
this.handleCancelGroupName(index, CITypeGroup)
|
||||
} else if (this.CITypeGroups.map(x => x.originName).includes(CITypeGroup.name)) {
|
||||
this.$message.error('分组名称已存在')
|
||||
} else {
|
||||
updateCITypeGroupById(CITypeGroup.id, { name: CITypeGroup.name, attributes: CITypeGroup.attributes.map(x => x.id), order: CITypeGroup.order })
|
||||
.then(res => {
|
||||
CITypeGroup.editable = false
|
||||
this.$set(this.CITypeGroups, index, CITypeGroup)
|
||||
this.$message.success('修改成功')
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
}
|
||||
},
|
||||
handleCancelGroupName (index, CITypeGroup) {
|
||||
CITypeGroup.editable = false
|
||||
this.$set(this.CITypeGroups, index, CITypeGroup)
|
||||
},
|
||||
|
||||
handleCancel (CITypeGroup) {
|
||||
CITypeGroup.editable = false
|
||||
},
|
||||
handleAddGroup () {
|
||||
this.addGroupBtnVisible = false
|
||||
},
|
||||
handleCreateGroup () {
|
||||
const groupOrders = this.CITypeGroups.map(x => x.order)
|
||||
|
||||
const maxGroupOrder = Math.max(groupOrders.length, groupOrders.length ? Math.max(...groupOrders) : 0)
|
||||
|
||||
console.log('groupOrder', groupOrders, 'maxOrder', maxGroupOrder)
|
||||
createCITypeGroupById(this.CITypeId, { name: this.newGroupName, order: maxGroupOrder + 1 })
|
||||
.then(res => {
|
||||
this.addGroupBtnVisible = true
|
||||
this.newGroupName = ''
|
||||
this.getCITypeGroupData()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleCancelCreateGroup () {
|
||||
this.addGroupBtnVisible = true
|
||||
this.newGroupName = ''
|
||||
},
|
||||
|
||||
handleMoveGroup (beforeIndex, afterIndex) {
|
||||
const beforeGroup = this.CITypeGroups[beforeIndex]
|
||||
this.CITypeGroups[beforeIndex] = this.CITypeGroups[afterIndex]
|
||||
|
||||
this.$set(this.CITypeGroups, beforeIndex, this.CITypeGroups[afterIndex])
|
||||
this.$set(this.CITypeGroups, afterIndex, beforeGroup)
|
||||
|
||||
this.updatePropertyIndex()
|
||||
},
|
||||
handleAddExistGroupAttr (index) {
|
||||
const group = this.CITypeGroups[index]
|
||||
this.modalVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
checkedAttributes: group.attributes.map(x => x.id),
|
||||
groupId: group.id,
|
||||
groupIndex: index
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleAddOtherGroupAttr () {
|
||||
this.modalVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
checkedAttributes: this.otherGroupAttributes.map(x => x.id),
|
||||
groupId: null
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Received values of form: ', values)
|
||||
|
||||
this.CITypeGroups.forEach(group => {
|
||||
if (group.id === values.groupId) {
|
||||
group.attributes = this.attributes.filter(x => values.checkedAttributes.includes(x.id))
|
||||
} else {
|
||||
group.attributes = group.attributes.filter(x => !values.checkedAttributes.includes(x.id))
|
||||
}
|
||||
})
|
||||
// this.CITypeGroups = this.CITypeGroups
|
||||
|
||||
this.otherGroupAttributes.forEach(attributes => {
|
||||
if (values.groupId === null) {
|
||||
this.otherGroupAttributes = this.otherGroupAttributes.filter(x => values.checkedAttributes.includes(x.id))
|
||||
} else {
|
||||
this.otherGroupAttributes = this.otherGroupAttributes.filter(x => !values.checkedAttributes.includes(x.id))
|
||||
}
|
||||
})
|
||||
|
||||
console.log('add group attribute', this.otherGroupAttributes, this.CITypeGroups)
|
||||
this.updatePropertyIndex()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleDeleteGroup (groupId) {
|
||||
deleteCITypeGroupById(groupId)
|
||||
.then(res => {
|
||||
this.updatePropertyIndex()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleChange (e) {
|
||||
console.log(e)
|
||||
if (e.hasOwnProperty('moved')) {
|
||||
this.shouldUpdatePropertyIndex = e.moved.newIndex !== e.moved.oldIndex
|
||||
} else {
|
||||
this.shouldUpdatePropertyIndex = true
|
||||
}
|
||||
},
|
||||
handleEnd (e) {
|
||||
if (this.shouldUpdatePropertyIndex) {
|
||||
this.updatePropertyIndex()
|
||||
this.shouldUpdatePropertyIndex = false
|
||||
}
|
||||
},
|
||||
updatePropertyIndex () {
|
||||
const attributes = []
|
||||
let attributeOrder = 0
|
||||
let groupOrder = 0
|
||||
const promises = [
|
||||
|
||||
]
|
||||
|
||||
this.CITypeGroups.forEach(group => {
|
||||
const groupName = group.name
|
||||
|
||||
let groupAttributes = []
|
||||
let groupUpdate = false
|
||||
group.order = groupOrder
|
||||
|
||||
group.attributes.forEach(attribute => {
|
||||
groupAttributes.push(attribute.id)
|
||||
|
||||
if (attribute.originGroupName !== group.name || attribute.originOrder !== attributeOrder) {
|
||||
attributes.push({ attr_id: attribute.id, order: attributeOrder })
|
||||
groupUpdate = true
|
||||
}
|
||||
attributeOrder++
|
||||
})
|
||||
|
||||
groupAttributes = new Set(groupAttributes)
|
||||
if (group.originCount !== groupAttributes.size || groupUpdate || group.originOrder !== group.order) {
|
||||
promises.push(updateCITypeGroupById(group.id, { name: groupName, attributes: [...groupAttributes], order: groupOrder }))
|
||||
}
|
||||
groupOrder++
|
||||
})
|
||||
|
||||
this.otherGroupAttributes.forEach(attribute => {
|
||||
if (attribute.originOrder !== attributeOrder) {
|
||||
console.log('this attribute:', attribute.name, 'old order', attribute.originOrder, 'new order', attributeOrder)
|
||||
attributes.push({ attr_id: attribute.id, order: attributeOrder })
|
||||
}
|
||||
|
||||
attributeOrder++
|
||||
})
|
||||
|
||||
if (attributes && attributes.length > 0) {
|
||||
promises.unshift(updateCITypeAttributesById(this.CITypeId, { attributes: attributes }))
|
||||
}
|
||||
|
||||
const that = this
|
||||
Promise.all(promises)
|
||||
.then(values => {
|
||||
that.$message.success(`修改成功`)
|
||||
that.getCITypeGroupData()
|
||||
that.modalVisible = false
|
||||
})
|
||||
.catch(err => that.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.property-item {
|
||||
width: calc(20% - 2rem);
|
||||
margin:0.5rem 0.8rem;
|
||||
border:1px solid #d9d9d9;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.property-item:hover{
|
||||
border:1px dashed #40a9ff;
|
||||
}
|
||||
|
||||
.property-item-empty {
|
||||
width: calc(100% - 10px);
|
||||
margin:0.5rem 0.8rem;
|
||||
border:1px dashed #d9d9d9;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
height: 3.5rem;
|
||||
line-height: 3.5rem;
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
312
cmdb-ui/src/views/cmdb/modeling/ci_type/list.vue
Normal file
312
cmdb-ui/src/views/cmdb/modeling/ci_type/list.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<a-list
|
||||
:dataSource="CITypes"
|
||||
:grid="{ gutter: 20, column: 4 }"
|
||||
class="ci-type-list"
|
||||
itemLayout="horizontal"
|
||||
size="small"
|
||||
>
|
||||
<a-list-item slot="renderItem" slot-scope="item">
|
||||
|
||||
<template v-if="Object.keys(item).length === 0">
|
||||
<a-button class="new-btn" type="dashed" @click="handleCreate">
|
||||
<a-icon type="plus"/>
|
||||
新增
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-card
|
||||
:bodyStyle="{padding: '0.9rem 0.8rem'}"
|
||||
:hoverable="true"
|
||||
>
|
||||
<template class="ant-card-actions" slot="actions">
|
||||
<router-link
|
||||
:to="{ name: 'ci_type_detail', params: { CITypeName: item.name, CITypeId: item.id }}"
|
||||
>
|
||||
<a-icon type="setting" />
|
||||
</router-link>
|
||||
<a-icon type="edit" @click="handleEdit(item)"/>
|
||||
<a-popconfirm title="确认删除" @confirm="handleDelete(item)" okText="是" cancelText="否">
|
||||
<a-icon type="delete"/>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
<a-card-meta>
|
||||
<div slot="title" style="margin-bottom: 3px">{{ item.alias || item.name }}</div>
|
||||
<a-avatar
|
||||
:src="item.icon_url"
|
||||
class="card-avatar"
|
||||
size="large"
|
||||
slot="avatar"
|
||||
style="color: #7f9eb2; backgroundColor: #e1eef6; padding-left: -1rem;"
|
||||
>
|
||||
{{ item.name[0].toUpperCase() }}
|
||||
</a-avatar>
|
||||
<div class="meta-content" slot="description">{{ item.name }}</div>
|
||||
</a-card-meta>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
</a-list-item>
|
||||
|
||||
</a-list>
|
||||
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="模型名(英文)"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder="英文"
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入属性名'},{message: '不能以数字开头,可以是英文 数字以及下划线 (_)', pattern: RegExp('^(?!\\d)[a-zA-Z_0-9]+$')}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="别名"
|
||||
>
|
||||
<a-input
|
||||
name="alias"
|
||||
v-decorator="['alias', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="唯一标识*"
|
||||
>
|
||||
|
||||
<a-select
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
name="unique_key"
|
||||
style="width: 200px"
|
||||
:filterOption="filterOption"
|
||||
v-decorator="['unique_key', {rules: [{required: true}], } ]"
|
||||
|
||||
>
|
||||
<a-select-option :key="item.id" :value="item.id" v-for="item in allAttributes">{{ item.alias || item.name }}</a-select-option>
|
||||
</a-select>
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { getCITypes, createCIType, updateCIType, deleteCIType } from '@/api/cmdb/CIType'
|
||||
import { searchAttributes } from '@/api/cmdb/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'CITypeList',
|
||||
components: {},
|
||||
data () {
|
||||
return {
|
||||
CITypes: [],
|
||||
allAttributes: [],
|
||||
form: this.$form.createForm(this),
|
||||
|
||||
drawerVisible: false,
|
||||
|
||||
drawerTitle: '',
|
||||
|
||||
formLayout: 'vertical'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getCITypes()
|
||||
this.getAttributes()
|
||||
},
|
||||
methods: {
|
||||
|
||||
filterOption (input, option) {
|
||||
return (
|
||||
option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
},
|
||||
getCITypes () {
|
||||
getCITypes().then(res => {
|
||||
this.CITypes = res.ci_types
|
||||
this.CITypes.unshift({})
|
||||
})
|
||||
},
|
||||
|
||||
getAttributes () {
|
||||
searchAttributes({ page_size: 10000 }).then(res => {
|
||||
this.allAttributes = res.attributes
|
||||
})
|
||||
},
|
||||
handleCreate () {
|
||||
this.drawerTitle = '新增模型'
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
handleEdit (record) {
|
||||
this.drawerTitle = '编辑模型'
|
||||
this.drawerVisible = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
alias: record.alias,
|
||||
name: record.name,
|
||||
unique_key: record.unique_id
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleDelete (record) {
|
||||
deleteCIType(record.id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.getCITypes()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Received values of form: ', values)
|
||||
|
||||
if (values.id) {
|
||||
this.updateCIType(values.id, values)
|
||||
} else {
|
||||
this.createCIType(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
createCIType (data) {
|
||||
createCIType(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.drawerVisible = false
|
||||
this.getCITypes()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
updateCIType (CITypeId, data) {
|
||||
updateCIType(CITypeId, data)
|
||||
.then(res => {
|
||||
this.$message.success(`修改成功`)
|
||||
this.drawerVisible = false
|
||||
this.getCITypes()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
props: {},
|
||||
watch: {}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.new-btn {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 7.5rem;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
338
cmdb-ui/src/views/cmdb/modeling/ci_type/relationTable.vue
Normal file
338
cmdb-ui/src/views/cmdb/modeling/ci_type/relationTable.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-button class="action-btn" @click="handleCreate" type="primary">新增关系</a-button>
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:pagination="pagination"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:showPagination="showPagination"
|
||||
ref="table"
|
||||
size="middle"
|
||||
:scroll="scroll"
|
||||
>
|
||||
|
||||
<span slot="is_check" slot-scope="text">
|
||||
<a-icon type="check" v-if="text"/>
|
||||
</span>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleDelete(record)">删除</a>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="visible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="源模型"
|
||||
>
|
||||
<a-select
|
||||
name="source_ci_type_id"
|
||||
style="width: 120px"
|
||||
v-decorator="['source_ci_type_id', {rules: [], } ]"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes" v-if="CITypeId === CIType.id">{{ CIType.alias }}</a-select-option>
|
||||
</a-select>
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="目标模型"
|
||||
>
|
||||
<a-select
|
||||
name="ci_type_id"
|
||||
style="width: 120px"
|
||||
v-decorator="['ci_type_id', {rules: [], } ]"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes">{{ CIType.alias }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="关联关系"
|
||||
>
|
||||
<a-select
|
||||
name="relation_type_id"
|
||||
style="width: 120px"
|
||||
v-decorator="['relation_type_id', {rules: [], } ]"
|
||||
>
|
||||
<a-select-option :value="relationType.id" :key="relationType.id" v-for="relationType in relationTypes">{{ relationType.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createRelation, deleteRelation, getCITypeChildren } from '@/api/cmdb/CITypeRelation'
|
||||
import { getRelationTypes } from '@/api/cmdb/relationType'
|
||||
import { getCITypes } from '@/api/cmdb/CIType'
|
||||
|
||||
import { STable } from '@/components'
|
||||
import PageView from '@/layouts/PageView'
|
||||
|
||||
export default {
|
||||
name: 'RelationTable',
|
||||
components: {
|
||||
PageView,
|
||||
STable
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
CITypeId: parseInt(this.$route.params.CITypeId),
|
||||
CITypeName: this.$route.params.CITypeName,
|
||||
form: this.$form.createForm(this),
|
||||
scroll: { x: 1300, y: 600 },
|
||||
|
||||
visible: false,
|
||||
|
||||
drawerTitle: '',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
CITypes: [],
|
||||
relationTypes: [],
|
||||
|
||||
pagination: {
|
||||
defaultPageSize: 20
|
||||
},
|
||||
showPagination: false,
|
||||
columns: [
|
||||
{
|
||||
title: '源模型英文名',
|
||||
dataIndex: 'source_ci_type_name',
|
||||
sorter: false,
|
||||
width: 200,
|
||||
scopedSlots: { customRender: 'source_ci_type_name' }
|
||||
},
|
||||
{
|
||||
title: '关联类型',
|
||||
dataIndex: 'relation_type',
|
||||
sorter: false,
|
||||
width: 100,
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: '目标模型名',
|
||||
dataIndex: 'alias',
|
||||
sorter: false,
|
||||
scopedSlots: { customRender: 'alias' }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
console.log('loadData.parameter', parameter)
|
||||
const CITypeId = this.CITypeId
|
||||
const CITypeName = this.CITypeName
|
||||
|
||||
console.log('this CITypeId ', CITypeId, 'type', typeof CITypeName, 'CITypeName', CITypeName)
|
||||
|
||||
return getCITypeChildren(this.CITypeId)
|
||||
.then(res => {
|
||||
let data = res.children
|
||||
|
||||
data = data.map(function (obj, index) {
|
||||
obj.source_ci_type_name = CITypeName
|
||||
obj.source_ci_type_id = CITypeId
|
||||
return obj
|
||||
})
|
||||
|
||||
return {
|
||||
data: data
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
},
|
||||
computed: {
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getCITypes()
|
||||
this.getRelationTypes()
|
||||
},
|
||||
methods: {
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 250
|
||||
},
|
||||
|
||||
getCITypes () {
|
||||
getCITypes().then(res => {
|
||||
this.CITypes = res.ci_types
|
||||
})
|
||||
},
|
||||
getRelationTypes () {
|
||||
getRelationTypes().then(res => {
|
||||
this.relationTypes = res
|
||||
})
|
||||
},
|
||||
|
||||
callback (key) {
|
||||
console.log(key)
|
||||
},
|
||||
handleDelete (record) {
|
||||
console.log(record)
|
||||
|
||||
deleteRelation(record.source_ci_type_id, record.id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
|
||||
this.handleOk()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.drawerTitle = '新增关系'
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
source_ci_type_id: this.CITypeId
|
||||
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e.target.checked}`)
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Received values of form: ', values)
|
||||
|
||||
createRelation(values.source_ci_type_id, values.ci_type_id, values.relation_type_id)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.onClose()
|
||||
this.handleOk()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
346
cmdb-ui/src/views/cmdb/modeling/preference_relation/index.vue
Normal file
346
cmdb-ui/src/views/cmdb/modeling/preference_relation/index.vue
Normal file
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-card :bordered="true" title="关系视图定义面板">
|
||||
<a-card-meta description="方法1. 右键选择树节点(推荐)"></a-card-meta>
|
||||
<a-card-meta description="方法2. 先打开右上角的开关,再选择树的节点"></a-card-meta>
|
||||
<a-switch
|
||||
slot="extra"
|
||||
@change="toggleSelect"
|
||||
checkedChildren="on"
|
||||
unCheckedChildren="off"
|
||||
/>
|
||||
<div
|
||||
id="visualization"
|
||||
style="height:400px"
|
||||
@mousedown="mouseDown"
|
||||
@mouseup="mouseUp"
|
||||
@mousemove="mouseMove">
|
||||
</div>
|
||||
<relation-view-form @refresh="reload" ref="relationViewForm"></relation-view-form>
|
||||
</a-card>
|
||||
|
||||
<a-row :gutter="0">
|
||||
<a-col
|
||||
:xl="12"
|
||||
:lg="12"
|
||||
:md="12"
|
||||
:sm="24"
|
||||
:xs="24"
|
||||
:key="view"
|
||||
v-for="view in Object.keys(relationViews.views)">
|
||||
<a-card :bordered="true" :title="view">
|
||||
<a slot="extra"><a-icon type="close" @click="deleteView(view)"></a-icon></a>
|
||||
<div :id=""view-" + (relationViews.views[view].topo_flatten || []).join("")" style="height:200px"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DataSet, Network } from 'vis-network'
|
||||
import { getCITypeRelations } from '@/api/cmdb/CITypeRelation'
|
||||
import { getRelationView, deleteRelationView } from '@/api/cmdb/preference'
|
||||
import RelationViewForm from './modules/RelationViewForm'
|
||||
import { notification } from 'ant-design-vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RelationViewForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
relationViews: { views: {} },
|
||||
relations: [],
|
||||
network: null,
|
||||
options: {},
|
||||
viewData: {},
|
||||
container: null,
|
||||
nodes: null,
|
||||
edges: null,
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
drag: false,
|
||||
canSelect: false,
|
||||
rect: {},
|
||||
drawingSurfaceImageData: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.create()
|
||||
},
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
create () {
|
||||
getRelationView().then(res => {
|
||||
this.relationViews = res
|
||||
this.createRelationViews()
|
||||
})
|
||||
getCITypeRelations().then(res => {
|
||||
// create an array with nodes
|
||||
this.relations = res
|
||||
const nodes = []
|
||||
const edges = []
|
||||
const nodeMap = {}
|
||||
res.forEach(item => {
|
||||
if (!(item.child.id in nodeMap)) {
|
||||
nodes.push({
|
||||
id: item.child.id,
|
||||
label: item.child.alias
|
||||
})
|
||||
}
|
||||
|
||||
if (!(item.parent.id in nodeMap)) {
|
||||
nodes.push({
|
||||
id: item.parent.id,
|
||||
label: item.parent.alias
|
||||
})
|
||||
}
|
||||
|
||||
nodeMap[item.child.id] = 1
|
||||
nodeMap[item.parent.id] = 1
|
||||
|
||||
edges.push({
|
||||
from: item.parent.id,
|
||||
to: item.child.id,
|
||||
label: item.relation_type.name
|
||||
})
|
||||
})
|
||||
const _nodes = new DataSet(nodes)
|
||||
|
||||
const _edges = new DataSet(edges)
|
||||
this.nodes = _nodes
|
||||
this.edges = _edges
|
||||
// create a network
|
||||
this.container = document.querySelector('#visualization')
|
||||
|
||||
// provide the data in the vis format
|
||||
var data = {
|
||||
nodes: _nodes,
|
||||
edges: _edges
|
||||
}
|
||||
var options = {
|
||||
layout: { randomSeed: 2 },
|
||||
autoResize: true,
|
||||
nodes: {
|
||||
size: 30,
|
||||
font: {
|
||||
size: 14
|
||||
},
|
||||
borderWidth: 2
|
||||
},
|
||||
edges: {
|
||||
width: 2,
|
||||
smooth: {
|
||||
enabled: false
|
||||
},
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: false
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
dragView: true,
|
||||
dragNodes: true,
|
||||
multiselect: true
|
||||
}
|
||||
}
|
||||
|
||||
// initialize your network!
|
||||
this.container.oncontextmenu = () => { return false }
|
||||
this.options = options
|
||||
this.viewData = data
|
||||
this.network = new Network(this.container, data, options)
|
||||
this.canvas = this.network.canvas.frame.canvas
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
})
|
||||
},
|
||||
|
||||
toggleSelect (checked) {
|
||||
if (checked) {
|
||||
this.canSelect = true
|
||||
this.options.autoResize = false
|
||||
this.options.interaction.hover = false
|
||||
this.options.interaction.dragView = false
|
||||
this.options.interaction.dragNodes = false
|
||||
this.network = new Network(this.container, this.viewData, this.options)
|
||||
this.canvas = this.network.canvas.frame.canvas
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
} else {
|
||||
this.canSelect = false
|
||||
this.options.autoResize = true
|
||||
this.options.interaction.hover = true
|
||||
this.options.interaction.dragView = true
|
||||
this.options.interaction.dragNodes = true
|
||||
this.network = new Network(this.container, this.viewData, this.options)
|
||||
this.canvas = this.network.canvas.frame.canvas
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
}
|
||||
},
|
||||
|
||||
createRelationViews () {
|
||||
Object.keys(this.relationViews.views).forEach(viewName => {
|
||||
const nodes = []
|
||||
const edges = []
|
||||
const len = this.relationViews.views[viewName].topo_flatten.length
|
||||
this.relationViews.views[viewName].topo_flatten.slice(0, len - 1).forEach((fromId, idx) => {
|
||||
nodes.push({
|
||||
id: fromId,
|
||||
label: this.relationViews.id2type[fromId].alias
|
||||
})
|
||||
edges.push({
|
||||
from: fromId,
|
||||
to: this.relationViews.views[viewName].topo_flatten[idx + 1]
|
||||
})
|
||||
})
|
||||
nodes.push({
|
||||
id: this.relationViews.views[viewName].topo_flatten[len - 1],
|
||||
label: this.relationViews.id2type[this.relationViews.views[viewName].topo_flatten[len - 1]].alias
|
||||
})
|
||||
const _nodes = new DataSet(nodes)
|
||||
const _edges = new DataSet(edges)
|
||||
var data = {
|
||||
nodes: _nodes,
|
||||
edges: _edges
|
||||
}
|
||||
|
||||
var options = {
|
||||
layout: { randomSeed: 2 },
|
||||
nodes: {
|
||||
size: 30,
|
||||
font: {
|
||||
size: 14
|
||||
},
|
||||
borderWidth: 2
|
||||
},
|
||||
edges: {
|
||||
width: 2,
|
||||
smooth: {
|
||||
enabled: false
|
||||
},
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
scaleFactor: 0.5
|
||||
}
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
enabled: false
|
||||
},
|
||||
interaction: {
|
||||
hover: true,
|
||||
dragView: false,
|
||||
dragNodes: false
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
const container = document.querySelector('#view-' + this.relationViews.views[viewName].topo_flatten.join(''))
|
||||
const n = new Network(container, data, options)
|
||||
console.log(n) // TODO
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
|
||||
toggleCreate (nodeIds) {
|
||||
const crIds = []
|
||||
this.relations.forEach(item => {
|
||||
if (nodeIds.includes(item.parent_id) && nodeIds.includes(item.child_id)) {
|
||||
crIds.push({
|
||||
parent_id: item.parent_id,
|
||||
child_id: item.child_id
|
||||
})
|
||||
}
|
||||
})
|
||||
this.$refs.relationViewForm.handleCreate(crIds)
|
||||
},
|
||||
|
||||
saveDrawingSurface () {
|
||||
this.drawingSurfaceImageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
|
||||
},
|
||||
|
||||
restoreDrawingSurface () {
|
||||
this.ctx.putImageData(this.drawingSurfaceImageData, 0, 0)
|
||||
},
|
||||
|
||||
selectNodesFromHighlight () {
|
||||
var nodesIdInDrawing = []
|
||||
var xRange = this.getStartToEnd(this.rect.startX, this.rect.w)
|
||||
var yRange = this.getStartToEnd(this.rect.startY, this.rect.h)
|
||||
|
||||
var allNodes = this.nodes.get()
|
||||
for (var i = 0; i < allNodes.length; i++) {
|
||||
var curNode = allNodes[i]
|
||||
var nodePosition = this.network.getPositions([curNode.id])
|
||||
var nodeXY = this.network.canvasToDOM({ x: nodePosition[curNode.id].x, y: nodePosition[curNode.id].y })
|
||||
if (xRange.start <= nodeXY.x && nodeXY.x <= xRange.end && yRange.start <= nodeXY.y && nodeXY.y <= yRange.end) {
|
||||
nodesIdInDrawing.push(curNode.id)
|
||||
}
|
||||
}
|
||||
this.toggleCreate(nodesIdInDrawing)
|
||||
this.network.selectNodes(nodesIdInDrawing)
|
||||
},
|
||||
|
||||
getStartToEnd (start, theLen) {
|
||||
return theLen > 0 ? { start: start, end: start + theLen } : { start: start + theLen, end: start }
|
||||
},
|
||||
|
||||
mouseMove () {
|
||||
if (this.drag) {
|
||||
this.restoreDrawingSurface()
|
||||
this.rect.w = event.offsetX - this.rect.startX
|
||||
this.rect.h = event.offsetY - this.rect.startY
|
||||
|
||||
this.ctx.setLineDash([5])
|
||||
this.ctx.strokeStyle = 'rgb(0, 102, 0)'
|
||||
this.ctx.strokeRect(this.rect.startX, this.rect.startY, this.rect.w, this.rect.h)
|
||||
this.ctx.setLineDash([])
|
||||
this.ctx.fillStyle = 'rgba(0, 255, 0, 0.2)'
|
||||
this.ctx.fillRect(this.rect.startX, this.rect.startY, this.rect.w, this.rect.h)
|
||||
}
|
||||
},
|
||||
|
||||
mouseDown () {
|
||||
if ((event.button === 0 && this.canSelect) || event.button === 2) {
|
||||
this.saveDrawingSurface()
|
||||
this.rect.startX = event.offsetX
|
||||
this.rect.startY = event.offsetY
|
||||
this.drag = true
|
||||
this.container.style.cursor = 'crosshair'
|
||||
}
|
||||
},
|
||||
|
||||
mouseUp () {
|
||||
if ((event.button === 0 && this.canSelect) || event.button === 2) {
|
||||
this.restoreDrawingSurface()
|
||||
this.drag = false
|
||||
|
||||
this.container.style.cursor = 'default'
|
||||
this.selectNodesFromHighlight()
|
||||
}
|
||||
},
|
||||
|
||||
deleteView (viewName) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: '确认要删除吗 ?',
|
||||
onOk () {
|
||||
deleteRelationView(viewName).then(res => {
|
||||
that.create()
|
||||
}).catch(e => {
|
||||
notification.error({ message: e.reponse.data.message })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="关系视图名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder=""
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入 关系视图名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { subscribeRelationView } from '@/api/cmdb/preference'
|
||||
|
||||
export default {
|
||||
name: 'RelationViewForm',
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增关系视图',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical',
|
||||
crIds: []
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
handleCreate (crIds) {
|
||||
this.crIds = crIds
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
console.log('Received values of form: ', values)
|
||||
this.createRelationView(values)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
createRelationView (data) {
|
||||
data.cr_ids = this.crIds
|
||||
subscribeRelationView(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.onClose()
|
||||
this.$emit('refresh')
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
console.log(err, 'error')
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
280
cmdb-ui/src/views/cmdb/modeling/relation_type/index.vue
Normal file
280
cmdb-ui/src/views/cmdb/modeling/relation_type/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
|
||||
<div class="action-btn">
|
||||
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
|
||||
</div>
|
||||
|
||||
<s-table
|
||||
:alert="options.alert"
|
||||
:columns="columns"
|
||||
:data="loadData"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
:showPagination="false"
|
||||
:pageSize="25"
|
||||
:rowKey="record=>record.id"
|
||||
:rowSelection="options.rowSelection"
|
||||
:scroll="scroll"
|
||||
ref="table"
|
||||
size="middle"
|
||||
|
||||
>
|
||||
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
|
||||
<a-input
|
||||
v-ant-ref="c => searchInput = c"
|
||||
:placeholder="` ${column.title}`"
|
||||
:value="selectedKeys[0]"
|
||||
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
|
||||
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="() => handleSearch(selectedKeys, confirm, column)"
|
||||
icon="search"
|
||||
size="small"
|
||||
style="width: 90px; margin-right: 8px"
|
||||
>搜索</a-button>
|
||||
<a-button
|
||||
@click="() => handleReset(clearFilters, column)"
|
||||
size="small"
|
||||
style="width: 90px"
|
||||
>重置</a-button>
|
||||
</div>
|
||||
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
|
||||
|
||||
<template slot="nameSearchRender" slot-scope="text">
|
||||
<span v-if="columnSearchText.name">
|
||||
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
|
||||
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
|
||||
<template v-else>{{ fragment }}</template>
|
||||
</template>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
</template>
|
||||
|
||||
<template slot="description" slot-scope="text">{{ text }}</template>
|
||||
|
||||
<span slot="action" slot-scope="text, record">
|
||||
<template>
|
||||
<a @click="handleEdit(record)">编辑</a>
|
||||
<a-divider type="vertical"/>
|
||||
|
||||
<a-popconfirm
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(record)"
|
||||
@cancel="cancel"
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a>删除</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</span>
|
||||
|
||||
</s-table>
|
||||
<relation-type-form ref="relationTypeForm" :handleOk="handleOk"> </relation-type-form>
|
||||
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
import RelationTypeForm from './modules/relationTypeForm'
|
||||
import { getRelationTypes, deleteRelationType } from '@/api/cmdb/relationType'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
STable,
|
||||
RelationTypeForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scroll: { x: 1000, y: 500 },
|
||||
btnName: '新增关系类型',
|
||||
|
||||
formLayout: 'vertical',
|
||||
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
|
||||
columnSearchText: {
|
||||
name: ''
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
width: 150,
|
||||
title: '类型名',
|
||||
dataIndex: 'name',
|
||||
sorter: false,
|
||||
scopedSlots: {
|
||||
customRender: 'nameSearchRender',
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon'
|
||||
},
|
||||
onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
|
||||
onFilterDropdownVisibleChange: (visible) => {
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
this.searchInput.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
width: 150,
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
scopedSlots: { customRender: 'action' }
|
||||
}
|
||||
],
|
||||
loadData: parameter => {
|
||||
return getRelationTypes()
|
||||
.then(res => {
|
||||
const result = {}
|
||||
result.data = res
|
||||
console.log('loadData.res', result)
|
||||
return result
|
||||
})
|
||||
},
|
||||
|
||||
mdl: {},
|
||||
// 高级搜索 展开/关闭
|
||||
advanced: false,
|
||||
// 查询参数
|
||||
queryParam: {},
|
||||
// 表头
|
||||
|
||||
selectedRowKeys: [],
|
||||
selectedRows: [],
|
||||
|
||||
// custom table alert & rowSelection
|
||||
options: {
|
||||
alert: false,
|
||||
rowSelection: null
|
||||
},
|
||||
optionAlertShow: false
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
this.setScrollY()
|
||||
},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
handleSearch (selectedKeys, confirm, column) {
|
||||
confirm()
|
||||
this.columnSearchText[column.dataIndex] = selectedKeys[0]
|
||||
this.queryParam[column.dataIndex] = selectedKeys[0]
|
||||
},
|
||||
|
||||
handleReset (clearFilters, column) {
|
||||
clearFilters()
|
||||
this.columnSearchText[column.dataIndex] = ''
|
||||
this.queryParam[column.dataIndex] = ''
|
||||
},
|
||||
|
||||
setScrollY () {
|
||||
this.scroll.y = window.innerHeight - this.$refs.table.$el.offsetTop - 200
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
console.log(record)
|
||||
this.$refs.relationTypeForm.handleEdit(record)
|
||||
},
|
||||
handleDelete (record) {
|
||||
this.deleteRelationType(record.id)
|
||||
},
|
||||
handleOk () {
|
||||
this.$refs.table.refresh()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$refs.relationTypeForm.handleCreate()
|
||||
},
|
||||
|
||||
deleteRelationType (id) {
|
||||
deleteRelationType(id)
|
||||
.then(res => {
|
||||
this.$message.success(`删除成功`)
|
||||
this.handleOk()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
},
|
||||
cancel () {
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
watch: {}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
// .fold {
|
||||
// width: calc(100% - 216px);
|
||||
// display: inline-block
|
||||
// }
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.custom-filter-dropdown {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 192, 105);
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:closable="false"
|
||||
:title="drawerTitle"
|
||||
:visible="drawerVisible"
|
||||
@close="onClose"
|
||||
placement="right"
|
||||
width="30%"
|
||||
>
|
||||
|
||||
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
|
||||
|
||||
<a-form-item
|
||||
:label-col="formItemLayout.labelCol"
|
||||
:wrapper-col="formItemLayout.wrapperCol"
|
||||
label="类型名"
|
||||
>
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder=""
|
||||
v-decorator="['name', {rules: [{ required: true, message: '请输入类型名'}]} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
name="id"
|
||||
type="hidden"
|
||||
v-decorator="['id', {rules: []} ]"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '0.8rem 1rem',
|
||||
background: '#fff',
|
||||
|
||||
}"
|
||||
>
|
||||
<a-button @click="handleSubmit" type="primary" style="margin-right: 1rem">确定</a-button>
|
||||
<a-button @click="onClose">取消</a-button>
|
||||
|
||||
</div>
|
||||
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRelationType, updateRelationType } from '@/api/cmdb/relationType'
|
||||
|
||||
export default {
|
||||
name: 'RelationTypeForm',
|
||||
data () {
|
||||
return {
|
||||
drawerTitle: '新增关系类型',
|
||||
drawerVisible: false,
|
||||
formLayout: 'vertical'
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
|
||||
computed: {
|
||||
formItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
labelCol: { span: 4 },
|
||||
wrapperCol: { span: 14 }
|
||||
} : {}
|
||||
},
|
||||
|
||||
horizontalFormItemLayout () {
|
||||
return {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 12 }
|
||||
}
|
||||
},
|
||||
buttonItemLayout () {
|
||||
const { formLayout } = this
|
||||
return formLayout === 'horizontal' ? {
|
||||
wrapperCol: { span: 14, offset: 4 }
|
||||
} : {}
|
||||
}
|
||||
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
handleCreate () {
|
||||
this.drawerVisible = true
|
||||
},
|
||||
onClose () {
|
||||
this.form.resetFields()
|
||||
this.drawerVisible = false
|
||||
},
|
||||
onChange (e) {
|
||||
console.log(`checked = ${e}`)
|
||||
},
|
||||
|
||||
handleEdit (record) {
|
||||
this.drawerVisible = true
|
||||
console.log(record)
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
name: record.name
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
console.log('Received values of form: ', values)
|
||||
|
||||
if (values.id) {
|
||||
this.updateRelationType(values.id, values)
|
||||
} else {
|
||||
this.createRelationType(values)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRelationType (id, data) {
|
||||
updateRelationType(id, data)
|
||||
.then(res => {
|
||||
this.$message.success(`更新成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
}).catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
createRelationType (data) {
|
||||
addRelationType(data)
|
||||
.then(res => {
|
||||
this.$message.success(`添加成功`)
|
||||
this.handleOk()
|
||||
this.onClose()
|
||||
})
|
||||
.catch(err => this.requestFailed(err))
|
||||
},
|
||||
|
||||
requestFailed (err) {
|
||||
const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
|
||||
this.$message.error(`${msg}`)
|
||||
}
|
||||
|
||||
},
|
||||
watch: {},
|
||||
props: {
|
||||
handleOk: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search {
|
||||
margin-bottom: 54px;
|
||||
}
|
||||
|
||||
.fold {
|
||||
width: calc(100% - 216px);
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.operator {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.action-btn {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.fold {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
cmdb-ui/src/views/cmdb/preference/index.vue
Normal file
366
cmdb-ui/src/views/cmdb/preference/index.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<a-card>
|
||||
<div class="card-list" ref="content">
|
||||
<a-list :grid="{gutter: 24, lg: 4, md: 3, sm: 2, xs: 1}" :dataSource="citypeData.ci_types">
|
||||
<a-list-item slot="renderItem" slot-scope="item" v-if="!item.is_attached && item.enabled">
|
||||
<template>
|
||||
<a-card :hoverable="true">
|
||||
<a-card-meta>
|
||||
<a-avatar
|
||||
class="card-avatar"
|
||||
slot="avatar"
|
||||
:src="item.avatar"
|
||||
:size="32"
|
||||
:style="item.is_subscribed ? 'color: #FFF; backgroundColor: green' : 'color: #FFF; backgroundColor: lightgrey'"
|
||||
>{{ item.avatar || item.name[0].toUpperCase() }}</a-avatar>
|
||||
<span class="margin-bottom: 3px" slot="title">{{ item.alias || item.name }}</span>
|
||||
<span
|
||||
:class="item.is_subscribed?'subscribe-success':'unsubscribe'"
|
||||
slot="title"
|
||||
>{{ item.is_subscribed ? "已订阅" : "未订阅" }}</span>
|
||||
</a-card-meta>
|
||||
<template class="ant-card-actions" slot="actions">
|
||||
<a :disabled="!item.is_subscribed" @click="unsubscribe(item.id)">取消</a>
|
||||
<a @click="showDrawer(item.id, item.alias || item.name)">订阅</a>
|
||||
</template>
|
||||
</a-card>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-drawer
|
||||
:title="'订阅模型: ' + typeName"
|
||||
:width="600"
|
||||
@close="onClose"
|
||||
:visible="visible"
|
||||
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
|
||||
>
|
||||
<a-alert message="既可以定义树形视图, 也可以订阅资源视图, 资源视图会在SideBar单独呈现" type="info" showIcon />
|
||||
<a-divider>
|
||||
树形视图
|
||||
<span
|
||||
v-if="treeSubscribed"
|
||||
style="font-weight: 500; font-size: 12px; color: green"
|
||||
>已订阅</span>
|
||||
<span style="font-weight: 500; font-size: 12px; color: red" v-else>未订阅</span>
|
||||
</a-divider>
|
||||
<a-select
|
||||
ref="tree"
|
||||
mode="multiple"
|
||||
placeholder="- - 目录层级的选择 - -"
|
||||
:value="treeViews"
|
||||
style="width: 100%"
|
||||
@change="handleTreeSub"
|
||||
>
|
||||
<a-select-option v-for="attr in attrList" :key="attr.title">{{ attr.title }}</a-select-option>
|
||||
</a-select>
|
||||
<a-button
|
||||
@click="subTreeSubmit"
|
||||
type="primary"
|
||||
:style="{float: 'right', marginTop: '10px'}"
|
||||
>订阅</a-button>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<a-divider>
|
||||
资源视图
|
||||
<span
|
||||
v-if="instanceSubscribed"
|
||||
style="font-weight: 500; font-size: 12px; color: green"
|
||||
>已订阅</span>
|
||||
<span style="font-weight: 500; font-size: 12px; color: red" v-else>未订阅</span>
|
||||
</a-divider>
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="attrList"
|
||||
:showSearch="true"
|
||||
:listStyle="{
|
||||
width: '230px',
|
||||
height: '500px',
|
||||
}"
|
||||
:titles="['未选属性','已选属性']"
|
||||
:render="item=>item.title"
|
||||
:targetKeys="selectedAttrList"
|
||||
@change="handleChange"
|
||||
@search="handleSearch"
|
||||
>
|
||||
<span slot="notFoundContent">没数据</span>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<div
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
borderTop: '1px solid #e9e9e9',
|
||||
padding: '10px 16px',
|
||||
background: '#fff',
|
||||
textAlign: 'right',
|
||||
}"
|
||||
>
|
||||
<a-button :style="{marginRight: '8px'}" @click="onClose">取消</a-button>
|
||||
<a-button @click="subInstanceSubmit" type="primary">订阅</a-button>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router, { resetRouter } from '@/router'
|
||||
import store from '@/store'
|
||||
import { notification } from 'ant-design-vue'
|
||||
import { getCITypes } from '@/api/cmdb/CIType'
|
||||
import {
|
||||
getPreference,
|
||||
subscribeCIType,
|
||||
getSubscribeAttributes,
|
||||
getSubscribeTreeView,
|
||||
subscribeTreeView
|
||||
} from '@/api/cmdb/preference'
|
||||
import { getCITypeAttributesByName } from '@/api/cmdb/CITypeAttr'
|
||||
import { Promise } from 'q'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
visible: false,
|
||||
typeId: null,
|
||||
typeName: null,
|
||||
instanceSubscribed: false,
|
||||
treeSubscribed: false,
|
||||
citypeData: {},
|
||||
attrList: [],
|
||||
selectedAttrList: [],
|
||||
treeViews: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getCITypes()
|
||||
},
|
||||
methods: {
|
||||
getCITypes () {
|
||||
getCITypes().then(res => {
|
||||
getPreference(true, true).then(pref => {
|
||||
pref.forEach(item => {
|
||||
res.ci_types.forEach(ciType => {
|
||||
if (item.id === ciType.id) {
|
||||
ciType.is_subscribed = true
|
||||
}
|
||||
})
|
||||
})
|
||||
this.citypeData = res
|
||||
})
|
||||
})
|
||||
},
|
||||
unsubscribe (citypeId) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: '真的要取消订阅吗 ?',
|
||||
onOk () {
|
||||
const unsubCIType = subscribeCIType(citypeId, '')
|
||||
const unsubTree = subscribeTreeView(citypeId, '')
|
||||
Promise.all([unsubCIType, unsubTree])
|
||||
.then(() => {
|
||||
notification.success({
|
||||
message: '取消成功'
|
||||
})
|
||||
that.resetRoute()
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
onCancel () {}
|
||||
})
|
||||
},
|
||||
// 显示右边的弹出框
|
||||
showDrawer (typeId, typeName) {
|
||||
this.typeId = typeId
|
||||
this.typeName = typeName
|
||||
this.getAttrList()
|
||||
this.getTreeView(typeId)
|
||||
},
|
||||
onClose () {
|
||||
this.visible = false
|
||||
this.resetRoute()
|
||||
},
|
||||
getAttrList () {
|
||||
getCITypeAttributesByName(this.typeId).then(res => {
|
||||
const attributes = res.attributes
|
||||
getSubscribeAttributes(this.typeId).then(_res => {
|
||||
const attrList = []
|
||||
const selectedAttrList = []
|
||||
const subAttributes = _res.attributes
|
||||
this.instanceSubscribed = _res.is_subscribed
|
||||
subAttributes.forEach(item => {
|
||||
selectedAttrList.push(item.id.toString())
|
||||
})
|
||||
|
||||
attributes.forEach(item => {
|
||||
const data = {
|
||||
key: item.id.toString(),
|
||||
title: item.alias || item.name
|
||||
}
|
||||
attrList.push(data)
|
||||
})
|
||||
|
||||
this.attrList = attrList
|
||||
this.selectedAttrList = selectedAttrList
|
||||
this.visible = true
|
||||
})
|
||||
})
|
||||
},
|
||||
handleTreeSub (values) {
|
||||
this.treeViews = values
|
||||
},
|
||||
// 处理点击改变事件
|
||||
handleChange (targetKeys, direction, moveKeys) {
|
||||
this.selectedAttrList = targetKeys
|
||||
},
|
||||
handleSearch (dir, value) {},
|
||||
// 处理提交事件
|
||||
subInstanceSubmit () {
|
||||
subscribeCIType(this.typeId, this.selectedAttrList)
|
||||
.then(res => {
|
||||
notification.success({
|
||||
message: '订阅成功'
|
||||
})
|
||||
this.resetRoute()
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
getTreeView (typeId) {
|
||||
const that = this
|
||||
this.treeViews = []
|
||||
getSubscribeTreeView()
|
||||
.then(res => {
|
||||
let hasMatch = false
|
||||
res.forEach(item => {
|
||||
if (item.type_id === typeId) {
|
||||
hasMatch = true
|
||||
const levels = []
|
||||
if (item.levels && item.levels.length >= 1) {
|
||||
item.levels.forEach(level => {
|
||||
levels.push(level.alias)
|
||||
})
|
||||
}
|
||||
if (levels.length > 0) {
|
||||
that.treeSubscribed = true
|
||||
} else {
|
||||
that.treeSubscribed = false
|
||||
}
|
||||
that.treeViews = levels
|
||||
}
|
||||
})
|
||||
if (!hasMatch) {
|
||||
that.treeSubscribed = false
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
subTreeSubmit () {
|
||||
subscribeTreeView(this.typeId, this.treeViews)
|
||||
.then(res => {
|
||||
notification.success({
|
||||
message: '订阅成功'
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
notification.error({
|
||||
message: e.response.data.message
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
resetRoute () {
|
||||
resetRouter()
|
||||
const roles = store.getters.roles
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
router.addRoutes(store.getters.addRouters)
|
||||
this.getCITypes()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 48px;
|
||||
}
|
||||
|
||||
.ant-card-actions {
|
||||
background: #f7f9fa;
|
||||
li {
|
||||
float: left;
|
||||
text-align: center;
|
||||
margin: 12px 0;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
width: 50%;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
line-height: 22px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-btn {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 188px;
|
||||
}
|
||||
|
||||
.meta-content {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
height: 64px;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.subscribe-success {
|
||||
float: right;
|
||||
color: green;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.unsubscribe {
|
||||
float: right;
|
||||
color: gray;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
441
cmdb-ui/src/views/cmdb/relation_views/index.vue
Normal file
441
cmdb-ui/src/views/cmdb/relation_views/index.vue
Normal file
@@ -0,0 +1,441 @@
|
||||
<template>
|
||||
<a-card :bordered="false" class="relation-card">
|
||||
<a-menu v-model="currentView" mode="horizontal" v-if="relationViews.name2id && relationViews.name2id.length">
|
||||
<a-menu-item :key="item[1]" v-for="item in relationViews.name2id">
|
||||
<router-link
|
||||
:to="{name: 'cmdb_relation_views_item', params: { viewId: item[1]} }"
|
||||
>{{ item[0] }}</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-alert message="管理员 还未配置关系视图, 或者你无权限访问!" banner v-else-if="relationViews.name2id && !relationViews.name2id.length"></a-alert>
|
||||
<div style="clear: both; margin-top: 20px"></div>
|
||||
<template>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="5">
|
||||
<a-tree showLine :loadData="onLoadData" @select="onSelect" :treeData="treeData"></a-tree>
|
||||
</a-col>
|
||||
<a-col :span="19">
|
||||
<a-menu v-model="currentTypeId" mode="horizontal" v-if="showTypeIds && showTypeIds.length > 1">
|
||||
<a-menu-item :key="item.id" v-for="item in showTypes">
|
||||
<a @click="changeCIType(item.id)">{{ item.alias || item.name }}</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<search-form style="margin-top: 10px" ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
|
||||
<s-table
|
||||
v-if="levels.length"
|
||||
bordered
|
||||
ref="table"
|
||||
size="middle"
|
||||
rowKey="ci_id"
|
||||
:columns="columns"
|
||||
:data="loadInstances"
|
||||
:scroll="{ x: scrollX, y: scrollY }"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
:pageSize="25"
|
||||
showPagination="auto"
|
||||
></s-table>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
|
||||
import { getRelationView, getSubscribeAttributes } from '@/api/cmdb/preference'
|
||||
import { searchCIRelation, statisticsCIRelation } from '@/api/cmdb/CIRelation'
|
||||
import { searchCI } from '@/api/cmdb/ci'
|
||||
import SearchForm from '@/views/cmdb/ci/modules/SearchForm'
|
||||
export default {
|
||||
components: {
|
||||
STable,
|
||||
SearchForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
parameter: {},
|
||||
treeData: [],
|
||||
triggerSelect: false,
|
||||
treeNode: null,
|
||||
ciTypes: [],
|
||||
relationViews: {},
|
||||
levels: [],
|
||||
showTypeIds: [],
|
||||
origShowTypeIds: [],
|
||||
showTypes: [],
|
||||
origShowTypes: [],
|
||||
leaf2showTypes: {},
|
||||
node2ShowTypes: {},
|
||||
leaf: [],
|
||||
typeId: null,
|
||||
viewId: null,
|
||||
viewName: null,
|
||||
currentView: [],
|
||||
currentTypeId: [],
|
||||
instanceList: [],
|
||||
treeKeys: [],
|
||||
columns: [],
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
loading: false,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
preferenceAttrList: [],
|
||||
|
||||
loadInstances: async parameter => {
|
||||
console.log(parameter, 'load instances')
|
||||
this.parameter = parameter
|
||||
const params = Object.assign(parameter || {}, this.$refs.search.queryParam)
|
||||
let q = ''
|
||||
Object.keys(params).forEach(key => {
|
||||
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
|
||||
if (typeof params[key] === 'object' && params[key].length > 1) {
|
||||
q += `,${key}:(${params[key].join(';')})`
|
||||
} else if (params[key]) {
|
||||
q += `,${key}:${params[key]}`
|
||||
}
|
||||
|
||||
if (typeof params[key] === 'string') {
|
||||
q += '*'
|
||||
}
|
||||
}
|
||||
})
|
||||
if ('pageNo' in params) {
|
||||
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
|
||||
}
|
||||
|
||||
if ('sortField' in params) {
|
||||
let order = ''
|
||||
if (params['sortOrder'] !== 'ascend') {
|
||||
order = '-'
|
||||
}
|
||||
q += `&sort=${order}${params['sortField']}`
|
||||
}
|
||||
if (q && q[0] === ',') {
|
||||
q = q.slice(1)
|
||||
}
|
||||
if (this.treeKeys.length === 0) {
|
||||
await this.judgeCITypes(q)
|
||||
q = `q=_type:${this.currentTypeId[0]},` + q
|
||||
return searchCI(q).then(res => {
|
||||
const result = {}
|
||||
result.pageNo = res.page
|
||||
result.pageSize = res.total
|
||||
result.totalCount = res.numfound
|
||||
result.totalPage = Math.ceil(res.numfound / (params.pageSize || 25))
|
||||
result.data = Object.assign([], res.result)
|
||||
result.data.forEach((item, index) => (item.key = item.ci_id))
|
||||
if (res.numfound !== 0) {
|
||||
setTimeout(() => {
|
||||
this.setColumnWidth()
|
||||
console.log('set column')
|
||||
}, 200)
|
||||
}
|
||||
this.loadRoot()
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('_')[0]}`
|
||||
const typeId = parseInt(this.treeKeys[this.treeKeys.length - 1].split('_')[1])
|
||||
|
||||
let level = []
|
||||
if (!this.leaf.includes(typeId)) {
|
||||
let startIdx = 0
|
||||
this.levels.forEach((item, idx) => {
|
||||
if (item.includes(typeId)) {
|
||||
startIdx = idx
|
||||
}
|
||||
})
|
||||
|
||||
this.leaf.forEach(leafId => {
|
||||
this.levels.forEach((item, levelIdx) => {
|
||||
if (item.includes(leafId) && levelIdx - startIdx + 1 > 0) {
|
||||
level.push(levelIdx - startIdx + 1)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
level = [1]
|
||||
}
|
||||
q += `&level=${level.join(',')}`
|
||||
await this.judgeCITypes(q)
|
||||
q = `q=_type:${this.currentTypeId[0]},` + q
|
||||
return searchCIRelation(q).then(res => {
|
||||
const result = {}
|
||||
result.pageNo = res.page
|
||||
result.pageSize = res.total
|
||||
result.totalCount = res.numfound
|
||||
result.totalPage = Math.ceil(res.numfound / (params.pageSize || 25))
|
||||
result.data = Object.assign([], res.result)
|
||||
result.data.forEach((item, index) => (item.key = item.ci_id))
|
||||
if (res.numfound !== 0) {
|
||||
setTimeout(() => {
|
||||
this.setColumnWidth()
|
||||
console.log('set column')
|
||||
}, 200)
|
||||
}
|
||||
this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level)
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.getRelationViews()
|
||||
},
|
||||
|
||||
inject: ['reload'],
|
||||
watch: {
|
||||
'$route.path': function (newPath, oldPath) {
|
||||
this.viewId = this.$route.params.viewId
|
||||
this.getRelationViews()
|
||||
this.reload()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshTable (bool = false) {
|
||||
this.$refs.table.refresh(bool)
|
||||
},
|
||||
|
||||
changeCIType (typeId) {
|
||||
this.currentTypeId = [typeId]
|
||||
this.loadColumns(typeId)
|
||||
this.$refs.table.renderClear()
|
||||
setTimeout(() => {
|
||||
this.refreshTable(true)
|
||||
}, 200)
|
||||
},
|
||||
|
||||
async judgeCITypes (q) {
|
||||
const showTypeIds = []
|
||||
let _showTypes = []
|
||||
let _showTypeIds = []
|
||||
|
||||
if (this.treeKeys.length) {
|
||||
const typeId = parseInt(this.treeKeys[this.treeKeys.length - 1].split('_')[1])
|
||||
|
||||
_showTypes = this.node2ShowTypes[typeId + '']
|
||||
_showTypes.forEach(item => {
|
||||
_showTypeIds.push(item.id)
|
||||
})
|
||||
} else {
|
||||
_showTypeIds = JSON.parse(JSON.stringify(this.origShowTypeIds))
|
||||
_showTypes = JSON.parse(JSON.stringify(this.origShowTypes))
|
||||
}
|
||||
|
||||
const promises = _showTypeIds.map(typeId => {
|
||||
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
|
||||
if (this.treeKeys.length === 0) {
|
||||
return searchCI(_q).then(res => {
|
||||
if (res.numfound !== 0) {
|
||||
showTypeIds.push(typeId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return searchCIRelation(_q).then(res => {
|
||||
if (res.numfound !== 0) {
|
||||
showTypeIds.push(typeId)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) {
|
||||
const showTypes = []
|
||||
_showTypes.forEach(item => {
|
||||
if (showTypeIds.includes(item.id)) {
|
||||
showTypes.push(item)
|
||||
}
|
||||
})
|
||||
this.showTypes = showTypes
|
||||
this.showTypeIds = showTypeIds
|
||||
if (!this.currentTypeId.length || (this.currentTypeId.length && !this.showTypeIds.includes(this.currentTypeId[0]))) {
|
||||
this.currentTypeId = [this.showTypeIds[0]]
|
||||
this.loadColumns()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async loadRoot () {
|
||||
searchCI(`q=_type:(${this.levels[0].join(';')})&count=10000`).then(async res => {
|
||||
const facet = []
|
||||
const ciIds = []
|
||||
res.result.forEach(item => {
|
||||
facet.push([item[item.unique], 0, item.ci_id, item.type_id])
|
||||
ciIds.push(item.ci_id)
|
||||
})
|
||||
const promises = this.leaf.map(leafId => {
|
||||
let level = 0
|
||||
this.levels.forEach((item, idx) => {
|
||||
if (item.includes(leafId)) {
|
||||
level = idx + 1
|
||||
}
|
||||
})
|
||||
return statisticsCIRelation({ root_ids: ciIds.join(','), level: level }).then(num => {
|
||||
facet.forEach((item, idx) => {
|
||||
item[1] += num[ciIds[idx] + '']
|
||||
})
|
||||
})
|
||||
})
|
||||
await Promise.all(promises)
|
||||
this.wrapTreeData(facet)
|
||||
})
|
||||
},
|
||||
|
||||
async loadNoRoot (rootIdAndTypeId, level) {
|
||||
const rootId = rootIdAndTypeId.split('_')[0]
|
||||
searchCIRelation(`root_id=${rootId}&level=1&count=10000`).then(async res => {
|
||||
const facet = []
|
||||
const ciIds = []
|
||||
res.result.forEach(item => {
|
||||
facet.push([item[item.unique], 0, item.ci_id, item.type_id])
|
||||
ciIds.push(item.ci_id)
|
||||
})
|
||||
|
||||
const promises = level.map(_level => {
|
||||
if (_level > 1) {
|
||||
return statisticsCIRelation({ root_ids: ciIds.join(','), level: _level - 1 }).then(num => {
|
||||
facet.forEach((item, idx) => {
|
||||
item[1] += num[ciIds[idx] + '']
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
this.wrapTreeData(facet)
|
||||
})
|
||||
},
|
||||
|
||||
onSelect (keys) {
|
||||
this.triggerSelect = true
|
||||
if (keys.length) {
|
||||
this.treeKeys = keys[0].split('-').filter(item => item !== '')
|
||||
}
|
||||
|
||||
this.$refs.table.refresh(true)
|
||||
},
|
||||
wrapTreeData (facet) {
|
||||
if (this.triggerSelect) {
|
||||
return
|
||||
}
|
||||
const treeData = []
|
||||
facet.forEach(item => {
|
||||
treeData.push({
|
||||
title: `${item[0]} (${item[1]})`,
|
||||
key: this.treeKeys.join('-') + '-' + item[2] + '_' + item[3],
|
||||
isLeaf: this.leaf.includes(item[3])
|
||||
})
|
||||
})
|
||||
if (this.treeNode === null) {
|
||||
this.treeData = treeData
|
||||
} else {
|
||||
this.treeNode.dataRef.children = treeData
|
||||
this.treeData = [...this.treeData]
|
||||
}
|
||||
},
|
||||
setColumnWidth () {
|
||||
let rows = []
|
||||
try {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
|
||||
} catch (e) {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
|
||||
}
|
||||
let scrollX = 0
|
||||
const columns = Object.assign([], this.columns)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
columns[i].width = rows[i].offsetWidth < 80 ? 80 : rows[i].offsetWidth
|
||||
scrollX += columns[i].width
|
||||
}
|
||||
this.columns = columns
|
||||
|
||||
this.scrollX = scrollX
|
||||
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
|
||||
},
|
||||
|
||||
onLoadData (treeNode) {
|
||||
this.triggerSelect = false
|
||||
return new Promise(resolve => {
|
||||
if (treeNode.dataRef.children) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this.treeKeys = treeNode.eventKey.split('-').filter(item => item !== '')
|
||||
this.treeNode = treeNode
|
||||
this.$refs.table.refresh(true)
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
|
||||
getRelationViews () {
|
||||
getRelationView().then(res => {
|
||||
this.relationViews = res
|
||||
|
||||
if ((Object.keys(this.relationViews.views) || []).length) {
|
||||
this.viewId = parseInt(this.$route.params.viewId) || this.relationViews.name2id[0][1]
|
||||
this.relationViews.name2id.forEach(item => {
|
||||
if (item[1] === this.viewId) {
|
||||
this.viewName = item[0]
|
||||
}
|
||||
})
|
||||
this.levels = this.relationViews.views[this.viewName].topo
|
||||
this.origShowTypes = this.relationViews.views[this.viewName].show_types
|
||||
const showTypeIds = []
|
||||
this.origShowTypes.forEach(item => {
|
||||
showTypeIds.push(item.id)
|
||||
})
|
||||
this.origShowTypeIds = showTypeIds
|
||||
this.leaf2showTypes = this.relationViews.views[this.viewName].leaf2show_types
|
||||
this.node2ShowTypes = this.relationViews.views[this.viewName].node2show_types
|
||||
this.leaf = this.relationViews.views[this.viewName].leaf
|
||||
this.currentView = [this.viewId]
|
||||
this.typeId = this.levels[0][0]
|
||||
this.$refs.table && this.$refs.table.refresh(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
loadColumns () {
|
||||
getSubscribeAttributes(this.currentTypeId[0]).then(res => {
|
||||
const prefAttrList = res.attributes
|
||||
this.preferenceAttrList = prefAttrList
|
||||
|
||||
const columns = []
|
||||
prefAttrList.forEach((item, index) => {
|
||||
const col = {}
|
||||
col.title = item.alias
|
||||
col.dataIndex = item.name
|
||||
if (index !== prefAttrList.length - 1) {
|
||||
col.width = 80
|
||||
}
|
||||
if (item.is_sortable) {
|
||||
col.sorter = true
|
||||
}
|
||||
if (item.is_choice) {
|
||||
const filters = []
|
||||
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
|
||||
col.filters = filters
|
||||
}
|
||||
col.scopedSlots = { customRender: item.name }
|
||||
columns.push(col)
|
||||
})
|
||||
|
||||
this.columns = columns
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less'>
|
||||
.ant-menu-horizontal {
|
||||
border-bottom: 1px solid #ebedf0 !important;
|
||||
}
|
||||
.ant-menu-horizontal {
|
||||
border-bottom: 1px solid #ebedf0 !important;
|
||||
}
|
||||
.relation-card > .ant-card-body {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
258
cmdb-ui/src/views/cmdb/tree_views/index.vue
Normal file
258
cmdb-ui/src/views/cmdb/tree_views/index.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-menu v-model="current" mode="horizontal" v-if="ciTypes && ciTypes.length">
|
||||
<a-menu-item :key="ciType.id" v-for="ciType in ciTypes">
|
||||
<router-link
|
||||
:to="{name: 'cmdb_tree_views_item', params: {typeId: ciType.id}}"
|
||||
>{{ ciType.alias || ciTypes.name }}</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-alert message="请先到 我的订阅 页面完成订阅!" banner v-else-if="ciTypes && !ciTypes.length"></a-alert>
|
||||
<div style="clear: both; margin-top: 20px"></div>
|
||||
<template>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="5">
|
||||
<a-tree showLine :loadData="onLoadData" @select="onSelect" :treeData="treeData"></a-tree>
|
||||
</a-col>
|
||||
<a-col :span="19">
|
||||
<search-form ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
|
||||
<s-table
|
||||
v-if="ciTypes && ciTypes.length"
|
||||
bordered
|
||||
ref="table"
|
||||
size="middle"
|
||||
rowKey="ci_id"
|
||||
:columns="columns"
|
||||
:data="loadInstances"
|
||||
:scroll="{ x: scrollX, y: scrollY }"
|
||||
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
|
||||
:pageSize="25"
|
||||
showPagination="auto"
|
||||
></s-table>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { STable } from '@/components'
|
||||
|
||||
import { getSubscribeTreeView, getSubscribeAttributes } from '@/api/cmdb/preference'
|
||||
import { searchCI } from '@/api/cmdb/ci'
|
||||
import SearchForm from '@/views/cmdb/ci/modules/SearchForm'
|
||||
export default {
|
||||
components: {
|
||||
STable,
|
||||
SearchForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
treeData: [],
|
||||
triggerSelect: false,
|
||||
treeNode: null,
|
||||
ciTypes: null,
|
||||
levels: [],
|
||||
typeId: null,
|
||||
current: [],
|
||||
instanceList: [],
|
||||
treeKeys: [],
|
||||
columns: [],
|
||||
pageSizeOptions: ['10', '25', '50', '100'],
|
||||
loading: false,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
preferenceAttrList: [],
|
||||
|
||||
loadInstances: parameter => {
|
||||
const params = Object.assign(parameter || {}, this.$refs.search.queryParam)
|
||||
let q = `q=_type:${this.typeId}`
|
||||
Object.keys(params).forEach(key => {
|
||||
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
|
||||
if (typeof params[key] === 'object' && params[key].length > 1) {
|
||||
q += `,${key}:(${params[key].join(';')})`
|
||||
} else if (params[key]) {
|
||||
q += `,${key}:${params[key]}`
|
||||
}
|
||||
|
||||
if (typeof params[key] === 'string') {
|
||||
q += '*'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (this.treeKeys.length > 0) {
|
||||
this.treeKeys.forEach((item, idx) => {
|
||||
q += `,${this.levels[idx].name}:${item}`
|
||||
})
|
||||
}
|
||||
|
||||
if (this.levels.length > this.treeKeys.length) {
|
||||
q += `&facet=${this.levels[this.treeKeys.length].name}`
|
||||
}
|
||||
|
||||
if ('pageNo' in params) {
|
||||
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
|
||||
}
|
||||
|
||||
if ('sortField' in params) {
|
||||
let order = ''
|
||||
if (params['sortOrder'] !== 'ascend') {
|
||||
order = '-'
|
||||
}
|
||||
q += `&sort=${order}${params['sortField']}`
|
||||
}
|
||||
|
||||
return searchCI(q).then(res => {
|
||||
const result = {}
|
||||
result.pageNo = res.page
|
||||
result.pageSize = res.total
|
||||
result.totalCount = res.numfound
|
||||
result.totalPage = Math.ceil(res.numfound / (params.pageSize || 25))
|
||||
result.data = Object.assign([], res.result)
|
||||
result.data.forEach((item, index) => (item.key = item.ci_id))
|
||||
setTimeout(() => {
|
||||
this.setColumnWidth()
|
||||
}, 200)
|
||||
|
||||
if (Object.values(res.facet).length) {
|
||||
this.wrapTreeData(res.facet)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.getCITypes()
|
||||
},
|
||||
|
||||
inject: ['reload'],
|
||||
watch: {
|
||||
'$route.path': function (newPath, oldPath) {
|
||||
this.typeId = this.$route.params.typeId
|
||||
this.getCITypes()
|
||||
this.reload()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshTable (bool = false) {
|
||||
this.$refs.table.refresh(bool)
|
||||
},
|
||||
|
||||
onSelect (keys) {
|
||||
this.triggerSelect = true
|
||||
if (keys.length) {
|
||||
this.treeKeys = keys[0].split('-').filter(item => item !== '')
|
||||
}
|
||||
|
||||
this.$refs.table.refresh(true)
|
||||
},
|
||||
wrapTreeData (facet) {
|
||||
if (this.triggerSelect) {
|
||||
return
|
||||
}
|
||||
const treeData = []
|
||||
Object.values(facet)[0].forEach(item => {
|
||||
treeData.push({
|
||||
title: `${item[0]} (${item[1]})`,
|
||||
key: this.treeKeys.join('-') + '-' + item[0],
|
||||
isLeaf: this.levels.length - 1 === this.treeKeys.length
|
||||
})
|
||||
})
|
||||
if (this.treeNode === null) {
|
||||
this.treeData = treeData
|
||||
} else {
|
||||
this.treeNode.dataRef.children = treeData
|
||||
this.treeData = [...this.treeData]
|
||||
}
|
||||
},
|
||||
setColumnWidth () {
|
||||
let rows = []
|
||||
try {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
|
||||
} catch (e) {
|
||||
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
|
||||
}
|
||||
let scrollX = 0
|
||||
|
||||
const columns = Object.assign([], this.columns)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
columns[i].width = rows[i].offsetWidth < 80 ? 80 : rows[i].offsetWidth
|
||||
scrollX += columns[i].width
|
||||
}
|
||||
this.columns = columns
|
||||
|
||||
this.scrollX = scrollX
|
||||
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
|
||||
},
|
||||
|
||||
onLoadData (treeNode) {
|
||||
this.triggerSelect = false
|
||||
return new Promise(resolve => {
|
||||
if (treeNode.dataRef.children) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
this.treeKeys = treeNode.eventKey.split('-').filter(item => item !== '')
|
||||
this.treeNode = treeNode
|
||||
this.$refs.table.refresh(true)
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
|
||||
getCITypes () {
|
||||
getSubscribeTreeView().then(res => {
|
||||
this.ciTypes = res
|
||||
if (this.ciTypes.length) {
|
||||
this.typeId = this.$route.params.typeId || this.ciTypes[0].id
|
||||
this.current = [this.typeId]
|
||||
this.loadColumns()
|
||||
this.levels = res.find(item => item.id === this.typeId).levels
|
||||
this.$refs.table && this.$refs.table.refresh(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
loadColumns () {
|
||||
getSubscribeAttributes(this.typeId).then(res => {
|
||||
const prefAttrList = res.attributes
|
||||
this.preferenceAttrList = prefAttrList
|
||||
|
||||
const columns = []
|
||||
prefAttrList.forEach((item, index) => {
|
||||
const col = {}
|
||||
col.title = item.alias
|
||||
col.dataIndex = item.name
|
||||
if (index !== prefAttrList.length - 1) {
|
||||
col.width = 80
|
||||
}
|
||||
if (item.is_sortable) {
|
||||
col.sorter = true
|
||||
}
|
||||
if (item.is_choice) {
|
||||
const filters = []
|
||||
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
|
||||
col.filters = filters
|
||||
}
|
||||
col.scopedSlots = { customRender: item.name }
|
||||
columns.push(col)
|
||||
})
|
||||
|
||||
this.columns = columns
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ant-menu-horizontal {
|
||||
border-bottom: 1px solid #ebedf0 !important;
|
||||
}
|
||||
.ant-menu-horizontal {
|
||||
border-bottom: 1px solid #ebedf0 !important;
|
||||
}
|
||||
</style>
|
||||
17
cmdb-ui/src/views/exception/403.vue
Normal file
17
cmdb-ui/src/views/exception/403.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<exception-page type="403" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ExceptionPage } from '@/components'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExceptionPage
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
cmdb-ui/src/views/exception/404.vue
Normal file
17
cmdb-ui/src/views/exception/404.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<exception-page type="404" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ExceptionPage } from '@/components'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExceptionPage
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
cmdb-ui/src/views/exception/500.vue
Normal file
17
cmdb-ui/src/views/exception/500.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<exception-page type="500" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ExceptionPage } from '@/components'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExceptionPage
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
210
cmdb-ui/src/views/user/Login.vue
Normal file
210
cmdb-ui/src/views/user/Login.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<a-form
|
||||
id="formLogin"
|
||||
class="user-layout-login"
|
||||
ref="formLogin"
|
||||
:form="form"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<a-tabs
|
||||
:activeKey="customActiveKey"
|
||||
:tabBarStyle="{ textAlign: 'center', borderBottom: 'unset' }"
|
||||
@change="handleTabClick"
|
||||
>
|
||||
<a-tab-pane key="tab1" tab="账号密码登录">
|
||||
<a-form-item>
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
placeholder="用户名或者邮箱"
|
||||
v-decorator="[
|
||||
'username',
|
||||
{rules: [{ required: true, message: '请输入帐户名或邮箱地址' }, { validator: handleUsernameOrEmail }], validateTrigger: 'change'}
|
||||
]"
|
||||
>
|
||||
<a-icon slot="prefix" type="user" :style="{ color: 'rgba(0,0,0,.25)' }"/>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="false"
|
||||
placeholder="密码"
|
||||
v-decorator="[
|
||||
'password',
|
||||
{rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur'}
|
||||
]"
|
||||
>
|
||||
<a-icon slot="prefix" type="lock" :style="{ color: 'rgba(0,0,0,.25)' }"/>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-decorator="['rememberMe']">自动登录</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item style="margin-top:24px">
|
||||
<a-button
|
||||
size="large"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
class="login-button"
|
||||
:loading="state.loginBtn"
|
||||
:disabled="state.loginBtn"
|
||||
>确定</a-button>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import md5 from 'md5'
|
||||
import { mapActions } from 'vuex'
|
||||
import { timeFix } from '@/utils/util'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
customActiveKey: 'tab1',
|
||||
loginBtn: false,
|
||||
// login type: 0 email, 1 username, 2 telephone
|
||||
loginType: 0,
|
||||
requiredTwoStepCaptcha: false,
|
||||
stepCaptchaVisible: false,
|
||||
form: this.$form.createForm(this),
|
||||
state: {
|
||||
time: 60,
|
||||
loginBtn: false,
|
||||
// login type: 0 email, 1 username, 2 telephone
|
||||
loginType: 0,
|
||||
smsSendBtn: false
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['Login', 'Logout']),
|
||||
// handler
|
||||
handleUsernameOrEmail (rule, value, callback) {
|
||||
const { state } = this
|
||||
const regex = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/
|
||||
if (regex.test(value)) {
|
||||
state.loginType = 0
|
||||
} else {
|
||||
state.loginType = 1
|
||||
}
|
||||
callback()
|
||||
},
|
||||
handleTabClick (key) {
|
||||
this.customActiveKey = key
|
||||
// this.form.resetFields()
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
const {
|
||||
form: { validateFields },
|
||||
state,
|
||||
customActiveKey,
|
||||
Login
|
||||
} = this
|
||||
|
||||
state.loginBtn = true
|
||||
|
||||
const validateFieldsKey = customActiveKey === 'tab1' ? ['username', 'password'] : ['mobile', 'captcha']
|
||||
|
||||
validateFields(validateFieldsKey, { force: true }, (err, values) => {
|
||||
if (!err) {
|
||||
const loginParams = { ...values }
|
||||
delete loginParams.username
|
||||
loginParams[!state.loginType ? 'email' : 'username'] = values.username
|
||||
loginParams.password = md5(values.password)
|
||||
Login(loginParams)
|
||||
.then((res) => this.loginSuccess(res))
|
||||
.catch(err => this.requestFailed(err))
|
||||
.finally(() => {
|
||||
state.loginBtn = false
|
||||
})
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
state.loginBtn = false
|
||||
}, 600)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
loginSuccess (res) {
|
||||
this.$router.push({ path: this.$route.query.redirect })
|
||||
// 延迟 1 秒显示欢迎信息
|
||||
setTimeout(() => {
|
||||
this.$notification.success({
|
||||
message: '欢迎',
|
||||
description: `${timeFix()},欢迎回来`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
requestFailed (err) {
|
||||
this.$notification['error']({
|
||||
message: '错误',
|
||||
description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
|
||||
duration: 4
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-layout-login {
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.getCaptcha {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.forge-password {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button.login-button {
|
||||
padding: 0 15px;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-login-other {
|
||||
text-align: left;
|
||||
margin-top: 24px;
|
||||
line-height: 22px;
|
||||
|
||||
.item-icon {
|
||||
font-size: 24px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
322
cmdb-ui/src/views/user/Register.vue
Normal file
322
cmdb-ui/src/views/user/Register.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="main user-layout-register">
|
||||
<h3><span>注册</span></h3>
|
||||
<a-form ref="formRegister" :form="form" id="formRegister">
|
||||
<a-form-item>
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
placeholder="邮箱"
|
||||
v-decorator="['email', {rules: [{ required: true, type: 'email', message: '请输入邮箱地址' }], validateTrigger: ['change', 'blur']}]"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-popover
|
||||
placement="rightTop"
|
||||
:trigger="['focus']"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
v-model="state.passwordLevelChecked">
|
||||
<template slot="content">
|
||||
<div :style="{ width: '240px' }" >
|
||||
<div :class="['user-register', passwordLevelClass]">强度:<span>{{ passwordLevelName }}</span></div>
|
||||
<a-progress :percent="state.percent" :showInfo="false" :strokeColor=" passwordLevelColor " />
|
||||
<div style="margin-top: 10px;">
|
||||
<span>请至少输入 6 个字符。请不要使用容易被猜到的密码。</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-form-item>
|
||||
<a-input
|
||||
size="large"
|
||||
type="password"
|
||||
@click="handlePasswordInputClick"
|
||||
autocomplete="false"
|
||||
placeholder="至少6位密码,区分大小写"
|
||||
v-decorator="['password', {rules: [{ required: true, message: '至少6位密码,区分大小写'}, { validator: this.handlePasswordLevel }], validateTrigger: ['change', 'blur']}]"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-popover>
|
||||
|
||||
<a-form-item>
|
||||
<a-input
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="false"
|
||||
placeholder="确认密码"
|
||||
v-decorator="['password2', {rules: [{ required: true, message: '至少6位密码,区分大小写' }, { validator: this.handlePasswordCheck }], validateTrigger: ['change', 'blur']}]"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-input size="large" placeholder="11 位手机号" v-decorator="['mobile', {rules: [{ required: true, message: '请输入正确的手机号', pattern: /^1[3456789]\d{9}$/ }, { validator: this.handlePhoneCheck } ], validateTrigger: ['change', 'blur'] }]">
|
||||
<a-select slot="addonBefore" size="large" defaultValue="+86">
|
||||
<a-select-option value="+86">+86</a-select-option>
|
||||
<a-select-option value="+87">+87</a-select-option>
|
||||
</a-select>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<!--<a-input-group size="large" compact>
|
||||
<a-select style="width: 20%" size="large" defaultValue="+86">
|
||||
<a-select-option value="+86">+86</a-select-option>
|
||||
<a-select-option value="+87">+87</a-select-option>
|
||||
</a-select>
|
||||
<a-input style="width: 80%" size="large" placeholder="11 位手机号"></a-input>
|
||||
</a-input-group>-->
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col class="gutter-row" :span="16">
|
||||
<a-form-item>
|
||||
<a-input size="large" type="text" placeholder="验证码" v-decorator="['captcha', {rules: [{ required: true, message: '请输入验证码' }], validateTrigger: 'blur'}]">
|
||||
<a-icon slot="prefix" type="mail" :style="{ color: 'rgba(0,0,0,.25)' }"/>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col class="gutter-row" :span="8">
|
||||
<a-button
|
||||
class="getCaptcha"
|
||||
size="large"
|
||||
:disabled="state.smsSendBtn"
|
||||
@click.stop.prevent="getCaptcha"
|
||||
v-text="!state.smsSendBtn && '获取验证码'||(state.time+' s')"></a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
size="large"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
class="register-button"
|
||||
:loading="registerBtn"
|
||||
@click.stop.prevent="handleSubmit"
|
||||
:disabled="registerBtn">注册
|
||||
</a-button>
|
||||
<router-link class="login" :to="{ name: 'login' }">使用已有账户登录</router-link>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixinDevice } from '@/utils/mixin.js'
|
||||
import { getSmsCaptcha } from '@/api/login'
|
||||
|
||||
const levelNames = {
|
||||
0: '低',
|
||||
1: '低',
|
||||
2: '中',
|
||||
3: '强'
|
||||
}
|
||||
const levelClass = {
|
||||
0: 'error',
|
||||
1: 'error',
|
||||
2: 'warning',
|
||||
3: 'success'
|
||||
}
|
||||
const levelColor = {
|
||||
0: '#ff0000',
|
||||
1: '#ff0000',
|
||||
2: '#ff7e05',
|
||||
3: '#52c41a'
|
||||
}
|
||||
export default {
|
||||
name: 'Register',
|
||||
components: {
|
||||
},
|
||||
mixins: [mixinDevice],
|
||||
data () {
|
||||
return {
|
||||
form: this.$form.createForm(this),
|
||||
|
||||
state: {
|
||||
time: 60,
|
||||
smsSendBtn: false,
|
||||
passwordLevel: 0,
|
||||
passwordLevelChecked: false,
|
||||
percent: 10,
|
||||
progressColor: '#FF0000'
|
||||
},
|
||||
registerBtn: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordLevelClass () {
|
||||
return levelClass[this.state.passwordLevel]
|
||||
},
|
||||
passwordLevelName () {
|
||||
return levelNames[this.state.passwordLevel]
|
||||
},
|
||||
passwordLevelColor () {
|
||||
return levelColor[this.state.passwordLevel]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePasswordLevel (rule, value, callback) {
|
||||
let level = 0
|
||||
|
||||
// 判断这个字符串中有没有数字
|
||||
if (/[0-9]/.test(value)) {
|
||||
level++
|
||||
}
|
||||
// 判断字符串中有没有字母
|
||||
if (/[a-zA-Z]/.test(value)) {
|
||||
level++
|
||||
}
|
||||
// 判断字符串中有没有特殊符号
|
||||
if (/[^0-9a-zA-Z_]/.test(value)) {
|
||||
level++
|
||||
}
|
||||
this.state.passwordLevel = level
|
||||
this.state.percent = level * 30
|
||||
if (level >= 2) {
|
||||
if (level >= 3) {
|
||||
this.state.percent = 100
|
||||
}
|
||||
callback()
|
||||
} else {
|
||||
if (level === 0) {
|
||||
this.state.percent = 10
|
||||
}
|
||||
callback(new Error('密码强度不够'))
|
||||
}
|
||||
},
|
||||
|
||||
handlePasswordCheck (rule, value, callback) {
|
||||
const password = this.form.getFieldValue('password')
|
||||
console.log('value', value)
|
||||
if (value === undefined) {
|
||||
callback(new Error('请输入密码'))
|
||||
}
|
||||
if (value && password && value.trim() !== password.trim()) {
|
||||
callback(new Error('两次密码不一致'))
|
||||
}
|
||||
callback()
|
||||
},
|
||||
|
||||
handlePhoneCheck (rule, value, callback) {
|
||||
console.log('handlePhoneCheck, rule:', rule)
|
||||
console.log('handlePhoneCheck, value', value)
|
||||
console.log('handlePhoneCheck, callback', callback)
|
||||
|
||||
callback()
|
||||
},
|
||||
|
||||
handlePasswordInputClick () {
|
||||
if (!this.isMobile()) {
|
||||
this.state.passwordLevelChecked = true
|
||||
return
|
||||
}
|
||||
this.state.passwordLevelChecked = false
|
||||
},
|
||||
|
||||
handleSubmit () {
|
||||
const { form: { validateFields }, state, $router } = this
|
||||
validateFields({ force: true }, (err, values) => {
|
||||
if (!err) {
|
||||
state.passwordLevelChecked = false
|
||||
$router.push({ name: 'registerResult', params: { ...values } })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getCaptcha (e) {
|
||||
e.preventDefault()
|
||||
const { form: { validateFields }, state, $message, $notification } = this
|
||||
|
||||
validateFields(['mobile'], { force: true },
|
||||
(err, values) => {
|
||||
if (!err) {
|
||||
state.smsSendBtn = true
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
if (state.time-- <= 0) {
|
||||
state.time = 60
|
||||
state.smsSendBtn = false
|
||||
window.clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const hide = $message.loading('验证码发送中..', 0)
|
||||
|
||||
getSmsCaptcha({ mobile: values.mobile }).then(res => {
|
||||
setTimeout(hide, 2500)
|
||||
$notification['success']({
|
||||
message: '提示',
|
||||
description: '验证码获取成功,您的验证码为:' + res.result.captcha,
|
||||
duration: 8
|
||||
})
|
||||
}).catch(err => {
|
||||
setTimeout(hide, 1)
|
||||
clearInterval(interval)
|
||||
state.time = 60
|
||||
state.smsSendBtn = false
|
||||
this.requestFailed(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
requestFailed (err) {
|
||||
this.$notification['error']({
|
||||
message: '错误',
|
||||
description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
|
||||
duration: 4
|
||||
})
|
||||
this.registerBtn = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'state.passwordLevel' (val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.user-register {
|
||||
|
||||
&.error {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #ff7e05;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.user-layout-register {
|
||||
.ant-input-group-addon:first-child {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.user-layout-register {
|
||||
|
||||
& > h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.getCaptcha {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.login {
|
||||
float: right;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
cmdb-ui/src/views/user/RegisterResult.vue
Normal file
50
cmdb-ui/src/views/user/RegisterResult.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<result
|
||||
:isSuccess="true"
|
||||
:content="false"
|
||||
:title="email"
|
||||
:description="description">
|
||||
|
||||
<template slot="action">
|
||||
<a-button size="large" type="primary">查看邮箱</a-button>
|
||||
<a-button size="large" style="margin-left: 8px" @click="goHomeHandle">返回首页</a-button>
|
||||
</template>
|
||||
|
||||
</result>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Result } from '@/components'
|
||||
|
||||
export default {
|
||||
name: 'RegisterResult',
|
||||
components: {
|
||||
Result
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
description: '激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。',
|
||||
form: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
email () {
|
||||
const v = this.form && this.form.email || 'xxx'
|
||||
const title = `你的账户:${v} 注册成功`
|
||||
return title
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.form = this.$route.params
|
||||
},
|
||||
methods: {
|
||||
goHomeHandle () {
|
||||
this.$router.push({ name: 'login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
34
cmdb-ui/src/views/welcome/index.vue
Normal file
34
cmdb-ui/src/views/welcome/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<page-view :title="false" :avatar="avatar">
|
||||
<a-card :bordered="false" style="margin: -24px -24px 0px;">
|
||||
<result type="error" :title="title" :description="description">
|
||||
<template slot="action">
|
||||
<a-button type="primary" @click="$router.push('/')" >欢迎访问</a-button>
|
||||
</template>
|
||||
</result>
|
||||
</a-card>
|
||||
</page-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Result } from '@/components'
|
||||
import PageView from '@/layouts/PageView'
|
||||
|
||||
export default {
|
||||
name: 'Error',
|
||||
components: {
|
||||
PageView,
|
||||
Result
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: '欢迎访问',
|
||||
description: '欢迎使用本系统......^-^'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
0
cmdb-ui/src/views/welcome/welcome.vue
Normal file
0
cmdb-ui/src/views/welcome/welcome.vue
Normal file
Reference in New Issue
Block a user