前后端全面升级

This commit is contained in:
pycook
2023-07-10 17:42:15 +08:00
parent f57ff80099
commit 98cc853dbc
641 changed files with 97789 additions and 23995 deletions

View File

@@ -1,15 +0,0 @@
<template>
<div>
404 page
</div>
</template>
<script>
export default {
name: '404'
}
</script>
<style scoped>
</style>

View File

@@ -1,202 +0,0 @@
<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="$t('acl.roleList')"
>
<a-select name="otherID" :filterOption="filterOption" v-decorator="['otherID', {rules: [{ required: true, message: $t('acl.selectOtherRole')}]} ]">
<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">{{ $t('acl.associatedParentRole') }}</a-button>
<a-button @click="handleAddChild" type="primary" style="margin-right: 1rem">{{ $t('acl.associatedChildRole') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.associatedRole'),
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.$t('acl.associatedSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
addChild (id, otherID) {
addChildRole(id, otherID)
.then(res => {
this.$message.success(this.$t('acl.associatedSuccess'))
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,235 +0,0 @@
<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="$t('acl.name')"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: $t('acl.resourceNameRequired') }]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.description')"
>
<a-textarea :placeholder="$t('acl.descriptionTip')" name="description" :rows="4" />
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.permission')"
>
<div :style="{ borderBottom: '1px solid #E9E9E9' }">
<a-checkbox :indeterminate="indeterminate" @change="onCheckAllChange" :checked="checkAll">
{{ $t('tip.selectAll') }}
</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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.newResourceType'),
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.$t('tip.updateSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createResourceType (data) {
addResourceType(data)
.then(res => {
this.$message.success(this.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,191 +0,0 @@
<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="$t('acl.name')"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: $t('acl.resourceNameRequired')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.resourceType')"
>
<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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.newResource'),
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.$t('tip.addSuccess'))
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,123 +0,0 @@
<template>
<a-modal
:title="drawerTitle"
v-model="drawerVisible"
width="50%"
>
<template slot="footer">
<a-button key="back" @click="handleCancel">{{ $t('tip.close') }}</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: this.$t('acl.permList'),
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(this.$t('tip.deleteSuccess'))
}).catch(err => this.requestFailed(err))
},
handleCancel (e) {
this.drawerVisible = false
},
requestFailed (err) {
console.log(err)
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,120 +0,0 @@
<template>
<a-drawer
:title="$t('acl.addPermTip')+instance.name"
width="30%"
:closable="true"
:visible="visible"
@close="closeForm"
>
<a-form :form="form">
<a-form-item
:label="$t('acl.roleList')"
>
<a-select
showSearch
name="roleIdList"
v-decorator="['roleIdList', {rules: []} ]"
mode="multiple"
:placeholder="$t('acl.selectRoleTip')"
: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="$t('acl.permList')"
>
<a-select name="permName" v-decorator="['permName', {rules: []} ]" mode="multiple" :placeholder="$t('acl.selectPermTip')">
<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">{{ $t('acl.add') }}</a-button>
<a-button @click="closeForm">{{ $t('button.cancel') }}</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.$t('tip.requestFailed')
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(this.$t('tip.addSuccess')) }).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>

View File

@@ -1,217 +0,0 @@
<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="$t('acl.name')"
>
<a-input
name="name"
:placeholder="$t('acl.name')"
v-decorator="['name', {rules: [{ required: true, message: $t('acl.resourceTypeNameRequired')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.description')"
>
<a-textarea :placeholder="$t('acl.descriptionTip')" name="description" :rows="4" v-decorator="['description', {rules: []} ]"/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.permission')"
>
<a-select mode="tags" v-model="perms" style="width: 100%" :placeholder="$t('acl.permissionNameRequired')">
</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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.newResourceType'),
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.$t('tip.updateSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createResourceType (data) {
addResourceType(data)
.then(res => {
this.$message.success(this.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,258 +0,0 @@
<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="$t('acl.roleName')"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: $t('acl.roleNameRequired')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="{span:6}"
:wrapper-col="{span:12}"
:label="$t('acl.inheritedFrom')"
>
<a-select
v-model="selectedParents"
:placeholder="$t('acl.selectInheritedRoles')"
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="$t('acl.isAppAdmin')"
>
<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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.newRole'),
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.$t('button.add')
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.$t('button.update')
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.$t('tip.updateSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createRole (data) {
addRole(data)
.then(res => {
this.$message.success(this.$t('tip.addSuccess'))
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.$t('tip.requestFailed')
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>

View File

@@ -1,293 +0,0 @@
<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="$t('acl.username')"
>
<a-input
name="username"
:placeholder="$t('acl.username')"
v-decorator="['username', {rules: [{ required: true, message: $t('acl.usernameRequired')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.nickname')"
>
<a-input
name="nickname"
v-decorator="['nickname', {rules: []} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.password')"
>
<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="$t('acl.department')"
>
<a-input
name="department"
v-decorator="['department', {rules: []} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.catalog')"
>
<a-input
name="catalog"
v-decorator="['catalog', {rules: []} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.email')"
>
<a-input
name="email"
v-decorator="[
'email',
{
rules: [
{
type: 'email',
message: $t('acl.emailValidate'),
},
{
required: true,
message: $t('acl.emailRequired'),
},
],
},
]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('acl.mobile')"
>
<a-input
name="mobile"
v-decorator="['mobile', {rules: [{message: $t('acl.mobileValidate'), pattern: /^1\d{10}$/ }]} ]"
/>
</a-form-item>
<a-form-item
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('acl.block')"
>
<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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('acl.newUser'),
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.$t('tip.updateSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createUser (data) {
addUser(data)
.then(res => {
this.$message.success(this.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,305 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('acl.newResourceType') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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="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)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
formLayout: 'vertical',
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
alias: '',
name: ''
},
columns: [
{
title: this.$t('acl.typeName'),
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: this.$t('acl.description'),
dataIndex: 'description',
width: 250,
sorter: false,
scopedSlots: { customRender: 'description' }
},
{
title: this.$t('acl.permission'),
dataIndex: 'id',
sorter: false,
scopedSlots: { customRender: 'id' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,313 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('acl.newResourceType') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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="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)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
formLayout: 'vertical',
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
alias: '',
name: ''
},
columns: [
{
title: this.$t('acl.typeName'),
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: this.$t('acl.description'),
dataIndex: 'description',
width: 200,
sorter: false,
scopedSlots: { customRender: 'description' }
},
{
title: this.$t('acl.permission'),
dataIndex: 'id',
width: 300,
sorter: false,
scopedSlots: { customRender: 'id' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,335 +0,0 @@
<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;">{{ $t('acl.newResource') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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>
<span slot="action" slot-scope="text, record">
<template>
<a @click="handlePerm(record)">{{ $t('acl.viewAuthorization') }}</a>
<a-divider type="vertical"/>
<a @click="handlePermManage(record)">{{ $t('acl.authorization') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
allResourceTypes: [],
currentType: { id: 0 },
formLayout: 'vertical',
allResources: [],
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
alias: '',
name: ''
},
columns: [
{
title: this.$t('acl.resourceName'),
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: this.$t('acl.createdAt'),
width: 200,
dataIndex: 'created_at'
},
{
title: this.$t('acl.updatedAt'),
width: 200,
dataIndex: 'updated_at'
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,306 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('acl.newRole') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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>
<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)">{{ $t('button.update') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
formLayout: 'vertical',
allRoles: [],
id2parents: {},
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
alias: '',
name: ''
},
columns: [
{
title: this.$t('acl.roleName'),
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: this.$t('acl.isAppAdmin'),
dataIndex: 'is_app_admin',
width: 100,
sorter: false,
scopedSlots: { customRender: 'is_app_admin' }
},
{
title: this.$t('acl.inheritedFrom'),
dataIndex: 'id',
sorter: false,
width: 250,
scopedSlots: { customRender: 'inherit' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,357 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('acl.newUser') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.reset') }}</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)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
CITypeName: this.$route.params.CITypeName,
CITypeId: this.$route.params.CITypeId,
formLayout: 'vertical',
allUsers: [],
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
alias: '',
name: ''
},
columns: [
{
title: this.$t('acl.username'),
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: this.$t('acl.nickname'),
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: this.$t('acl.department'),
dataIndex: 'department',
width: 100,
sorter: false,
scopedSlots: { customRender: 'department' }
},
{
title: this.$t('acl.catalog'),
dataIndex: 'catalog',
sorter: false,
width: 100,
scopedSlots: { customRender: 'catalog' }
},
{
title: this.$t('acl.email'),
dataIndex: 'email',
sorter: false,
width: 200,
scopedSlots: { customRender: 'email' }
},
{
title: this.$t('acl.mobile'),
dataIndex: 'mobile',
sorter: false,
width: 150,
scopedSlots: { customRender: 'mobile' }
},
{
title: this.$t('acl.joinedAt'),
dataIndex: 'date_joined',
sorter: false,
width: 200,
scopedSlots: { customRender: 'date_joined' }
},
{
title: this.$t('acl.block'),
dataIndex: 'block',
width: 100,
scopedSlots: { customRender: 'block' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,85 +0,0 @@
<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">{{ $t('button.upload') }}</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="$t('batch.uploadResult')">
<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('CI Type not yet selected!')
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>

View File

@@ -1,88 +0,0 @@
<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: 'insert a line at the top'
},
row_below: {
name: 'insert a line at the bottom'
},
moverow: {
name: 'Delete rows'
},
unfreeze_column: {
name: 'Uncolumn fixation'
},
hsep1: '---------',
hsep2: '---------'
}
},
fixedColumnsLeft: 0,
fixedRowsTop: 0,
manualColumnFreeze: true,
comments: true,
customBorders: [],
columnSorting: true,
stretchH: 'all',
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>

View File

@@ -1,112 +0,0 @@
<template>
<div>
<a-form :form="form" style="max-width: 500px; margin: 30px auto 0;">
<a-row>
<a-col :span="18">
<a-form-item :label="$t('batch.modelType')" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-select
:placeholder="$t('batch.pleaseSelectModelType')"
v-decorator="['ciTypes', { rules: [{required: true, message: 'CI Type must be selected'}] }]"
@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"
>{{ $t('button.downloadTemplate') }}</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>

View File

@@ -1,61 +0,0 @@
<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">{{ $t('batch.dragFileHere') }}</p>
<p class="ant-upload-hint">{{ $t('batch.suportFileType') }} : 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>

View File

@@ -1,94 +0,0 @@
<template>
<div>
<h4>A total of <span style="color: blue">{{ total }}</span>, <span style="color: lightgreen">{{ complete }}</span> completed, <span style="color: red">{{ errorNum }} </span>failed</h4>
<a-progress :percent="mPercent"/>
<div class="my-box">
<span>Error message:</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.$t('tip.requestFailed')))
})
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>

View File

@@ -1,616 +0,0 @@
<template>
<div>
<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'"
>{{ $t('button.new') }}</a-button>
<a-button class="right" @click="showDrawer(typeId)">{{ $t('button.displayFields') }}</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" />{{ $t('button.update') }}
</span>
</a-menu-item>
<a-menu-item
key="batchUpdateRelation"
@click="$refs.batchUpdateRelation.visible = true"
>
<span @click="$refs.batchUpdateRelation.visible = true">
<a-icon type="link" />{{ $t('ci.batchUpdateRelation') }}
</span>
</a-menu-item>
<a-menu-item key="batchDownload" @click="batchDownload">
<json-excel :fetch="batchDownload" name="cmdb.xls">
<a-icon type="download" />&nbsp;&nbsp;&nbsp;&nbsp;{{ $t('button.download') }}
</json-excel>
</a-menu-item>
<a-menu-item key="batchDelete" @click="batchDelete">
<a-icon type="delete" />{{ $t('button.delete') }}
</a-menu-item>
</a-menu>
<a-button style="margin-left: 8px">
{{ $t('ci.batchOperate') }}
<a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<s-table
bordered
ref="table"
size="middle"
rowKey="ci_id"
:loaded="tableLoaded"
:columns="columns"
:data="loadInstances"
:alert="options.alert"
:rowSelection="options.rowSelection"
:scroll="{ x: scrollX, y: scrollY }"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} ${total} records in 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()"
>{{ $t('tip.detail') }}</a>
<a-divider type="vertical" />
<a @click="deleteCI(record)">{{ $t('tip.delete') }}</a>
</template>
</span>
</s-table>
<create-instance-form @refresh="refreshTable" ref="create" @submit="batchUpdate" />
<batch-update-relation :typeId="typeId" ref="batchUpdateRelation" @submit="batchUpdateRelation" />
</a-spin>
</a-card>
<template>
<div>
<a-drawer
:title="$t('ci.displayFieldDefine')"
:width="600"
@close="onClose"
:visible="visible"
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
>
<template>
<a-transfer
:dataSource="attrList"
:showSearch="true"
:listStyle="{
width: '230px',
height: '500px',
}"
:titles="[$t('tip.unselectedAttribute'), $t('tip.selectedAttribute')]"
:render="item=>item.title"
:targetKeys="selectedAttrList"
@change="handleChange"
@search="handleSearch"
>
<span slot="notFoundContent">{{ $t('tip.noData') }}</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">{{ $t('button.cancel') }}</a-button>
<a-button @click="subInstanceSubmit" type="primary">{{ $t('button.submit') }}</a-button>
</div>
</a-drawer>
</div>
</template>
</div>
</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 BatchUpdateRelation from './modules/BatchUpdateRelation'
import EditableCell from './modules/EditableCell'
import CiDetail from './modules/CiDetail'
import { searchCI, updateCI, deleteCI } from '@/api/cmdb/ci'
import { batchUpdateCIRelation } from '@/api/cmdb/CIRelation'
import { getSubscribeAttributes, subscribeCIType } from '@/api/cmdb/preference'
import { notification } from 'ant-design-vue'
import { getCITypeAttributesByName } from '@/api/cmdb/CITypeAttr'
var valueTypeMap = {
'0': 'int',
'1': 'float',
'2': 'text',
'3': 'datetime',
'4': 'date',
'5': 'time',
'6': 'json'
}
export default {
name: 'InstanceList',
components: {
STable,
EditableCell,
JsonExcel,
SearchForm,
CreateInstanceForm,
BatchUpdateRelation,
CiDetail
},
data () {
return {
loading: false,
tableLoaded: 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: [],
selectedAttrList: [],
attrList: [],
visible: false,
instanceList: [],
columns: [],
loadInstances: parameter => {
this.tableLoaded = false
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] && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:*${params[key]}*`
}
}
})
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))
this.$nextTick(() => {
this.tableLoaded = true
})
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: {
showDrawer () {
this.getAttrList()
},
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
})
})
},
onClose () {
this.visible = false
},
subInstanceSubmit () {
const that = this
subscribeCIType(this.typeId, this.selectedAttrList)
.then(res => {
notification.success({
message: that.$t('tip.updateSuccess')
})
this.reload()
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
handleChange (targetKeys, direction, moveKeys) {
this.selectedAttrList = targetKeys
},
handleSearch (dir, value) {},
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
col.value_type = item.value_type
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: this.$t('tip.operate'),
key: 'operation',
width: 115,
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 = this.$t('tip.downloading')
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: that.$t('tip.warning'),
content: that.$t('ci.confirmBatchUpdate'),
onOk () {
that.loading = true
that.loadTip = that.$t('ci.batchUpdate')
const payload = {}
Object.keys(values).forEach(key => {
if (values[key] || values[key] === 0) {
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.$t('ci.batchUpdateSuccess')
})
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)
})
}
})
},
batchUpdateRelation (values) {
const that = this
this.$confirm({
title: that.$t('tip.warning'),
content: that.$t('ci.confirmBatchUpdate'),
onOk () {
that.loading = true
that.loadTip = that.$t('ci.batchUpdate')
const payload = {}
Object.keys(values).forEach(key => {
if (values[key] || values[key] === 0) {
payload[key] = values[key]
}
})
batchUpdateCIRelation(that.selectedRowKeys, payload).then(() => {
that.loading = false
notification.success({
message: that.$t('ci.batchUpdateSuccess')
})
that.$refs.create.visible = false
that.$refs.table.clearSelected()
}).catch(e => {
console.log(e)
that.loading = false
notification.error({
message: e.response.data.message
})
})
}
})
},
batchDelete () {
const that = this
this.$confirm({
title: that.$t('tip.warning'),
content: that.$t('ci.confirmDelete'),
onOk () {
that.loading = true
that.loadTip = that.$t('tip.deleting')
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.$t('tip.deleteSuccess')
})
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: that.$t('tip.warning'),
content: that.$t('ci.confirmDelete'),
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 lang='less' scoped>
/deep/ .ant-table-thead > tr > th,
/deep/ .ant-table-tbody > tr > td {
white-space: nowrap;
overflow: hidden;
}
/deep/ .spin-content {
border: 1px solid #91d5ff;
background-color: #e6f7ff;
padding: 30px;
}
.right {
float: right;
}
</style>

View File

@@ -1,80 +0,0 @@
<template>
<a-drawer
:title="$t('ci.batchUpdateRelation')"
width="50%"
@close="() => { visible = false; $emit('refresh', true) }"
:visible="visible"
:wrapStyle="{ overflow: 'auto' }"
>
<a-form :form="form" :layout="formLayout" @submit="commitUpdateRelation">
<a-button type="primary" @click="commitUpdateRelation">Submit</a-button>
<a-form-item
v-bind="formItemLayout"
:label="item.alias || item.name"
v-for="item in parentCITypes"
:key="item.id"
>
<template v-for="_item in item.attributes">
<a-input
v-decorator="[_item.name, {validateTrigger: ['submit'], rules: []}]"
style="width: 100%"
v-if="_item.id == item.unique_id"
:key="_item.id"
:placeholder="_item.alias || _item.name"
/>
</template>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script>
import { getCITypeParent } from '@/api/cmdb/CITypeRelation'
export default {
props: {
typeId: {
type: Number,
required: true
}
},
data () {
return {
action: '',
form: this.$form.createForm(this),
parentCITypes: [],
visible: false,
formItemLayout: {
labelCol: {
xs: { span: 24 },
sm: { span: 8 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 }
}
},
formLayout: 'horizontal'
}
},
created () {
this.getParentCITypes()
},
methods: {
getParentCITypes () {
getCITypeParent(this.typeId).then(res => {
this.parentCITypes = res.parents
})
},
commitUpdateRelation (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
this.$emit('submit', values)
}
})
}
}
}
</script>

View File

@@ -1,361 +0,0 @@
<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 || $t('tip.other')"
: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 i18n from '@/locales'
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: this.$t('ci.attribute')
},
{
key: 'tab_2',
tab: this.$t('ci.relation')
},
{
key: 'tab_3',
tab: this.$t('ci.history')
}
],
activeTabKey: 'tab_1',
rowSpanMap: {},
historyColumns: [
{
title: this.$t('ci.time'),
dataIndex: 'created_at',
key: 'created_at',
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {}
}
obj.attrs.rowSpan = this.rowSpanMap[index]
return obj
}
},
{
title: this.$t('ci.user'),
dataIndex: 'username',
key: 'username',
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {}
}
obj.attrs.rowSpan = this.rowSpanMap[index]
return obj
}
},
{
title: this.$t('tip.operate'),
dataIndex: 'operate_type',
key: 'operate_type',
scopedSlots: { customRender: 'operate_type' }
},
{
title: this.$t('ci.attribute'),
dataIndex: 'attr_alias',
key: 'attr_name'
},
{
title: 'Old',
dataIndex: 'old',
key: 'old'
},
{
title: 'New',
dataIndex: 'new',
key: 'new'
}
],
ciHistory: []
}
},
filters: {
operateTypeFilter (operateType) {
const operateTypeMap = {
'0': i18n.t('button.add'),
'1': i18n.t('button.delete'),
'2': i18n.t('button.update')
}
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>

View File

@@ -1,156 +0,0 @@
<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'">{{ $t('ci.batchUpdateTip') }}</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="$t('tip.pleaseSelect')"
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',
'6': 'json'
}
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' ? this.$t('tip.create') + ' ' : this.$t('ci.batchUpdate') + ' '
}
},
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
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: this.$t('tip.addSuccess')
})
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
} else {
notification.error({
message: err
})
}
})
}
}
}
</script>

View File

@@ -1,94 +0,0 @@
<template>
<div class="editable-cell">
<div v-if="editable.x" class="editable-cell-input-wrapper">
<a-input :value="value | joinList" @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: {
// eslint-disable-next-line
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: {
jsonDump: function (v) {
if (typeof v === 'object') {
return JSON.stringify(v)
} else {
return v
}
},
joinList: function (itemValue) {
if (typeof itemValue === 'object' && itemValue) {
try {
if (typeof itemValue[0] !== 'object') {
return itemValue.join(',')
} else {
return JSON.stringify(itemValue)
}
} catch (e) {
return JSON.stringify(itemValue)
}
} else if (itemValue !== null && itemValue !== 'undefined' && itemValue !== undefined && 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>

View File

@@ -1,131 +0,0 @@
<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="$t('tip.pleaseSelect')"
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="$t('tip.pleaseSelect')" 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">{{ $t('button.query') }}</a-button>
<a-button style="margin-left: 8px" @click="() => queryParam = {}" v-if="preferenceAttrList.length">{{ $t('button.reset') }}</a-button>
<a
@click="toggleAdvanced"
style="margin-left: 8px"
v-if="preferenceAttrList.length > 4"
>
{{ advanced ? $t('tip.fold') : $t('tip.unfold') }}
<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',
'6': 'json'
}
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>

View File

@@ -1,387 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('ciType.addAttribute') }}</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} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
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: this.$t('ciType.alias'),
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: this.$t('ciType.name'),
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: this.$t('ciType.type'),
dataIndex: 'value_type',
sorter: false,
width: 80,
scopedSlots: { customRender: 'value_type' },
customRender: (text) => valueTypeMap[text]
},
{
title: this.$t('ciType.unique'),
dataIndex: 'is_unique',
width: 50,
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.index'),
dataIndex: 'is_index',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.sort'),
dataIndex: 'is_sortable',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.link'),
dataIndex: 'is_link',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.password'),
dataIndex: 'is_password',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.list'),
dataIndex: 'is_list',
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,337 +0,0 @@
<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="$t('ciType.name')"
>
<a-input
name="name"
v-decorator="['name', {rules: [{ required: true, message: $t('ciType.nameRequired')},{message: $t('ciType.nameValidate'), pattern: RegExp('^(?!\\d)[a-zA-Z_0-9]+$')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('ciType.alias')"
>
<a-input
name="alias"
v-decorator="['alias', {rules: []} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('ciType.type')"
>
<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="$t('ciType.isIt') + $t('ciType.unique')"
>
<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="$t('ciType.isIt') + $t('ciType.index')"
>
<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="$t('ciType.isIt') + $t('ciType.sort')"
>
<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="$t('ciType.isIt') + $t('ciType.link')"
>
<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="$t('ciType.isIt') + $t('ciType.password')"
>
<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="$t('ciType.isIt') + $t('ciType.list')"
>
<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="$t('ciType.predefinedValue')"
>
<a-textarea
:rows="5"
name="choice_value"
:placeholder="$t('ciType.predefinedValueTip')"
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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('ciType.addAttribute'),
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.$t('tip.updateSuccess'))
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.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
} else {
this.$message.success(this.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
}
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,11 +0,0 @@
import i18n from '@/locales'
export const valueTypeMap = {
'0': i18n.t('ciType.integer'),
'1': i18n.t('ciType.float'),
'2': i18n.t('ciType.text'),
'3': 'Datetime',
'4': 'Date',
'5': 'Time',
'6': 'Json'
}

View File

@@ -1,558 +0,0 @@
<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)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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="[$t('tip.unselectedAttribute'), $t('tip.selectedAttribute')]"
: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">{{ $t('button.submit') }}</a-button>
<a-button @click="onBatchBindAttrActionClose">{{ $t('button.cancel') }}</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: this.$t('ciType.addAttribute'),
drawerTitle: this.$t('ciType.addAttribute'),
drawerVisible: false
},
batchBindAttrAction: {
btnName: this.$t('ciType.bindAttribute'),
drawerTitle: this.$t('ciType.bindAttribute'),
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: this.$t('ciType.alias'),
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: this.$t('ciType.name'),
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: this.$t('ciType.type'),
dataIndex: 'value_type',
sorter: false,
width: 100,
scopedSlots: { customRender: 'value_type' },
customRender: (text) => valueTypeMap[text]
},
{
title: this.$t('ciType.unique'),
dataIndex: 'is_unique',
width: 50,
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.index'),
dataIndex: 'is_index',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.sort'),
dataIndex: 'is_sortable',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.link'),
dataIndex: 'is_link',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.password'),
dataIndex: 'is_password',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.list'),
dataIndex: 'is_list',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.required'),
dataIndex: 'is_required',
sorter: false,
width: 50,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('ciType.defaultShow'),
dataIndex: 'default_show',
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
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.$t('tip.updateSuccess'))
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.$t('tip.requestFailed')
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>

View File

@@ -1,347 +0,0 @@
<template>
<div>
<a-button class="action-btn" @click="handleCreate" type="primary">{{ $t('button.batchUpdate') }}</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)">{{ $t('tip.delete') }}</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="[$t('tip.unselectedAttribute'), $t('tip.selectedAttribute')]"
: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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('ciType.alias'),
dataIndex: 'alias',
sorter: false,
width: 200,
scopedSlots: { customRender: 'alias' }
},
{
title: this.$t('ciType.name'),
dataIndex: 'name',
sorter: false,
width: 200,
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('ciType.required'),
dataIndex: 'is_required',
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
handleOk () {
this.$refs.table.refresh()
},
handleCreate () {
this.drawerTitle = this.$t('button.batchUpdate')
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.$t('tip.updateSuccess'))
this.visible = false
this.handleOk()
}
).catch(err => {
this.requestFailed(err)
})
}
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,352 +0,0 @@
<template>
<div>
<a-button class="action-btn" @click="handleCreate" type="primary">{{ $t('button.batchUpdate') }}</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)">{{ $t('tip.delete') }}</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="[$t('tip.unselectedAttribute'), $t('tip.selectedAttribute')]"
: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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('ciType.alias'),
dataIndex: 'alias',
sorter: false,
width: 200,
scopedSlots: { customRender: 'alias' }
},
{
title: this.$t('ciType.name'),
dataIndex: 'name',
sorter: false,
width: 200,
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('ciType.defaultShow'),
dataIndex: 'default_show',
sorter: false,
scopedSlots: { customRender: 'is_check' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
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.$t('button.batchUpdate')
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.$t('tip.updateSuccess'))
this.visible = false
this.handleOk()
}
).catch(err => {
this.requestFailed(err)
})
}
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,89 +0,0 @@
<template>
<a-card :bordered="false">
<a-tabs defaultActiveKey="1">
<a-tab-pane key="1" :tab="$t('ciType.typeAttribute')">
<AttributesTable></AttributesTable>
</a-tab-pane>
<a-tab-pane forceRender key="2" :tab="$t('ciType.typeRelation')">
<RelationTable :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
</a-tab-pane>
<a-tab-pane forceRender key="3" :tab="$t('ciType.requiredCheck')">
<CheckTable></CheckTable>
</a-tab-pane>
<a-tab-pane forceRender key="4" :tab="$t('ciType.defaultShowAttribute')">
<DefaultShowTable></DefaultShowTable>
</a-tab-pane>
<a-tab-pane forceRender key="5" :tab="$t('ciType.attributeGroup')">
<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>

View File

@@ -1,621 +0,0 @@
<template>
<div>
<div style="margin-bottom: 2rem">
<a-button type="primary" v-if="addGroupBtnVisible" @click="handleAddGroup">{{ $t('ciType.addGroup') }}</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">{{ $t('button.save') }}</a>
<a @click="handleCancelCreateGroup">{{ $t('button.cancel') }}</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">{{ $t('button.save') }}</a>
<a @click="handleCancelGroupName(index, CITypeGroup)">{{ $t('button.cancel') }}</a>
</span>
</template>
<div style="float: right">
<a-button-group size="small">
<a-tooltip>
<template slot="title">
{{ $t('ciType.up') }}
</template>
<a-button icon="arrow-up" size="small" @click="handleMoveGroup(index, index-1)" :disabled="index===0"/>
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ $t('ciType.down') }}
</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">
{{ $t('ciType.addAttribute1') }}
</template>
<a-button icon="plus" size="small" @click="handleAddExistGroupAttr(index)"/>
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ $t('ciType.deleteGroup') }}
</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"
@change="(e)=>{handleChange(e, CITypeGroup.id)}"
: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="">{{ $t('ciType.addAttribute1') }}</li>
</template>
</draggable>
</div>
</div>
<div class="group-header">
<template>
<span style="margin-right: 0.2rem">{{ $t('ciType.moreAttribute') }}</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">
{{ $t('ciType.up') }}
</template>
<a-button icon="arrow-up" size="small" disabled/>
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ $t('ciType.down') }}
</template>
<a-button icon="arrow-down" size="small" disabled />
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ $t('ciType.addAttribute1') }}
</template>
<a-button icon="plus" size="small" @click="handleAddOtherGroupAttr"/>
</a-tooltip>
<a-tooltip>
<template slot="title">
{{ $t('ciType.deleteGroup') }}
</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"
@change="(e)=>{handleChange(e, -1)}"
: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="">{{ $t('ciType.addAttribute1') }}</li>
</template>
</draggable>
</div>
<a-modal
:title="$t('ciType.addAttribute1')"
: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>
/* eslint-disable */
import {
deleteCITypeGroupById,
getCITypeGroupById,
createCITypeGroupById,
updateCITypeGroupById
} from '@/api/cmdb/CIType'
import { getCITypeAttributesById, updateCITypeAttributesById, transferCITypeAttrIndex, transferCITypeGroupIndex } 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: [],
addRemoveGroupFlag: {},
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(this.$t('ciType.groupNameExisted'))
} 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(this.$t('tip.updateSuccess'))
})
.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 fromGroupId = this.CITypeGroups[beforeIndex].id
const toGroupId = this.CITypeGroups[afterIndex].id
transferCITypeGroupIndex(this.CITypeId, { from: fromGroupId, to: toGroupId }).then(res => {
this.$message.success(this.$t('ciType.moveSuccess'))
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)
}).catch(err => {
this.$httpError(err, this.$t('ciType.moveFailed'))
})
},
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, group) {
if (e.hasOwnProperty('moved') && e.moved.oldIndex !== e.moved.newIndex) {
if (group === -1) {
this.$message.error(this.$t('ciType.moreAttributeCannotSort'))
} else {
transferCITypeAttrIndex(this.CITypeId,
{
from: { attr_id: e.moved.element.id, group_id: group > -1 ? group : null },
to: { order: e.moved.newIndex, group_id: group > -1 ? group : null }
}).then(res => this.$message.success(this.$t('tip.saveSuccess'))).catch(err => {
this.$httpError(err)
this.abortDraggable()
})
}
}
if (e.hasOwnProperty('added')) {
this.addRemoveGroupFlag = { to: { group_id: group > -1 ? group : null, order: e.added.newIndex }, inited: true }
}
if (e.hasOwnProperty('removed')) {
this.$nextTick(() => {
transferCITypeAttrIndex(this.CITypeId,
{
from: { attr_id: e.removed.element.id, group_id: group > -1 ? group : null },
to: { group_id: this.addRemoveGroupFlag.to.group_id, order: this.addRemoveGroupFlag.to.order }
}).then(res => this.$message.success(this.$t('tip.saveSuccess'))).catch(err => {
this.$httpError(err)
this.abortDraggable()
}).finally(() => {
this.addRemoveGroupFlag = {}
})
})
}
},
abortDraggable () {
this.$nextTick(() => {
this.$router.push({name: 'ci_type'})
})
},
updatePropertyIndex () {
const attributes = [] // all attributes
let attributeOrder = 0 // attribute group
let groupOrder = 0 // sort by group
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(this.$t('tip.updateSuccess'))
that.getCITypeGroupData()
that.modalVisible = false
})
.catch(err => that.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,311 +0,0 @@
<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"/>
{{ $t('ciType.add') }}
</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="$t('tip.confirmDelete')" @confirm="handleDelete(item)" :okText="$t('button.yes')" :cancelText="$t('button.no')">
<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="$t('ciType.name')"
>
<a-input
name="name"
v-decorator="['name', {rules: [{ required: true, message: $t('ciType.nameRequired')},{message: $t('ciType.nameValidate'), pattern: RegExp('^(?!\\d)[a-zA-Z_0-9]+$')}]} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('ciType.alias')"
>
<a-input
name="alias"
v-decorator="['alias', {rules: []} ]"
/>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('ciType.unique') + '*'"
>
<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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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.$t('ciType.newCIType')
this.drawerVisible = true
},
onClose () {
this.form.resetFields()
this.drawerVisible = false
},
handleEdit (record) {
this.drawerTitle = this.$t('ciType.editCIType')
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.$t('tip.deleteSuccess'))
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.$t('tip.addSuccess'))
this.drawerVisible = false
this.getCITypes()
})
.catch(err => this.requestFailed(err))
},
updateCIType (CITypeId, data) {
updateCIType(CITypeId, data)
.then(res => {
this.$message.success(this.$t('tip.updateSuccess'))
this.drawerVisible = false
this.getCITypes()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,338 +0,0 @@
<template>
<div>
<a-button class="action-btn" @click="handleCreate" type="primary">{{ $t('ciType.newRelation') }}</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)">{{ $t('tip.delete') }}</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="$t('ciType.sourceCIType')"
>
<a-select
name="source_ci_type_id"
style="width: 120px"
v-decorator="['source_ci_type_id', {rules: [], } ]"
>
<template v-for="CIType in CITypes">
<a-select-option :value="CIType.id" :key="CIType.id" v-if="CITypeId == CIType.id">{{ CIType.alias }}</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('ciType.targetCIType')"
>
<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="$t('ciType.relationType')"
>
<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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</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: this.$t('ciType.sourceCIType') + this.$t('ciType.name'),
dataIndex: 'source_ci_type_name',
sorter: false,
width: 200,
scopedSlots: { customRender: 'source_ci_type_name' }
},
{
title: this.$t('ciType.relationType'),
dataIndex: 'relation_type',
sorter: false,
width: 100,
scopedSlots: { customRender: 'name' }
},
{
title: this.$t('ciType.targetCIType') + this.$t('ciType.alias'),
dataIndex: 'alias',
sorter: false,
scopedSlots: { customRender: 'alias' }
},
{
title: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
}).catch(err => this.requestFailed(err))
},
handleOk () {
this.$refs.table.refresh()
},
handleCreate () {
this.drawerTitle = this.$t('ciType.newRelation')
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.$t('tip.addSuccess'))
this.onClose()
this.handleOk()
}).catch(err => this.requestFailed(err))
}
})
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,346 +0,0 @@
<template>
<div>
<a-card :bordered="true" :title="$t('ciType.relationViewDefinePanel')">
<a-card-meta :description="$t('ciType.relationViewDefinePanelTip1')"></a-card-meta>
<a-card-meta :description="$t('ciType.relationViewDefinePanelTip2')"></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="&quot;view-&quot; + (relationViews.views[view].topo_flatten || []).join(&quot;&quot;)" 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: that.$t('tip.warning'),
content: that.$t('tip.confirmDelete'),
onOk () {
deleteRelationView(viewName).then(res => {
that.create()
}).catch(e => {
notification.error({ message: e.reponse.data.message })
})
}
})
}
}
}
</script>

View File

@@ -1,160 +0,0 @@
<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="$t('ciType.RealtionViewName')"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: $t('ciType.RealtionViewNameRequired')}]} ]"
/>
</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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</a-button>
</div>
</a-form>
</a-drawer>
</template>
<script>
import { subscribeRelationView } from '@/api/cmdb/preference'
export default {
name: 'RelationViewForm',
data () {
return {
drawerTitle: this.$t('ciType.newRealtionView'),
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.$t('tip.addSuccess'))
this.onClose()
this.$emit('refresh')
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
console.log(err, 'error')
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,276 +0,0 @@
<template>
<a-card :bordered="false">
<div class="action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ $t('ciType.newRelationType') }}</a-button>
</div>
<s-table
:alert="options.alert"
:columns="columns"
:data="loadData"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} ${total} records in 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"
>{{ $t('button.query') }}</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>{{ $t('button.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="description" slot-scope="text">{{ text }}</template>
<span slot="action" slot-scope="text, record">
<template>
<a @click="handleEdit(record)">{{ $t('tip.edit') }}</a>
<a-divider type="vertical"/>
<a-popconfirm
:title="$t('tip.confirmDelete')"
@confirm="handleDelete(record)"
@cancel="cancel"
:okText="$t('button.yes')"
:cancelText="$t('button.no')"
>
<a>{{ $t('tip.delete') }}</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 },
formLayout: 'vertical',
pageSizeOptions: ['10', '25', '50', '100'],
columnSearchText: {
name: ''
},
columns: [
{
width: 150,
title: this.$t('ciType.name'),
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: this.$t('tip.operate'),
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.$t('tip.deleteSuccess'))
this.handleOk()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requestFailed')
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>

View File

@@ -1,192 +0,0 @@
<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="$t('ciType.name')"
>
<a-input
name="name"
placeholder=""
v-decorator="['name', {rules: [{ required: true, message: $t('ciType.relationTypeNameRequired')}]} ]"
/>
</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">{{ $t('button.submit') }}</a-button>
<a-button @click="onClose">{{ $t('button.cancel') }}</a-button>
</div>
</a-form>
</a-drawer>
</template>
<script>
import { addRelationType, updateRelationType } from '@/api/cmdb/relationType'
export default {
name: 'RelationTypeForm',
data () {
return {
drawerTitle: this.$t('ciType.newRelationType'),
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.$t('tip.updateSuccess'))
this.handleOk()
this.onClose()
}).catch(err => this.requestFailed(err))
},
createRelationType (data) {
addRelationType(data)
.then(res => {
this.$message.success(this.$t('tip.addSuccess'))
this.handleOk()
this.onClose()
})
.catch(err => this.requestFailed(err))
},
requestFailed (err) {
const msg = ((err.response || {}).data || {}).message || this.$t('tip.requsetFailed')
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>

View File

@@ -1,363 +0,0 @@
<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 ? $t('tip.subscribed') : $t('tip.unsubscribed') }}</span>
</a-card-meta>
<template class="ant-card-actions" slot="actions">
<a :disabled="!item.is_subscribed" @click="unsubscribe(item.id)">{{ $t('button.cancel') }}</a>
<a @click="showDrawer(item.id, item.alias || item.name)">{{ $t('button.subscribe') }}</a>
</template>
</a-card>
</template>
</a-list-item>
</a-list>
<template>
<div>
<a-drawer
:title="$t('preference.subscribeModel') +':'+ typeName"
:width="600"
@close="onClose"
:visible="visible"
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
>
<a-alert :message="$t('preference.subFormTip')" type="info" showIcon />
<a-divider>
{{ $t('menu.treeViews') }}
<span
v-if="treeSubscribed"
style="font-weight: 500; font-size: 12px; color: green"
>{{ $t('tip.subscribed') }}</span>
<span style="font-weight: 500; font-size: 12px; color: red" v-else>{{ $t('tip.unsubscribed') }}</span>
</a-divider>
<a-select
ref="tree"
mode="multiple"
:placeholder="$t('ci.selectLevel')"
: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'}"
>{{ $t('button.subscribe') }}</a-button>
<br />
<br />
<a-divider>
{{ $t('preference.resourceView') }}
<span
v-if="instanceSubscribed"
style="font-weight: 500; font-size: 12px; color: green"
>{{ $t('tip.subscribed') }}</span>
<span style="font-weight: 500; font-size: 12px; color: red" v-else>{{ $t('tip.unsubscribed') }}</span>
</a-divider>
<template>
<a-transfer
:dataSource="attrList"
:showSearch="true"
:listStyle="{
width: '230px',
height: '500px',
}"
:titles="[$t('tip.unselectedAttribute'), $t('tip.selectedAttribute')]"
:render="item=>item.title"
:targetKeys="selectedAttrList"
@change="handleChange"
@search="handleSearch"
>
<span slot="notFoundContent">{{ $t('tip.noData') }}</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">{{ $t('button.cancel') }}</a-button>
<a-button @click="subInstanceSubmit" type="primary">{{ $t('button.subscribe') }}</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: that.$t('tip.warning'),
content: that.$t('preference.cancelSubscribeConfirm'),
onOk () {
const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree])
.then(() => {
notification.success({
message: that.$t('tip.cancelSuccess')
})
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.$t('preference.subscribeSuccess')
})
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: this.$t('preference.subscribeSuccess')
})
})
.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>

View File

@@ -1,439 +0,0 @@
<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="$t('relationView.tip')" 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} records in 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] && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:*${params[key]}*`
}
}
})
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' scoped>
/deep/ .ant-table-thead > tr > th,
/deep/ .ant-table-tbody > tr > td {
white-space: nowrap;
overflow: hidden;
}
/deep/ .ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
/deep/ .relation-card > .ant-card-body {
padding-top: 0 !important;
}
</style>

View File

@@ -1,257 +0,0 @@
<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="$t('treeView.tip')" 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} records in 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] && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:*${params[key]}*`
}
}
})
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 lang='less' scoped>
/deep/ .ant-table-thead > tr > th,
/deep/ .ant-table-tbody > tr > td {
white-space: nowrap;
overflow: hidden;
}
/deep/ .ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="ops-fullscreen-dashboard">
<ul>
<li class="ops-fullscreen-dashboard-li" v-for="item in list" :key="item.name">
<a class="ops-fullscreen-dashboard-a" @click="goto(item.href)">
<div class="ops-fullscreen-dashboard-image" :style="{ backgroundImage: `url('${item.image}')` }"></div>
<h4>{{ item.name }}</h4>
</a>
</li>
</ul>
</div>
</template>
<script>
import cmdb_fullscreen from '../../assets/fullscreen/cmdb_fullscreen.png'
export default {
name: 'Fullscreen',
data() {
return {
list: [
{
name: 'CMDB',
image: cmdb_fullscreen,
href: '/cmdb/screen',
},
],
}
},
methods: {
goto(href) {
this.$router.push(href)
},
},
}
</script>
<style lang="less" scoped>
.ops-fullscreen-dashboard {
position: fixed;
left: 0;
top: 40px;
z-index: 150;
width: 100%;
padding: 24px;
> ul {
list-style: none;
padding: 0;
.ops-fullscreen-dashboard-li {
display: inline-block;
vertical-align: top;
width: 25%;
margin: 0;
padding: 0;
.ops-fullscreen-dashboard-a {
&:hover .ops-fullscreen-dashboard-image {
transform: translateY(-4px);
border-color: transparent;
box-shadow: 0 6px 16px #6b93e024;
}
.ops-fullscreen-dashboard-image {
background-color: #fff;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
border: 1px solid #f0f0f0;
border-radius: 6px;
overflow: hidden;
height: calc(100vw / 7 - 20px);
min-height: 130px;
max-height: 280px;
object-fit: cover;
transition: all 0.3s;
display: flex;
justify-content: center;
}
h4 {
margin: 16px 0 0;
text-align: center;
color: #101424;
font-size: 16px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<TwoColumnLayout appName="notice-center">
<template #one>
<div
:class="{ 'notice-center-left': true, 'notice-center-left-select': current.value === item.value }"
v-for="item in leftList"
:key="item.label"
@click="
() => {
current = item
selectedRowKeys = []
$refs.opsTable.getVxetableRef().clearCheckboxRow()
$refs.opsTable.getVxetableRef().clearCheckboxReserve()
updateTableData()
}
"
>
<span>{{ item.label }}</span>
<span v-if="item.value === false">{{ totalUnreadNum > 99 ? '99+' : totalUnreadNum }}</span>
</div>
</template>
<template #two>
<div class="notice-center-header">
<div>
<a-badge
v-for="app in apps"
:key="app.value"
:count="getAppCount(app)"
:offset="[-4, 5]"
:numberStyle="{
minWidth: '14px',
height: '14px',
lineHeight: '14px',
borderRadius: '7px',
padding: ' 0 4px',
}"
:class="{ 'notice-center-header-app': true, 'notice-center-header-app-selected': currentApp === app.value }"
>
<span @click="changeApp(app)">{{ app.label }}</span>
</a-badge>
</div>
<div class="notice-center-categories">
<span
:class="{ 'notice-center-categories-selected': currentCategory === cate }"
v-for="cate in categories"
:key="cate"
@click="changeCate(cate)"
>{{ cate }}</span
>
</div>
<div>
<a-input-search
allow-clear
v-model="filterName"
class="ops-input-radius"
:style="{ width: '300px', marginRight: '20px' }"
placeholder="请输入你需要搜索的内容"
@search="updateTableData()"
/>
<div class="ops-list-batch-action">
<template v-if="!!selectedRowKeys.length">
<span @click="batchChangeIsRead('read')">标为已读</span>
<a-divider type="vertical" />
<span @click="batchChangeIsRead('unread')">标为未读</span>
<span>选取: {{ selectedRowKeys.length }} </span>
</template>
</div>
</div>
</div>
<OpsTable
size="small"
ref="opsTable"
stripe
class="ops-stripe-table"
:data="tableData"
show-overflow
show-header-overflow
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
:row-class-name="rowClassName"
:checkbox-config="{ reserve: true }"
:row-config="{ keyField: 'id' }"
:height="tableHeight"
>
<vxe-column type="checkbox" width="60px"></vxe-column>
<vxe-column field="content" title="标题内容">
<template #default="{row}">
<span v-html="row.content"></span>
</template>
</vxe-column>
<vxe-column field="created_at" title="提交时间" width="150px">
<template #default="{row}">
{{ moment(row.created_at).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</vxe-column>
<vxe-column field="category" title="类型" width="150px">
<template #default="{row}">
{{ `${row.app_name}-${row.category}` }}
</template>
</vxe-column>
</OpsTable>
<div class="notice-center-pagination">
<a-pagination
size="small"
show-size-changer
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
/>
</div>
</template>
</TwoColumnLayout>
</template>
<script>
import moment from 'moment'
import { mapState } from 'vuex'
import TwoColumnLayout from '@/components/TwoColumnLayout'
import { getMessage, getNoticeApps, getNoticeCategoriesByApp, batchUpdateMessage } from '../../api/message'
import Bus from '../../bus'
export default {
name: 'Notice',
components: {
TwoColumnLayout,
},
data() {
return {
leftList: [
{
value: '',
label: '全部消息',
},
{
value: false,
label: '未读消息',
},
{
value: true,
label: '已读消息',
},
],
current: {
value: '',
label: '全部消息',
},
tablePage: {
currentPage: 1,
pageSize: 20,
totalResult: 0,
},
filterName: '',
tableData: [],
apps: [],
interval: null,
currentApp: '',
categories: [],
currentCategory: undefined,
selectedRowKeys: [],
}
},
computed: {
...mapState({
totalUnreadNum: (state) => state.notice.totalUnreadNum,
appUnreadNum: (state) => state.notice.appUnreadNum,
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
if (this.categories.length) {
return this.windowHeight - 236
}
return this.windowHeight - 205
},
},
mounted() {
this.intervalFunc()
this.interval = setInterval(() => {
this.intervalFunc()
}, 30000)
},
beforeDestroy() {
clearInterval(this.interval)
this.interval = null
},
methods: {
moment,
intervalFunc() {
this.getNoticeApps()
this.getNoticeCategoriesByApp()
this.updateTableData(this.tablePage.currentPage, this.tablePage.pageSize)
},
getNoticeApps() {
getNoticeApps().then((res) => {
const _apps = res.app_names.map((appName) => ({
value: appName,
label: appName,
}))
if (_apps.length) {
_apps.unshift({
value: '',
label: '全部',
})
}
this.apps = _apps
})
},
getNoticeCategoriesByApp() {
if (this.currentApp) {
getNoticeCategoriesByApp(this.currentApp).then((res) => {
this.categories = res.categories
})
} else {
this.categories = []
}
},
updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
getMessage({
is_read: this.current.value,
app_name: this.currentApp,
category: this.currentCategory,
page: currentPage,
page_size: pageSize,
order: this.tableSortData || '-create_at',
search: this.filterName,
}).then((res) => {
this.tableData = res?.data_list || []
this.tablePage = {
...this.tablePage,
currentPage: res.page,
pageSize: res.page_size,
totalResult: res.total,
}
})
},
pageOrSizeChange(currentPage, pageSize) {
this.updateTableData(currentPage, pageSize)
},
onSelectChange({ records }) {
this.selectedRowKeys = records
},
getAppCount(app) {
if (app.value) {
const _find = this.appUnreadNum.find((item) => item.app_name === app.value)
return _find?.count ?? 0
}
return this.totalUnreadNum
},
changeApp(app) {
this.currentApp = app.value
this.currentCategory = undefined
this.filterName = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.getNoticeCategoriesByApp()
this.updateTableData()
},
changeCate(cate) {
this.filterName = ''
this.selectedRowKeys = []
this.$refs.opsTable.getVxetableRef().clearCheckboxRow()
this.$refs.opsTable.getVxetableRef().clearCheckboxReserve()
this.currentCategory = cate
this.updateTableData()
},
batchChangeIsRead(action) {
batchUpdateMessage({ action, message_id_list: this.selectedRowKeys.map((item) => item.id) }).then((res) => {
this.updateTableData()
Bus.$emit('getUnreadMessageCount')
})
},
rowClassName({ row, rowIndex, $rowIndex }) {
if (row.is_read) {
return 'notice-center-is_read'
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.notice-center-left {
color: rgba(0, 0, 0, 0.7);
padding: 0 12px 0 24px;
height: 32px;
line-height: 32px;
border-left: 3px solid #fff;
cursor: pointer;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
> span:nth-child(2) {
background-color: #e1efff;
border-radius: 2px;
color: #9094a6;
padding: 0 4px;
font-size: 12px;
height: 20px;
line-height: 20px;
}
}
.notice-center-left:hover,
.notice-center-left-select {
background-color: #f0f5ff;
border-color: #custom_colors[color_1];
> span:nth-child(2) {
background-color: #fff;
color: #custom_colors[color_1];
}
}
.notice-center-header {
> div {
margin-bottom: 10px;
}
.notice-center-header-app {
border-radius: 16px;
background-color: #f0f5ff;
color: #9094a6;
padding: 5px 14px;
cursor: pointer;
margin-right: 12px;
}
> .notice-center-header-app:hover,
.notice-center-header-app-selected {
background-color: #custom_colors[color_1];
color: #fff;
}
.notice-center-categories {
> span {
color: #a5a9bc;
padding: 4px 18px;
background-color: #f0f5ff;
cursor: pointer;
}
> span:hover,
.notice-center-categories-selected {
color: #fff;
background-color: #custom_colors[color_1];
}
}
}
.notice-center-pagination {
width: 100%;
margin-top: 12px;
display: inline-flex;
justify-content: flex-end;
}
</style>
<style lang="less">
.notice-center-is_read {
opacity: 0.6;
}
</style>

View File

@@ -0,0 +1,371 @@
<template>
<div class="ops-setting-companyinfo" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="infoData" :model="infoData" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rule">
<SpanTitle>公司描述</SpanTitle>
<a-form-model-item label="名称" prop="name">
<a-input v-model="infoData.name" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="描述">
<a-input v-model="infoData.description" type="textarea" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司地址</SpanTitle>
<a-form-model-item label="国家/地区">
<a-input v-model="infoData.country" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="城市">
<a-input v-model="infoData.city" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="地址">
<a-input v-model="infoData.address" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="邮编">
<a-input v-model="infoData.postCode" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>联系方式</SpanTitle>
<a-form-model-item label="网站">
<a-input v-model="infoData.website" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电话号码" prop="phone">
<a-input v-model="infoData.phone" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="传真号码" prop="faxCode">
<a-input v-model="infoData.faxCode" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电子邮箱" prop="email">
<a-input v-model="infoData.email" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司标识</SpanTitle>
<a-form-model-item label="部署域名" prop="domainName">
<a-input v-model="infoData.domainName" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="公司logo">
<a-space>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '400px', height: '80px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.logoName"
:style="{ width: '400px', height: '80px' }"
@click="eidtImageOption.type = 'Logo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.logoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'Logo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '82px', height: '82px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.smallLogoName"
:style="{ width: '82px', height: '82px' }"
@click="eidtImageOption.type = 'SmallLogo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.smallLogoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteSmallLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'SmallLogo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-form-model>
<edit-image
v-if="showEditImage"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:eidtImageOption="eidtImageOption"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo, postImageFile } from '@/api/company'
import { mapMutations, mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import EditImage from '../components/EditImage.vue'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'CompanyInfo',
mixins: [mixinPermissions],
components: { SpanTitle, EditImage },
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
infoData: {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
},
rule: {
name: [{ required: true, whitespace: true, message: '请输入名称', trigger: 'blur' }],
phone: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的电话号码',
trigger: 'blur',
},
],
faxCode: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的传真号码',
trigger: 'blur',
},
],
email: [
{
required: false,
whitespace: true,
pattern: new RegExp('^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$', 'g'),
message: '请输入正确的邮箱地址',
trigger: 'blur',
},
],
},
getId: -1,
showEditImage: false,
editImage: null,
eidtImageOption: {
type: 'Logo',
fixedNumber: [15, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
},
}
},
async mounted() {
const res = await getCompanyInfo()
if (!res.id) {
this.getId = -1
} else {
this.infoData = res.info
this.getId = res.id
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '公司信息', ['update'])
},
},
methods: {
...mapMutations(['SET_FILENAME', 'SET_SMALL_FILENAME']),
deleteLogo() {
this.infoData.logoName = ''
},
deleteSmallLogo() {
this.infoData.smallLogoName = ''
},
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
if (this.getId === -1) {
await postCompanyInfo(this.infoData)
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.SET_FILENAME(this.infoData.logoName)
this.SET_SMALL_FILENAME(this.infoData.smallFileName)
this.$message.success('保存成功')
} else {
this.$message.warning('检查您的输入是否正确!')
return false
}
})
},
resetForm() {
this.infoData = {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
}
},
customRequest(file) {
const reader = new FileReader()
var self = this
if (this.eidtImageOption.type === 'Logo') {
this.eidtImageOption = {
type: 'Logo',
fixedNumber: [20, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
}
} else if (this.eidtImageOption.type === 'SmallLogo') {
this.eidtImageOption = {
type: 'SmallLogo',
fixedNumber: [4, 4],
title: '编辑企业logo缩略图',
previewWidth: '80px',
previewHeight: '80px',
autoCropWidth: 250,
autoCropHeight: 250,
}
}
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
submitImage(file) {
postImageFile(file).then((res) => {
if (res.file_name) {
if (this.eidtImageOption.type === 'Logo') {
this.infoData.logoName = res.file_name
} else if (this.eidtImageOption.type === 'SmallLogo') {
this.infoData.smallLogoName = res.file_name
}
} else {
}
})
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
},
}
</script>
<style lang="less">
.ops-setting-companyinfo {
padding-top: 15px;
background-color: #fff;
border-radius: 15px;
overflow: auto;
margin-bottom: -24px;
.ops-setting-companyinfo-upload-show {
position: relative;
width: 290px;
height: 100px;
max-height: 100px;
img {
width: 100%;
height: 100%;
}
.delete-icon {
display: none;
}
}
.ant-upload:hover .delete-icon {
display: block;
position: absolute;
top: 5px;
right: 5px;
color: rgb(247, 85, 85);
}
.ant-form-item {
margin-bottom: 10px;
}
.ant-form-item label {
padding-right: 10px;
}
.avatar-uploader > .ant-upload {
// max-width: 100px;
max-height: 100px;
}
// .ant-upload.ant-upload-select-picture-card {
// width: 100%;
// > .ant-upload {
// padding: 0px;
.ant-upload-picture-card-wrapper {
height: 100px;
.ant-upload.ant-upload-select-picture-card {
width: 100%;
height: 100%;
margin: 0;
> .ant-upload {
padding: 0px;
}
}
}
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<a-modal
destroyOnClose
dialogClass="ops-modal"
width="500px"
v-model="visible"
:title="title"
layout="inline"
@cancel="close"
>
<a-form-model v-if="visible && batchProps.type === 'department_id'">
<div :style="{ width: '420px', display: 'inline-block', margin: '0 7px' }">
<div :style="{ height: '40px', lineHeight: '40px' }">选择部门:</div>
<DepartmentTreeSelect v-model="batchForm.value"> </DepartmentTreeSelect>
</div>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'direct_supervisor_id'" ref="ruleForm">
<div :style="{ width: '420px', display: 'inline-block', margin: '0 7px' }">
<div :style="{ height: '40px', lineHeight: '40px' }">选择上级:</div>
<EmployeeTreeSelect v-model="batchForm.value" />
</div>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'position_name'">
<a-form-model-item label="编辑岗位">
<a-input v-model="batchForm.value" />
</a-form-model-item>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'annual_leave'">
<a-form-model-item label="编辑年假">
<a-input-number
:min="0"
:step="1"
:style="{ width: '100%' }"
v-model="batchForm.value"
placeholder="请输入年假"
:formatter="(value) => `${value} 天`"
/>
</a-form-model-item>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'password'" ref="batchForm" :model="batchForm" :rules="rules">
<a-form-model-item label="重置密码" prop="password">
<a-input-password v-model="batchForm.value" />
</a-form-model-item>
<a-form-model-item label="确认密码" prop="repeatPassword">
<a-input-password v-model="batchForm.confirmValue" />
</a-form-model-item>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'block' && batchProps.state === 1">
<a-icon type="info-circle" :style="{ color: '#FF9E58', fontSize: '16px', marginRight: '10px' }" />
<span v-if="batchProps.selectedRowKeys.length > 1">这些用户将会被禁用是否继续?</span>
<span v-else>该用户将会被禁用是否继续?</span>
</a-form-model>
<a-form-model v-else-if="batchProps.type === 'block' && batchProps.state === 0">
<a-icon type="info-circle" :style="{ color: '#FF9E58', fontSize: '16px', marginRight: '10px' }" />
<span v-if="batchProps.selectedRowKeys.length > 1">这些用户将会被恢复是否继续?</span>
<span v-else>该用户将会被恢复是否继续?</span>
</a-form-model>
<template slot="footer">
<a-button key="back" @click="close"> 取消 </a-button>
<a-button key="submit" type="primary" @click="batchModalHandleOk"> 确定 </a-button>
</template>
</a-modal>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { batchEditEmployee } from '@/api/employee'
import EmployeeTreeSelect from '../components/employeeTreeSelect.vue'
import DepartmentTreeSelect from '../components/departmentTreeSelect.vue'
import { getDirectorName } from '@/utils/util'
import Bus from './eventBus/bus'
export default {
components: { Treeselect, DepartmentTreeSelect, EmployeeTreeSelect },
inject: ['provide_allFlatEmployees'],
data() {
const validatePass = (rule, value, callback) => {
console.log(this.batchForm)
if (this.batchForm.value === '') {
callback(new Error('请输入密码'))
} else {
this.$refs.batchForm.validateField('repeatPassword')
callback()
}
}
const validatePass2 = (rule, value, callback) => {
console.log(this.batchForm)
if (this.batchForm.confirmValue === '') {
callback(new Error('请输入密码'))
} else if (this.batchForm.confirmValue !== this.batchForm.value) {
callback(new Error('两次密码不一致'))
} else {
callback()
}
}
return {
visible: false,
batchProps: {},
batchForm: {
value: '',
confirmValue: '',
},
title: '',
rules: {
password: [{ required: true, validator: validatePass, trigger: 'blur' }],
repeatPassword: [{ required: true, validator: validatePass2, trigger: 'blur' }],
},
}
},
computed: {
allFlatEmployees() {
return this.provide_allFlatEmployees()
},
},
methods: {
open(batchProps) {
this.visible = true
this.batchProps = batchProps
const { type, selectedRowKeys, state } = batchProps
this.title = '批量编辑'
if (type === 'department_id') {
this.batchForm.value = null
} else if (type === 'direct_supervisor_id') {
this.batchForm.value = undefined
} else if (type === 'password') {
if (selectedRowKeys.length <= 1) {
this.title = '重置密码'
}
} else if (type === 'block') {
this.batchForm.value = state
if (selectedRowKeys.length <= 1) {
this.title = state ? '禁用' : '恢复'
}
}
},
close() {
this.batchForm.value = ''
this.batchForm.confirmValue = ''
this.visible = false
},
async batchModalHandleOk() {
if (this.batchProps.type === 'direct_supervisor_id') {
this.batchForm.value = this.batchForm.value
? this.batchForm.value.includes('-')
? Number(this.batchForm.value.split('-')[1])
: Number(this.batchForm.value)
: 0
}
if (this.batchProps.type === 'password') {
this.$refs.batchForm.validate(async (valid) => {
if (valid) {
this.sendReq()
}
})
} else {
this.sendReq()
}
},
async sendReq() {
const employeeIdList = this.batchProps.selectedRowKeys.map((item) => item.employee_id)
const res = await batchEditEmployee({
column_name: this.batchProps.type,
column_value: this.batchForm.value,
employee_id_list: employeeIdList,
})
if (res.length) {
this.$notification.error({
message: '操作失败',
description: res
.map((item) => `${getDirectorName(this.allFlatEmployees, item.employee_id)}${item.err}`)
.join('\n'),
duration: null,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
whiteSpace: 'pre-line',
},
})
} else {
this.$message.success('操作成功')
}
if (this.batchProps.type === 'department_id') {
Bus.$emit('clickSelectGroup', 1)
} else {
this.$emit('refresh')
}
Bus.$emit('updataAllIncludeEmployees')
this.close()
},
},
}
</script>

View File

@@ -0,0 +1,550 @@
<template>
<a-modal
:visible="visible"
title="批量导入"
dialogClass="ops-modal setting-structure-upload"
:width="800"
@cancel="close"
>
<div class="setting-structure-upload-steps">
<div
:class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
v-for="(step, index) in stepList"
:key="step.value"
>
<div :class="{ 'setting-structure-upload-step-icon': true }">
<ops-icon :type="step.icon" />
</div>
<span>{{ step.label }}</span>
</div>
</div>
<template v-if="currentStep === 1">
<a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
<a-button :style="{ marginBottom: '20px' }" type="primary"> <a-icon type="upload" />选择文件</a-button>
</a-upload>
<p><a @click="download">点击下载员工导入模板</a></p>
</template>
<div
:style="{ height: '60px', display: 'flex', justifyContent: 'center', alignItems: 'center' }"
v-if="currentStep === 3"
>
导入总数据{{ allCount }}, 导入成功 <span :style="{ color: '#2362FB' }">{{ allCount - errorCount }}</span> ,
导入失败<span :style="{ color: '#D81E06' }">{{ errorCount }}</span
>
</div>
<vxe-table
v-if="currentStep === 2 || has_error"
ref="employeeTable"
stripe
:data="importData"
show-overflow
show-header-overflow
highlight-hover-row
size="small"
class="ops-stripe-table"
:max-height="400"
:column-config="{ resizable: true }"
>
<vxe-column field="email" title="邮箱" min-width="120" fixed="left"></vxe-column>
<vxe-column field="username" title="用户名" min-width="80" ></vxe-column>
<vxe-column field="nickname" title="姓名" min-width="80"></vxe-column>
<vxe-column field="password" title="密码" min-width="80"></vxe-column>
<vxe-column field="sex" title="性别" min-width="60"></vxe-column>
<vxe-column field="mobile" title="手机号" min-width="80"></vxe-column>
<vxe-column field="position_name" title="岗位" min-width="80"></vxe-column>
<vxe-column field="department_name" title="部门" min-width="80"></vxe-column>
<vxe-column field="current_company" v-if="useDFC" title="目前所属主体" min-width="120"></vxe-column>
<vxe-column field="dfc_entry_date" v-if="useDFC" title="初始入职日期" min-width="120"></vxe-column>
<vxe-column field="entry_date" title="目前主体入职日期" min-width="120"></vxe-column>
<vxe-column field="is_internship" title="正式/实习生" min-width="120"></vxe-column>
<vxe-column field="leave_date" title="离职日期" min-width="120"></vxe-column>
<vxe-column field="id_card" title="身份证号码" min-width="120"></vxe-column>
<vxe-column field="nation" title="民族" min-width="80"></vxe-column>
<vxe-column field="id_place" title="籍贯" min-width="80"></vxe-column>
<vxe-column field="party" title="组织关系" min-width="80"></vxe-column>
<vxe-column field="household_registration_type" title="户籍类型" min-width="80"></vxe-column>
<vxe-column field="hometown" title="户口所在地" min-width="80"></vxe-column>
<vxe-column field="marry" title="婚姻情况" min-width="80"></vxe-column>
<vxe-column field="max_degree" title="最高学历" min-width="80"></vxe-column>
<vxe-column field="emergency_person" title="紧急联系人" min-width="120"></vxe-column>
<vxe-column field="emergency_phone" title="紧急联系电话" min-width="120"></vxe-column>
<vxe-column field="bank_card_number" title="卡号" min-width="120"></vxe-column>
<vxe-column field="bank_card_name" title="银行" min-width="80"></vxe-column>
<vxe-column field="opening_bank" title="开户行" min-width="80"></vxe-column>
<vxe-column field="account_opening_location" title="开户地" min-width="120"></vxe-column>
<vxe-column field="school" title="学校" min-width="80"></vxe-column>
<vxe-column field="major" title="专业" min-width="80"></vxe-column>
<vxe-column field="education" title="学历" min-width="80"></vxe-column>
<vxe-column field="graduation_year" title="毕业年份" min-width="120"></vxe-column>
<vxe-column v-if="has_error" field="err" title="失败原因" min-width="120" fixed="right">
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
</template>
</vxe-column>
</vxe-table>
<a-space slot="footer">
<a-button size="small" type="primary" ghost @click="close">取消</a-button>
<a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">上一步</a-button>
<a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">下一步</a-button>
<a-button v-else size="small" type="primary" @click="close">完成</a-button>
</a-space>
</a-modal>
</template>
<script>
import { downloadExcel, excel2Array } from '@/utils/download'
import { importEmployee } from '@/api/employee'
import appConfig from '@/config/app'
export default {
name: 'BatchUpload',
data() {
const stepList = [
{
value: 1,
label: '上传文件',
icon: 'icon-shidi-tianjia',
},
{
value: 2,
label: '确认数据',
icon: 'icon-shidi-yunshangchuan',
},
{
value: 3,
label: '上传完成',
icon: 'icon-shidi-queren',
},
]
const dfc_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'current_company',
'dfc_entry_date',
'entry_date',
'is_internship',
'leave_date',
'id_card',
'nation',
'id_place',
'party',
'household_registration_type',
'hometown',
'marry',
'max_degree',
'emergency_person',
'emergency_phone',
'bank_card_number',
'bank_card_name',
'opening_bank',
'account_opening_location',
'school',
'major',
'education',
'graduation_year',
]
const common_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'entry_date',
'is_internship',
'leave_date',
'id_card',
'nation',
'id_place',
'party',
'household_registration_type',
'hometown',
'marry',
'max_degree',
'emergency_person',
'emergency_phone',
'bank_card_number',
'bank_card_name',
'opening_bank',
'account_opening_location',
'school',
'major',
'education',
'graduation_year',
]
return {
stepList,
dfc_importParamsList,
common_importParamsList,
visible: false,
currentStep: 1,
importData: [],
has_error: false,
allCount: 0,
errorCount: 0,
useDFC: appConfig.useDFC,
}
},
methods: {
open() {
this.importData = []
this.has_error = false
this.errorCount = 0
this.visible = true
},
close() {
this.currentStep = 1
this.visible = false
},
async goNext() {
if (this.currentStep === 2) {
// 此处调用后端接口
this.allCount = this.importData.length
const importData = this.importData.map((item) => {
const { _X_ROW_KEY, ...rest } = item
const keyArr = Object.keys(rest)
keyArr.forEach((key) => {
if (rest[key]) {
rest[key] = rest[key] + ''
}
})
rest.educational_experience = [{
'school': rest.school,
'major': rest.major,
'education': rest.education,
'graduation_year': rest.graduation_year
}]
delete rest.school
delete rest.major
delete rest.education
delete rest.graduation_year
return rest
})
const res = await importEmployee({ employee_list: importData })
if (res.length) {
const errData = res.filter((item) => {
return item.err.length
})
console.log('err', errData)
this.has_error = true
this.errorCount = errData.length
this.currentStep += 1
this.importData = errData
this.$message.error('数据存在错误')
} else {
this.currentStep += 1
this.$message.success('操作成功')
}
this.$emit('refresh')
}
},
goPre() {
this.has_error = false
this.errorCount = 0
this.currentStep -= 1
},
download() {
const data = [
[
{
v:
'1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码密码由6-20位字母、数字组成\n4、部门上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门则默认导入组织架构中顺序靠前的部门',
t: 's',
s: {
alignment: {
wrapText: true,
vertical: 'center',
},
},
},
],
[
{
v: '*邮箱',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*用户名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*姓名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*密码',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '性别',
t: 's',
},
{
v: '手机号',
t: 's',
},
{
v: '岗位',
t: 's',
},
{
v: '部门',
t: 's',
},
{
v: '目前所属主体',
t: 's',
},
{
v: '初始入职日期',
t: 's',
},
{
v: '目前主体入职日期',
t: 's',
},
{
v: '正式/实习生',
t: 's',
},
{
v: '离职日期',
t: 's',
},
{
v: '身份证号码',
t: 's',
},
{
v: '民族',
t: 's',
},
{
v: '籍贯',
t: 's',
},
{
v: '组织关系',
t: 's',
},
{
v: '户籍类型',
t: 's',
},
{
v: '户口所在地',
t: 's',
},
{
v: '婚姻情况',
t: 's',
},
{
v: '最高学历',
t: 's',
},
{
v: '紧急联系人',
t: 's',
},
{
v: '紧急联系电话',
t: 's',
},
{
v: '卡号',
t: 's',
},
{
v: '银行',
t: 's',
},
{
v: '开户行',
t: 's',
},
{
v: '开户地',
t: 's',
},
{
v: '学校',
t: 's',
},
{
v: '专业',
t: 's',
},
{
v: '学历',
t: 's',
},
{
v: '毕业年份',
t: 's',
},
],
]
if (this.useDFC) {
downloadExcel(data, '员工导入模板')
} else {
data[1] = data[1].filter(item => item['v'] !== '目前所属主体')
data[1] = data[1].filter(item => item['v'] !== '初始入职日期')
downloadExcel(data, '员工导入模板')
}
},
customRequest(data) {
this.fileList = [data.file]
excel2Array(data.file).then((res) => {
res = res.filter((item) => item.length)
this.importData = res.slice(2).map((item) => {
const obj = {}
// 格式化日期字段
if (this.useDFC) {
item[9] = this.formatDate(item[9]) // 初始入职日期日期
item[10] = this.formatDate(item[10]) // 目前主体入职日期
item[12] = this.formatDate(item[12]) // 离职日期
item[30] = this.formatDate(item[30]) // 毕业年份
item.forEach((ele, index) => {
obj[this.dfc_importParamsList[index]] = ele
})
} else {
item[8] = this.formatDate(item[8]) // 目前主体入职日期
item[10] = this.formatDate(item[10]) // 离职日期
item[28] = this.formatDate(item[28]) // 毕业年份
item.forEach((ele, index) => {
obj[this.common_importParamsList[index]] = ele
})
}
return obj
})
this.currentStep = 2
})
},
formatDate(numb) {
if (numb) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
time.setMonth(time.getMonth())
time.setHours(time.getHours() - 8)
time.setMinutes(time.getMinutes())
time.setMilliseconds(time.getMilliseconds())
// return time.valueOf()
// 日期格式
const format = 'Y-m-d'
const year = time.getFullYear()
// 由于 getMonth 返回值会比正常月份小 1
let month = time.getMonth() + 1
let day = time.getDate()
month = month > 9 ? month : `0${month}`
day = day > 9 ? day : `0${day}`
const hash = {
'Y': year,
'm': month,
'd': day,
}
return format.replace(/\w/g, o => {
return hash[o]
})
} else {
return null
}
}
},
}
</script>
<style lang="less">
.setting-structure-upload {
.ant-modal-body {
padding: 24px 48px;
}
.setting-structure-upload-steps {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
.setting-structure-upload-step {
display: inline-block;
text-align: center;
position: relative;
.setting-structure-upload-step-icon {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-image: url('../../../assets/icon-bg.png');
margin-bottom: 20px;
> i {
font-size: 40px;
color: #fff;
}
}
> span {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
}
}
.setting-structure-upload-step:not(:first-child)::before {
content: '';
height: 2px;
width: 223px;
position: absolute;
background-color: #e7ecf3;
left: -223px;
top: 43px;
z-index: 0;
}
.selected.setting-structure-upload-step {
&:not(:first-child)::before {
background-color: #7eb0ff;
}
}
.selected {
.setting-structure-upload-step-icon {
background-image: url('../../../assets/icon-bg-selected.png');
}
> span {
color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

View File

@@ -0,0 +1,321 @@
<template>
<li class="ops-setting-companystructure-sidebar-tree">
<div
:class="{
'ops-setting-companystructure-sidebar-group-tree-item': true,
'ops-setting-companystructure-sidebar-group-tree-line': showLine,
isSelected: activeId === TreeData.id || asFatherSelected,
}"
>
<div class="ops-setting-companystructure-sidebar-group-tree-info" @click.stop="selectItem(TreeData)">
<!-- <div class="info-title"> -->
<span :title="TreeData.title">
<ops-icon :style="{ marginRight: '8px' }" :type="icon" />
{{ TreeData.title }}
</span>
<!-- </div> -->
<!-- <span class="item-title"
:title="TreeData.title"
><ops-icon :style="{ marginRight: '8px' }" :type="icon" />{{ TreeData.title }}{{ TreeData.count }}</span
> -->
<div class="ops-setting-companystructure-sidebar-group-tree-info-count-toggle">
<div class="item-count-before">{{ TreeData.count }}</div>
<!-- 显示折叠展开的图标如果没有下级目录的话则不显示 -->
<div class="item-folder">
<span v-if="isFolder" @click.stop="toggle">
<a-icon :style="{ color: '#a1bcfb' }" :type="open ? 'up-circle' : 'down-circle'" theme="filled" />
</span>
</div>
</div>
</div>
<ul v-if="isFolder && open" :style="{ marginLeft: '12px' }">
<draggable v-model="TreeData.children" @end="handleEndDrag(TreeData.children)" :disabled="!isEditable">
<CategroyTree
v-for="(SubTree, SubIndex) in TreeData.children"
:id="SubTree.id"
:key="SubTree.id"
:TreeData="SubTree"
:showLine="SubIndex !== TreeData.children.length - 1"
icon="setting-structure-depart2"
/>
</draggable>
</ul>
</div>
</li>
</template>
<script>
import draggable from 'vuedraggable'
import Bus from '@/views/setting/companyStructure/eventBus/bus'
import { updateDepartmentsSort } from '@/api/company'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'CategroyTree',
mixins: [mixinPermissions],
components: { draggable },
props: {
TreeData: {
type: Object,
required: true,
},
showLine: {
type: Boolean,
},
icon: {
type: String,
default: 'setting-structure-depart2',
},
},
data() {
return {
// 默认不显示下级目录
open: false,
activeId: null,
asFatherSelected: false,
// isClick: 'item-count-before',
}
},
computed: {
// 控制是否有下级目录和显示下级目录
isFolder() {
return this.TreeData.hasSub
},
isEditable() {
return this.hasDetailPermission('backend', '公司架构', ['update'])
},
},
created() {
Bus.$on('changeActiveId', (cid) => {
this.activeId = cid
})
Bus.$on('asFatherSelected', (cid) => {
this.fatherSelected(cid)
})
Bus.$on('resettoggle', (isToggle) => {
this.open = isToggle
})
},
destroyed() {
Bus.$off('changeActiveId')
Bus.$off('asFatherSelected')
},
methods: {
// 点击折叠展开的方法
toggle() {
if (this.isFolder) {
this.selectItem(this.TreeData)
if (!this.open) {
Bus.$emit('reqChildren')
}
this.open = !this.open
}
},
selectItem(selectDepartment) {
Bus.$emit('selectDepartment', selectDepartment)
this.activeId = selectDepartment.id
Bus.$emit('changeActiveId', selectDepartment.id)
Bus.$emit('asFatherSelected', this.TreeData.id)
},
fatherSelected(childId) {
this.asFatherSelected = this.ownIdInChildren(childId, this.TreeData, false)
},
ownIdInChildren(cid, TreeData, flag = false) {
if (TreeData.children) {
if (TreeData.children.map((item) => item.id).includes(cid)) {
flag = true
return true
} else {
return TreeData.children
.map((item) => {
return this.ownIdInChildren(cid, item, flag)
})
.includes(true)
}
} else {
return flag
}
},
handleEndDrag(data) {
updateDepartmentsSort({
department_list: data.map((item, index) => {
return { id: item.id, sort_value: index }
}),
}).then(() => {
Bus.$emit('updateAllIncludeDepartment')
})
},
// mouseOver: function() {
// this.isClick = 'item-count-after'
// },
// mouseLeave: function() {
// this.isClick = 'item-count-before'
// },
},
}
</script>
<style lang="less">
@import '~@/style/static.less';
ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
.ops-setting-companystructure-sidebar-tree {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 30px;
position: relative;
// padding: 7px 0 7px 10px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
.ops-setting-companystructure-sidebar-group-tree-info:hover {
color: #custom_colors[color_1];
> .ops-setting-companystructure-sidebar-group-tree-info::before {
background-color: #custom_colors[color_1];
}
}
// .ops-setting-companystructure-sidebar-group-tree-info:first-child::before {
// content: '';
// position: absolute;
// top: 50%;
// left: 0;
// transform: translateY(-50%);
// display: inline-block;
// width: 5px;
// height: 5px;
// background-color: #cacaca;
// border-radius: 50%;
// }
.ops-setting-companystructure-sidebar-group-tree-item {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
cursor: pointer;
user-select: none;
.ops-setting-companystructure-sidebar-group-tree-info {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: relative;
display: flex;
justify-content: space-between;
margin-bottom: 10px;
> span:first-child {
font-size: 15px;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: calc(100% - 14px - 15px);
// margin-bottom: 10px;
// line-height: 10px;
// height: 5%;
}
.info-title {
display: flex;
align-items: center;
justify-content: center;
// > span:first-child {
// font-size: 15px;
// display: inline-block;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// width: calc(100% - 14px - 15px);
// margin-bottom: 10px;
// }
}
//flex-wrap: wrap;
// align-items: center;
// .item-title{
// display: inline-block;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
.item-count-after {
//position: absolute;
display: inline-block;
margin: 0 auto;
width: 27px;
height: 15px;
background: #ffffff;
border-radius: 2px;
text-align: center;
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-size: 10px;
line-height: 12px;
color: #2f54eb;
}
.ops-setting-companystructure-sidebar-group-tree-info-count-toggle {
display: flex;
align-items: center;
justify-content: center;
.item-count-before {
display: flex;
align-items: center;
justify-content: center;
// display: inline-block;
margin: 0 auto;
width: 27px;
height: 15px;
background: #e1efff;
border-radius: 2px;
text-align: center;
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-size: 10px;
line-height: 12px;
color: #9094a6;
margin-right: 5px;
}
.item-folder {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
// // display: inline-block;
// justify-content: center;
// align-items: center;
}
}
// > span:nth-child(2) {
// color: #a1bcfb!important;
// }
}
}
// .ops-setting-companystructure-sidebar-group-tree-line::after {
// content: '';
// position: absolute;
// width: 1px;
// height: 100%;
// background-color: rgba(0, 0, 0, 0.1);
// top: 12px;
// left: 12px;
// }
.isSelected {
color: #2f54eb;
> .ops-setting-companystructure-sidebar-group-tree-info {
> span:nth-child(2) > i {
color: #a1bcfb !important;
}
}
> .ops-setting-companystructure-sidebar-group-tree-info::before {
background-color: #custom_colors[color_1];
}
}
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<a-modal
destroyOnClose
width="500px"
v-model="visible"
:title="type === 'add' ? '创建子部门' : '编辑部门'"
layout="inline"
@cancel="close"
>
<a-form-model
ref="departmentFormData"
:model="departmentFormData"
:rules="rules"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-model-item ref="title" label="部门名称" prop="department_name">
<a-input v-model="departmentFormData.department_name" placeholder="请输入部门名称" />
</a-form-model-item>
<a-form-model-item label="上级部门" prop="department_parent_id">
<DepartmentTreeSelect v-model="departmentFormData.department_parent_id" />
<!-- <Treeselect
v-else
:multiple="false"
:options="currentDepartmentParentList"
v-model="departmentFormData.department_parent_id"
class="ops-setting-treeselect"
placeholder="请选择上级部门"
:normalizer="
(node) => {
return {
id: node.department_id,
label: node.department_name,
children: node.children,
}
}
"
/> -->
</a-form-model-item>
<a-form-model-item label="部门负责人" prop="department_director_id">
<EmployeeTreeSelect v-model="departmentFormData.department_director_id" />
</a-form-model-item>
</a-form-model>
<template slot="footer">
<a-button key="back" @click="close"> 取消 </a-button>
<a-button key="submit" type="primary" @click="departmentModalHandleOk"> 确定 </a-button>
</template>
</a-modal>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { getParentDepartmentList, putDepartmentById, postDepartment } from '@/api/company'
import EmployeeTreeSelect from '../components/employeeTreeSelect.vue'
import DepartmentTreeSelect from '../components/departmentTreeSelect.vue'
import Bus from './eventBus/bus'
export default {
name: 'DepartmentModal',
components: { Treeselect, EmployeeTreeSelect, DepartmentTreeSelect },
data() {
return {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
visible: false,
departmentFormData: {
department_name: '',
department_parent_id: '',
department_director_id: undefined,
},
rules: {
department_name: [{ required: true, message: '请输入部门名称' }],
department_parent_id: [{ required: true, message: '请选择上级部门' }],
},
currentDepartmentParentList: [],
selectDepartment: {},
type: 'add',
}
},
inject: ['provide_allFlatEmployees'],
computed: {
allFlatEmployees() {
return this.provide_allFlatEmployees()
},
},
mounted() {},
methods: {
async open({ type, selectDepartment }) {
this.selectDepartment = selectDepartment
this.type = type
const { title, parentId, leaderId, id } = selectDepartment
let department_director_id
if (type === 'add') {
this.departmentFormData = {
department_name: '',
department_parent_id: id,
department_director_id,
}
} else if (type === 'edit') {
const res = await getParentDepartmentList({ department_id: id })
this.currentDepartmentParentList = res
if (leaderId) {
const _find = this.allFlatEmployees.find((item) => item.employee_id === leaderId)
department_director_id = `${_find.department_id}-${leaderId}`
}
this.departmentFormData = {
department_name: title,
department_parent_id: parentId,
department_director_id,
}
}
this.visible = true
},
close() {
this.selectDepartment = {}
this.visible = false
},
async departmentModalHandleOk() {
this.$refs.departmentFormData.validate(async (valid) => {
if (valid) {
const { department_director_id } = this.departmentFormData
const params = {
...this.departmentFormData,
department_director_id: department_director_id
? String(department_director_id).split('-')[String(department_director_id).split('-').length - 1]
: undefined,
}
if (this.type === 'edit') {
await putDepartmentById(this.selectDepartment.id, params)
} else if (this.type === 'add') {
await postDepartment(params)
}
this.$message.success('操作成功')
this.$emit('refresh')
Bus.$emit('updateAllIncludeDepartment')
this.close()
}
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,695 @@
<template>
<a-modal
dialogClass="ops-modal"
destroyOnClose
width="810px"
v-model="visible"
:title="title"
layout="inline"
@cancel="close"
:body-style="{ height: `${windowHeight - 320}px`, overflow: 'hidden', overflowY: 'scroll' }"
>
<a-form-model ref="employeeFormData" :model="employeeFormData" :rules="rules" :colon="false">
<a-form-model-item
ref="email"
label="邮箱"
prop="email"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'email') !== -1"
>
<a-input v-model="employeeFormData.email" placeholder="请输入邮箱" />
</a-form-model-item>
<a-form-model-item
ref="username"
label="用户名"
prop="username"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'username') !== -1"
>
<a-input v-model="employeeFormData.username" placeholder="请输入用户名" />
</a-form-model-item>
<a-form-model-item
v-if="type === 'add'"
ref="password"
label="登录密码"
prop="password"
:style="formModalItemStyle"
>
<a-input-password v-model="employeeFormData.password" placeholder="请输入登录密码" />
</a-form-model-item>
<a-form-model-item
ref="nickname"
label="姓名"
prop="nickname"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'nickname') !== -1"
>
<a-input v-model="employeeFormData.nickname" placeholder="请输入姓名" />
</a-form-model-item>
<a-form-model-item
label="性别"
prop="sex"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'sex') !== -1"
>
<a-select v-model="employeeFormData.sex" placeholder="请选择性别">
<a-select-option value=""> </a-select-option>
<a-select-option value=""> </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="mobile"
label="手机号"
prop="mobile"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'mobile') !== -1"
>
<a-input v-model="employeeFormData.mobile" placeholder="请输入手机号" />
</a-form-model-item>
<div
:style="{ width: '361px', display: 'inline-block', margin: '0 7px' }"
v-if="attributes.findIndex((v) => v == 'department_id') !== -1"
>
<div :style="{ height: '41px', lineHeight: '40px' }">部门</div>
<DepartmentTreeSelect v-model="employeeFormData.department_id" />
</div>
<a-form-model-item
ref="position_name"
label="岗位"
prop="position_name"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'position_name') !== -1"
>
<a-input v-model="employeeFormData.position_name" placeholder="请输入岗位" />
</a-form-model-item>
<div
:style="{ width: '361px', display: 'inline-block', margin: '0 7px' }"
v-if="attributes.findIndex((v) => v == 'direct_supervisor_id') !== -1"
>
<div :style="{ height: '41px', lineHeight: '40px' }">上级</div>
<EmployeeTreeSelect v-model="employeeFormData.direct_supervisor_id" />
</div>
<a-form-model-item
ref="annual_leave"
label="年假"
prop="annual_leave"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'annual_leave') !== -1"
>
<a-input-number
:min="0"
:step="1"
:style="{ width: '100%' }"
v-model="employeeFormData.annual_leave"
placeholder="请输入年假"
:formatter="(value) => `${value} 天`"
/>
</a-form-model-item>
<a-form-model-item
ref="virtual_annual_leave"
label="虚拟年假"
prop="virtual_annual_leave"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'virtual_annual_leave') !== -1"
>
<a-input-number
:min="0"
:step="1"
:style="{ width: '100%' }"
v-model="employeeFormData.virtual_annual_leave"
placeholder="请输入虚拟年假"
:formatter="(value) => `${value} 天`"
/>
</a-form-model-item>
<a-form-model-item
ref="parenting_leave"
label="育儿假"
prop="parenting_leave"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'parenting_leave') !== -1"
>
<a-input-number
:min="0"
:step="1"
:style="{ width: '100%' }"
v-model="employeeFormData.parenting_leave"
placeholder="请输入育儿假"
:formatter="(value) => `${value} 天`"
/>
</a-form-model-item>
<a-form-model-item
v-if="useDFC && attributes.findIndex((v) => v == 'current_company') !== -1"
ref="current_company"
label="目前所属主体"
prop="current_company"
:style="formModalItemStyle"
>
<a-input v-model="employeeFormData.current_company" placeholder="请输入目前所属主体" />
</a-form-model-item>
<a-form-model-item
v-if="useDFC && attributes.findIndex((v) => v == 'dfc_entry_date') !== -1"
ref="dfc_entry_date"
label="初始入职日期"
prop="dfc_entry_date"
:style="formModalItemStyle"
>
<a-date-picker
placeholder="请选择初始入职日期"
v-model="employeeFormData.dfc_entry_date"
:style="{ width: '100%' }"
@change="onChange($event, 'dfc_entry_date')"
></a-date-picker>
</a-form-model-item>
<a-form-model-item
ref="entry_date"
label="目前主体入职日期"
prop="entry_date"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'entry_date') !== -1"
>
<a-date-picker
placeholder="请选择目前主体入职日期"
v-model="employeeFormData.entry_date"
:style="{ width: '100%' }"
@change="onChange($event, 'entry_date')"
></a-date-picker>
</a-form-model-item>
<a-form-model-item
ref="is_internship"
label="正式/实习生"
prop="is_internship"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'is_internship') !== -1"
>
<a-select v-model="employeeFormData.is_internship" placeholder="请选择是否正式/实习生">
<a-select-option :value="0"> 正式 </a-select-option>
<a-select-option :value="1"> 实习生 </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="leave_date"
label="离职日期"
prop="leave_date"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'leave_date') !== -1"
>
<a-date-picker
v-model="employeeFormData.leave_date"
placeholder="请选择离职日期"
:style="{ width: '100%' }"
@change="onChange($event, 'leave_date')"
></a-date-picker>
</a-form-model-item>
<a-form-model-item
ref="id_card"
label="身份证号码"
prop="id_card"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'id_card') !== -1"
>
<a-input v-model="employeeFormData.id_card" placeholder="请输入身份证号码" />
</a-form-model-item>
<a-form-model-item
ref="nation"
label="民族"
prop="nation"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'nation') !== -1"
>
<a-input v-model="employeeFormData.nation" placeholder="请输入民族" />
</a-form-model-item>
<a-form-model-item
ref="id_place"
label="籍贯"
prop="id_place"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'id_place') !== -1"
>
<a-input v-model="employeeFormData.id_place" placeholder="请输入籍贯" />
</a-form-model-item>
<a-form-model-item
ref="party"
label="组织关系"
prop="party"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'party') !== -1"
>
<a-select v-model="employeeFormData.party" placeholder="请选择组织关系">
<a-select-option value="党员"> 党员 </a-select-option>
<a-select-option value="团员"> 团员 </a-select-option>
<a-select-option value="群众"> 群众 </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="household_registration_type"
label="户籍类型"
prop="household_registration_type"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'household_registration_type') !== -1"
>
<a-select v-model="employeeFormData.household_registration_type" placeholder="请选择户籍类型">
<a-select-option value="城镇"> 城镇 </a-select-option>
<a-select-option value="农业"> 农业 </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="hometown"
label="户口所在地"
prop="hometown"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'hometown') !== -1"
>
<a-input v-model="employeeFormData.hometown" placeholder="请输入户口所在地" />
</a-form-model-item>
<a-form-model-item
ref="marry"
label="婚姻情况"
prop="marry"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'marry') !== -1"
>
<a-select v-model="employeeFormData.marry" placeholder="请选择婚姻情况">
<a-select-option value="未婚"> 未婚 </a-select-option>
<a-select-option value="已婚"> 已婚 </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="max_degree"
label="最高学历"
prop="max_degree"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'max_degree') !== -1"
>
<a-select v-model="employeeFormData.max_degree" placeholder="请选择最高学历">
<a-select-option value="博士"> 博士 </a-select-option>
<a-select-option value="硕士"> 硕士 </a-select-option>
<a-select-option value="本科"> 本科 </a-select-option>
<a-select-option value="专科"> 专科 </a-select-option>
<a-select-option value="高中"> 高中 </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
ref="emergency_person"
label="紧急联系人"
prop="emergency_person"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'emergency_person') !== -1"
>
<a-input v-model="employeeFormData.emergency_person" placeholder="请输入紧急联系人" />
</a-form-model-item>
<a-form-model-item
ref="emergency_phone"
label="紧急联系电话"
prop="emergency_phone"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'emergency_phone') !== -1"
>
<a-input v-model="employeeFormData.emergency_phone" placeholder="请输入紧急联系电话" />
</a-form-model-item>
<a-form-model-item
label="教育经历"
prop="employeeFormData"
:style="{ display: 'inline-block', width: '100%', margin: '0 7px' }"
v-if="attributes.findIndex((v) => v == 'educational_experience') !== -1"
>
<a-row :gutter="[8, { xs: 8 }]" v-for="item in educational_experience" :key="item.id">
<a-col :span="5">
<a-input v-model="item.school" placeholder="学校" allowClear></a-input>
</a-col>
<a-col :span="5">
<a-input v-model="item.major" placeholder="专业" allowClear></a-input>
</a-col>
<a-col :span="5">
<a-select v-model="item.education" placeholder="学历" allowClear>
<a-select-option value="小学"> 小学 </a-select-option>
<a-select-option value="初中"> 初中 </a-select-option>
<a-select-option value="中专/高中"> 中专/高中 </a-select-option>
<a-select-option value="专科"> 专科 </a-select-option>
<a-select-option value="本科"> 本科 </a-select-option>
<a-select-option value="硕士"> 硕士 </a-select-option>
<a-select-option value="博士"> 博士 </a-select-option>
</a-select>
</a-col>
<a-col :span="5">
<a-month-picker
v-model="item.graduation_year"
placeholder="毕业年份"
@change="onChange($event, 'graduation_year', item.id)"
></a-month-picker>
</a-col>
<a-col :span="1">
<a @click="addEducation">
<a-icon type="plus-circle" />
</a>
</a-col>
<a-col :span="1" v-if="educational_experience.length > 1">
<a @click="() => removeEducation(item.id)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-col>
</a-row>
</a-form-model-item>
<a-form-model-item
label="子女信息"
prop="employeeFormData"
:style="{ display: 'inline-block', width: '100%', margin: '0 7px' }"
v-if="attributes.findIndex((v) => v == 'children_information') !== -1"
>
<!-- <a-space
v-for="(item,index) in educational_experience"
:key="index"
align="baseline"
> -->
<a-row :gutter="[8, { xs: 8 }]" v-for="item in children_information" :key="item.id">
<a-col :span="5">
<a-input v-model="item.name" placeholder="姓名" allowClear></a-input>
</a-col>
<a-col :span="5">
<a-select v-model="item.gender" placeholder="请选择性别" allowClear>
<a-select-option value=""> </a-select-option>
<a-select-option value=""> </a-select-option>
</a-select>
</a-col>
<a-col :span="5">
<a-date-picker
v-model="item.birthday"
placeholder="出生日期"
@change="onChange($event, 'birth_date', item.id)"
></a-date-picker>
</a-col>
<a-col :span="5">
<a-input-number
:min="0"
:step="1"
:style="{ width: '100%' }"
v-model="item.parental_leave_left"
placeholder="请输入剩余育儿假"
:formatter="(value) => `${value} 天`"
/>
</a-col>
<a-col :span="1">
<a @click="addChildren">
<a-icon type="plus-circle" />
</a>
</a-col>
<a-col :span="1" v-if="children_information.length > 1">
<a @click="() => removeChildren(item.id)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-col>
</a-row>
</a-form-model-item>
<a-form-model-item
label="银行卡"
prop="bank_card"
:style="{ display: 'inline-block', width: '98%', margin: '0 7px 24px' }"
v-if="
attributes.findIndex((v) => v == 'bank_card_number') !== -1 ||
attributes.findIndex((v) => v == 'bank_card_name') !== -1 ||
attributes.findIndex((v) => v == 'opening_bank') !== -1 ||
attributes.findIndex((v) => v == 'account_opening_location') !== -1
"
>
<a-row :gutter="[8, { xs: 8 }]">
<a-col :span="6" v-if="attributes.findIndex((v) => v == 'bank_card_number') !== -1">
<a-input v-model="employeeFormData.bank_card_number" placeholder="卡号" allowClear></a-input>
</a-col>
<a-col :span="6" v-if="attributes.findIndex((v) => v == 'bank_card_name') !== -1">
<a-input v-model="employeeFormData.bank_card_name" placeholder="银行" allowClear></a-input>
</a-col>
<a-col :span="6" v-if="attributes.findIndex((v) => v == 'opening_bank') !== -1">
<a-input v-model="employeeFormData.opening_bank" placeholder="开户行" allowClear></a-input>
</a-col>
<a-col :span="6" v-if="attributes.findIndex((v) => v == 'account_opening_location') !== -1">
<a-input v-model="employeeFormData.account_opening_location" placeholder="开户地" allowClear></a-input>
</a-col>
</a-row>
</a-form-model-item>
</a-form-model>
<template slot="footer">
<a-button key="back" @click="close"> 取消 </a-button>
<a-button type="primary" @click="employeeModalHandleOk"> 确定 </a-button>
</template>
</a-modal>
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { postEmployee, putEmployee } from '@/api/employee'
import Bus from './eventBus/bus'
import EmployeeTreeSelect from '../components/employeeTreeSelect.vue'
import DepartmentTreeSelect from '../components/departmentTreeSelect.vue'
import appConfig from '@/config/app'
import moment from 'moment'
import { v4 as uuidv4 } from 'uuid'
export default {
components: { EmployeeTreeSelect, DepartmentTreeSelect },
data() {
return {
visible: false,
employeeFormData: {},
formModalItemStyle: { display: 'inline-block', width: '48%', margin: '0 7px 24px', overflow: 'hidden' },
rules: {
email: [
{ required: true, whitespace: true, message: '请输入邮箱', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
message: '邮箱格式错误',
trigger: 'blur',
},
{ max: 50, message: '字符数须小于50' },
],
username: [
{ required: true, whitespace: true, message: '请输入用户名', trigger: 'blur' },
{ max: 20, message: '字符数须小于20' },
],
password: [{ required: true, whitespace: true, message: '请输入密码', trigger: 'blur' }],
nickname: [
{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' },
{ max: 20, message: '字符数须小于20' },
],
mobile: [
{
pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
},
type: 'add',
useDFC: appConfig.useDFC,
educational_experience: [],
children_information: [],
file_is_show: true,
attributes: [],
}
},
created() {
Bus.$on('getAttributes', (attributes) => {
this.attributes = attributes
})
},
inject: ['provide_allTreeDepartment', 'provide_allFlatEmployees'],
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
departemntTreeSelectOption() {
return this.provide_allTreeDepartment()
},
allFlatEmployees() {
return this.provide_allFlatEmployees()
},
title() {
if (this.type === 'add') {
return '新建员工'
}
return '编辑员工'
},
},
beforeDestroy() {
Bus.$off('getAttributes')
},
methods: {
async open(getData, type) {
// 提交时去掉school, major, education, graduation_year, name, gender, birthday, parental_leave_left
const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } =
getData
const _getData = _.cloneDeep(newGetData)
const { direct_supervisor_id } = newGetData
if (direct_supervisor_id) {
const _find = this.allFlatEmployees.find((item) => item.employee_id === direct_supervisor_id)
_getData.direct_supervisor_id = `${_find.department_id}-${direct_supervisor_id}`
} else {
_getData.direct_supervisor_id = undefined
}
this.employeeFormData = _.cloneDeep(_getData)
// if (type !== 'add' && this.employeeFormData.educational_experience.length !== 0) {
// this.educational_experience = this.employeeFormData.educational_experience
// }
this.children_information = this.formatChildrenInformationList() || [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.educational_experience = this.formatEducationalExperienceList() || [
{
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.type = type
this.visible = true
},
close() {
this.$refs.employeeFormData.resetFields()
this.educational_experience = [
{
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.children_information = [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.visible = false
},
formatChildrenInformationList() {
let arr = []
arr = this.employeeFormData.children_information ? this.employeeFormData.children_information : undefined
if (arr && arr.length) {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
}
return null
},
formatEducationalExperienceList() {
let arr = []
arr = this.employeeFormData.educational_experience ? this.employeeFormData.educational_experience : undefined
if (arr && arr.length) {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
}
return null
},
addEducation() {
const newEducational_experience = {
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null,
}
this.educational_experience.push(newEducational_experience)
},
removeEducation(removeId) {
const _idx = this.educational_experience.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.educational_experience.splice(_idx, 1)
}
},
addChildren() {
const newChildrenInfo = {
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
}
this.children_information.push(newChildrenInfo)
},
removeChildren(removeId) {
const _idx = this.children_information.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.children_information.splice(_idx, 1)
}
},
onChange(date, param, id) {
// if (param === 'graduation_year') {
// if (date === null) {
// this.educational_experience[index].graduation_year = null
// } else {
// this.educational_experience[index].graduation_year = moment(date).format('YYYY-MM-DD')
// }
// } else {
// if (date === null) {
// this.employeeFormData[param] = null
// } else {
// this.employeeFormData[param] = moment(date).format('YYYY-MM-DD')
// }
// }
if (date !== null) {
if (param === 'graduation_year') {
const _idx = this.educational_experience.findIndex((item) => item.id === id)
this.educational_experience[_idx].graduation_year = moment(date).format('YYYY-MM')
} else if (param === 'birth_date') {
const _idx = this.children_information.findIndex((item) => item.id === id)
this.children_information[_idx].birthday = moment(date).format('YYYY-MM-DD')
} else {
this.employeeFormData[param] = moment(date).format('YYYY-MM-DD')
}
}
},
async employeeModalHandleOk() {
if (this.attributes.includes('educational_experience')) {
this.employeeFormData.educational_experience = this.educational_experience
}
if (this.attributes.includes('children_information')) {
this.employeeFormData.children_information = this.children_information
}
// if (!this.employeeFormData.annual_leave) {
// this.employeeFormData.annual_leave = 0
// }
const getFormData = this.employeeFormData
getFormData.direct_supervisor_id = getFormData.direct_supervisor_id
? (getFormData.direct_supervisor_id + '').includes('-')
? +getFormData.direct_supervisor_id.split('-')[1]
: +getFormData.direct_supervisor_id
: 0
this.$refs.employeeFormData.validate(async (valid) => {
if (valid) {
if (this.type === 'add') {
await postEmployee(getFormData)
}
if (this.type === 'edit') {
await putEmployee(getFormData.employee_id, getFormData)
}
this.$message.success('操作成功')
this.$emit('refresh')
Bus.$emit('updataAllIncludeEmployees')
this.close()
} else {
this.$message.warning('检查您的输入是否正确!')
return false
}
})
},
},
}
</script>
<style lang="less" scoped>
.el-date-picker {
width: 100%;
height: 36px;
}
</style>

View File

@@ -0,0 +1,3 @@
// 用于树状递归组件的通信
import Vue from 'vue'
export default new Vue()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
<template>
<a-modal
class="ops-modal"
v-loading="loading"
:title="title"
width="600px"
:append-to-body="true"
:close-on-click-modal="false"
:visible.sync="showDialog"
@cancel="hiddenView"
>
<div class="ops-modal-cropper-box">
<vueCropper
ref="cropper"
:can-move="true"
:auto-crop="true"
:fixed="true"
:img="cropperImg"
output-type="png"
@realTime="realTime"
v-bind="eidtImageOption"
/>
<div class="ops-modal-preview">
<div class="ops-modal-preview-name">预览</div>
<img
:style="{
width: eidtImageOption.previewWidth,
height: eidtImageOption.previewHeight,
border: '1px solid #f2f2f2',
}"
:src="previewImg"
class="ops-modal-preview-img"
/>
</div>
</div>
<div slot="footer" class="ops-modal-dialog-footer">
<a-button type="primary" @click="submitImage()">确定</a-button>
</div>
</a-modal>
</template>
<script type="text/javascript">
import { VueCropper } from 'vue-cropper'
export default {
name: 'EditImage', // 处理头像
components: {
VueCropper,
},
props: {
title: {
type: String,
default: '编辑头像',
},
show: {
type: Boolean,
default: false,
},
image: {
type: String,
default: '',
},
eidtImageOption: {
type: Object,
default: () => {},
},
},
data() {
return {
loading: false,
showDialog: false,
cropperImg: '',
previewImg: '',
}
},
computed: {},
watch: {
show: {
handler(val) {
this.showDialog = val
},
deep: true,
immediate: true,
},
image: function(val) {
this.cropperImg = val
},
},
mounted() {
this.cropperImg = this.image
},
methods: {
realTime(data) {
this.$refs.cropper.getCropData((cropperData) => {
this.previewImg = cropperData
})
},
submitImage() {
// 获取截图的blob数据
this.$refs.cropper.getCropBlob((data) => {
const form = new FormData()
form.append('file', data)
this.$emit('save', form)
this.hiddenView()
})
},
hiddenView() {
this.$emit('close')
},
},
}
</script>
<style lang="less" scoped>
.ops-modal {
.ops-modal-cropper-box {
position: relative;
width: 300px;
height: 300px;
}
.ops-modal-preview {
position: absolute;
bottom: 0;
left: 325px;
.ops-modal-preview-name {
margin-bottom: 8px;
font-size: 13px;
color: #666;
}
.ops-modal-preview-img {
display: block;
}
}
.ops-modal-content {
position: relative;
padding: 0 30px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div :style="{ marginLeft: '10px'}">
<FilterComp
ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
:placement="placement"
@setExpFromFilter="setExpFromFilter"
>
<div slot="popover_item" class="search-form-bar-filter">
<a-icon :class="filterExp.length ? 'search-form-bar-filter-icon' : 'search-form-bar-filter-icon_selected'" type="filter"/>
条件过滤
<a-icon :class="filterExp.length ? 'search-form-bar-filter-icon' : 'search-form-bar-filter-icon_selected'" type="down"/>
</div>
</FilterComp>
</div>
</div>
</template>
<script>
import FilterComp from './settingFilterComp'
export default {
name: 'SearchForm',
components: {
FilterComp,
},
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
placement: {
type: String,
default: 'bottomLeft'
}
},
data() {
return {
filterExp: []
}
},
methods: {
setExpFromFilter(filterExp) {
// const regSort = /(?<=sort=).+/g
// const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
// let expression = ''
// if (filterExp) {
// expression = `q=${filterExp}`
// }
// if (expSort) {
// expression += `&sort=${expSort}`
// }
this.filterExp = filterExp
this.emitRefresh(filterExp)
},
emitRefresh(filterExp) {
this.$nextTick(() => {
this.$emit('refresh', filterExp)
})
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.search-form-bar-filter {
background-color: rgb(240, 245, 255);
.ops_display_wrapper();
.search-form-bar-filter-icon {
color: #custom_colors[color_1];
font-size: 12px;
}
.search-form-bar-filter-icon_selected{
color:#606266;
font-size: 12px;
}
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<treeselect
:multiple="false"
:options="departemntTreeSelectOption"
placeholder="请选择部门"
v-model="treeValue"
:normalizer="normalizer"
noChildrenText=""
noOptionsText=""
class="ops-setting-treeselect"
v-bind="$attrs"
appendToBody
:zIndex="1050"
/>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
export default {
name: 'DepartmentTreeSelect',
components: { Treeselect },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
},
data() {
return {
normalizer: (node) => {
if (node.sub_departments && node.sub_departments.length) {
return {
id: node.department_id,
label: node.department_name,
children: node.sub_departments,
}
}
return {
id: node.department_id,
label: node.department_name,
}
},
}
},
inject: ['provide_allTreeDepartment'],
computed: {
departemntTreeSelectOption() {
return this.provide_allTreeDepartment()
},
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
}
</script>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
<template>
<treeselect
:disable-branch-nodes="multiple ? false : true"
:multiple="multiple"
:options="employeeTreeSelectOption"
placeholder="请选择员工"
v-model="treeValue"
:max-height="200"
noChildrenText=""
noOptionsText=""
:class="className ? className: 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="20"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
>
</treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect'
}
},
data() {
return {}
},
inject: ['provide_allTreeDepAndEmp'],
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp)
},
},
methods: {},
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,37 @@
<template>
<a-modal title="关联员工" :visible="visible" @cancel="handleCancel" @ok="handleOK">
<EmployeeTreeSelect v-model="values" :multiple="true" />
</a-modal>
</template>
<script>
import EmployeeTreeSelect from './employeeTreeSelect.vue'
export default {
name: 'RelateEmployee',
components: { EmployeeTreeSelect },
data() {
return {
visible: false,
values: [],
}
},
methods: {
open() {
this.visible = true
},
handleCancel() {
this.visible = false
this.values = []
},
handleOK() {
this.$emit('relate', {
action: 'add',
employee_id_list: this.values.filter((item) => String(item).includes('-')).map((item) => item.split('-')[1]),
})
this.handleCancel()
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,33 @@
export const ruleTypeList = [
{ value: '&', label: '' },
{ value: '|', label: '' },
// { value: 'not', label: '' },
]
export const expList = [
{ value: 1, label: '等于' },
{ value: 2, label: '不等于' },
// { value: 'contain', label: '包含' },
// { value: '~contain', label: '不包含' },
// { value: 'start_with', label: '以...开始' },
// { value: '~start_with', label: '不以...开始' },
// { value: 'end_with', label: '以...结束' },
// { value: '~end_with', label: '不以...结束' },
{ value: 7, label: '为空' }, // 为空的定义有点绕
{ value: 8, label: '不为空' },
]
export const advancedExpList = [
// { value: 'in', label: 'in查询' },
// { value: '~in', label: '非in查询' },
// { value: 'range', label: '范围' },
// { value: '~range', label: '范围外' },
{ value: 'compare', label: '比较' },
]
export const compareTypeList = [
{ value: 5, label: '大于' },
// { value: '2', label: '>=' },
{ value: 6, label: '小于' },
// { value: '4', label: '<=' },
]

View File

@@ -0,0 +1,380 @@
<template>
<a-popover
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<svg
:style="{ position: 'absolute', top: '0', left: '0', width: '110px', height: '100%', zIndex: '-1' }"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height="100%"
width="100%"
id="svgDom"
></svg>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.relation"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.column"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>value
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<!-- <ValueTypeMapIcon :attr="node.raw" /> -->
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<!-- <ValueTypeMapIcon :attr="node.raw" /> -->
{{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.operator"
:multiple="false"
:clearable="false"
searchable
:options="[...expList, ...compareTypeList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.column) && (item.operator === 1 || item.operator === 2)"
:options="getChoiceValueByProperty(item.column)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<div v-else-if="item.column === 'direct_supervisor_id' && (item.operator === 1 || item.operator === 2)" style="width: 175px">
<EmployeeTreeSelect v-model="item.value" className="custom-treeselect"/>
</div>
<a-input
v-else-if="item.operator !== 7 && item.operator !== 8"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<!-- <div v-else :style="{ width: '175px' }"></div> -->
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><a-icon type="copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)" :style="{ color: 'red' }"><a-icon type="delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import Treeselect from '@riophae/vue-treeselect'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import DepartmentTreeSelect from '../../components/departmentTreeSelect.vue'
import EmployeeTreeSelect from '../../components/employeeTreeSelect.vue'
export default {
name: 'FilterComp',
components: {
// ValueTypeMapIcon,
Treeselect,
DepartmentTreeSelect,
EmployeeTreeSelect },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
inject: ['provide_allFlatEmployees'],
computed: {
allFlatEmployees() {
return this.provide_allFlatEmployees()
}
},
methods: {
visibleChange(open) {
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const _exp = this.ruleList.length
? this.ruleList
: null
if (open && _exp) {
_exp.forEach((item, index) => {
if (item.column === 'direct_supervisor_id' && item.value) {
if (!(item.value + '').includes('-')) {
const _find = this.allFlatEmployees.find((v) => v.employee_id === item.value)
_exp[index].value = `${_find.department_id}-${item.value}`
}
}
})
this.ruleList = _exp
} else if (open) {
this.ruleList = [
{
id: uuidv4(),
relation: '&',
column: this.canSearchPreferenceAttrList[0].value,
operator: 1,
value: null,
},
]
}
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
relation: '&',
column: this.canSearchPreferenceAttrList[0].value,
operator: 1,
value: null,
})
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
relation: '&',
column: this.canSearchPreferenceAttrList[0].value,
operator: 1,
value: null,
},
]
this.filterExp = []
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
const getDataFromRuleList = this.ruleList
getDataFromRuleList.forEach((item, index) => {
if (item.column === 'direct_supervisor_id') {
getDataFromRuleList[index].value = item.value ? (item.value + '').includes('-') ? +item.value.split('-')[1] : +item.value : 0
}
})
getDataFromRuleList[0].relation = '&' // 增删后以防万一第一个不是and
this.$emit('setExpFromFilter', getDataFromRuleList)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 7 || value === 8) {
_ruleList[index] = {
..._ruleList[index],
value: null,
operator: value
}
} else {
_ruleList[index] = {
..._ruleList[index],
operator: value,
}
}
this.ruleList = _ruleList
},
filterOption(input, option) {
return option.componentOptions.children[1].children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
// getExpListByProperty(column) {
// if (column) {
// const _find = this.canSearchPreferenceAttrList.find((item) => item.value === column)
// if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
// return [
// { value: 'is', label: '等于' },
// { value: '~is', label: '不等于' },
// { value: '~value', label: '为空' }, // 为空的定义有点绕
// { value: 'value', label: '不为空' },
// ]
// }
// return this.expList
// }
// return this.expList
// },
isChoiceByProperty(column) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.value === column)
if (_find) {
return _find.is_choice
}
return false
},
getChoiceValueByProperty(column) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.value === column)
if (_find) {
return _find.choice_value
}
return []
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less" scoped>
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<a-row>
<a-col :span="span">
<div class="ops-setting-spantitle"><slot></slot></div>
</a-col>
</a-row>
</template>
<script>
export default {
name: 'SpanTitle',
props: {
span: {
type: Number,
default: 3,
},
},
}
</script>
<style lang="less" scoped>
.ops-setting-spantitle {
height: 28px;
margin-bottom: 12px;
line-height: 28px;
padding-left: 24px;
border-radius: 0px 20px 20px 0px;
font-weight: 700;
color: #0637bf;
background-color: #e0e9ff;
}
</style>

View File

@@ -0,0 +1,367 @@
<template>
<div class="setting-person">
<div class="setting-person-left">
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '1'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
>
<ops-icon type="icon-shidi-yonghu" />个人信息
</div>
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '2'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
>
<a-icon type="unlock" theme="filled" />账号密码
</div>
</div>
<div class="setting-person-right">
<a-form-model
ref="personForm"
:model="form"
:rules="current === '1' ? rules1 : rules2"
:colon="false"
labelAlign="left"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 10 }"
>
<div v-show="current === '1'">
<a-form-model-item label="头像" :style="{ display: 'flex', alignItems: 'center' }">
<a-space>
<a-avatar v-if="form.avatar" :src="`/api/common-setting/v1/file/${form.avatar}`" :size="64"> </a-avatar>
<a-avatar v-else style="backgroundColor:#F0F5FF" :size="64">
<ops-icon type="icon-shidi-yonghu" :style="{ color: '#2F54EB' }" />
</a-avatar>
<a-upload
name="avatar"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '310px', height: '100px' }"
accept="png,jpg,jpeg"
>
<a-button type="primary" ghost size="small">更换头像</a-button>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item label="姓名" prop="nickname">
<a-input v-model="form.nickname" />
</a-form-model-item>
<a-form-model-item label="用户名">
<div class="setting-person-right-disabled">{{ form.username }}</div>
</a-form-model-item>
<a-form-model-item label="邮箱">
<div class="setting-person-right-disabled">{{ form.email }}</div>
</a-form-model-item>
<a-form-model-item label="直属上级">
<div class="setting-person-right-disabled">
{{ getDirectorName(allFlatEmployees, form.direct_supervisor_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="性别">
<a-select v-model="form.sex">
<a-select-option value=""></a-select-option>
<a-select-option value=""></a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="手机号" prop="mobile">
<a-input v-model="form.mobile" />
</a-form-model-item>
<a-form-model-item label="部门">
<div class="setting-person-right-disabled">
{{ getDepartmentName(allFlatDepartments, form.department_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="岗位">
<div class="setting-person-right-disabled">{{ form.position_name }}</div>
</a-form-model-item>
<a-form-model-item label="绑定信息">
<a-space>
<div :class="{ 'setting-person-bind': true, 'setting-person-bind-existed': form.wx_id }">
<ops-icon type="ops-setting-notice-wx" />
</div>
<div @click="handleBindWx" class="setting-person-bind-button">
{{ form.wx_id ? '重新绑定' : '绑定' }}
</div>
</a-space>
</a-form-model-item>
</div>
<div v-show="current === '2'">
<a-form-model-item label="新密码" prop="password1">
<a-input v-model="form.password1" />
</a-form-model-item>
<a-form-model-item label="确认密码" prop="password2">
<a-input v-model="form.password2" />
</a-form-model-item>
</div>
<div style="margin-right: 120px">
<a-form-model-item label=" ">
<a-button type="primary" @click="handleSave" :style="{ width: '100%' }">保存</a-button>
</a-form-model-item>
</div>
</a-form-model>
</div>
<EditImage
v-if="showEditImage"
:fixed-number="eidtImageOption.fixedNumber"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:preview-width="eidtImageOption.previewWidth"
:preview-height="eidtImageOption.previewHeight"
preview-radius="0"
width="550px"
save-button-title="确定"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { getAllDepartmentList, postImageFile } from '@/api/company'
import {
getEmployeeList,
getEmployeeByUid,
updateEmployeeByUid,
updatePasswordByUid,
bindWxByUid,
} from '@/api/employee'
import { getDepartmentName, getDirectorName } from '@/utils/util'
import EditImage from '../components/EditImage.vue'
export default {
name: 'Person',
components: { EditImage },
data() {
const validatePassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请二次确认新密码'))
}
if (value !== this.form.password1) {
callback(new Error('两次输入密码不一致'))
}
callback()
}
return {
current: '1',
form: {},
rules1: {
nickname: [
{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' },
{ max: 20, message: '字符数须小于20' },
],
mobile: [
{
pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
},
rules2: {
password1: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
password2: [{ required: true, message: '两次输入密码不一致', trigger: 'blur', validator: validatePassword }],
},
allFlatEmployees: [],
allFlatDepartments: [],
showEditImage: false,
eidtImageOption: {
type: 'avatar',
fixedNumber: [4, 4],
title: '编辑头像',
previewWidth: '60px',
previewHeight: '60px',
},
editImage: null,
}
},
computed: {
...mapGetters(['uid']),
},
mounted() {
this.getAllFlatEmployees()
this.getAllFlatDepartment()
this.getEmployeeByUid()
},
methods: {
...mapActions(['GetInfo']),
getDepartmentName,
getDirectorName,
getEmployeeByUid() {
getEmployeeByUid(this.uid).then((res) => {
this.form = { ...res }
})
},
getAllFlatEmployees() {
getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => {
this.allFlatEmployees = res.data_list
})
},
getAllFlatDepartment() {
getAllDepartmentList({ is_tree: 0 }).then((res) => {
this.allFlatDepartments = res
})
},
async handleSave() {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar, password1 } = this.form
const params = { nickname, mobile, sex, avatar }
if (this.current === '1') {
await updateEmployeeByUid(this.uid, params).then((res) => {
this.$message.success('保存成功!')
this.getEmployeeByUid()
this.GetInfo()
})
} else {
await updatePasswordByUid(this.uid, { password: password1 }).then((res) => {
this.$message.success('保存成功!')
})
}
}
})
},
customRequest(file) {
const reader = new FileReader()
var self = this
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
submitImage(file) {
postImageFile(file).then((res) => {
if (res.file_name) {
this.form.avatar = res.file_name
}
})
},
async handleBindWx() {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar } = this.form
const params = { nickname, mobile, sex, avatar }
await updateEmployeeByUid(this.uid, params)
bindWxByUid(this.uid)
.then(() => {
this.$message.success('绑定成功!')
})
.finally(() => {
this.getEmployeeByUid()
this.GetInfo()
})
}
})
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.setting-person {
display: flex;
flex-direction: row;
.setting-person-left {
width: 200px;
height: 400px;
margin-right: 24px;
background-color: #fff;
border-radius: 15px;
padding-top: 15px;
.setting-person-left-item {
cursor: pointer;
padding: 10px 20px;
color: #a5a9bc;
border-left: 4px solid #fff;
margin-bottom: 5px;
&:hover {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
> i {
margin-right: 10px;
}
}
.setting-person-left-item-selected {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
}
.setting-person-right {
width: 800px;
height: 700px;
background-color: #fff;
border-radius: 15px;
padding: 24px 48px;
.setting-person-right-disabled {
background-color: #custom_colors[color_2];
border-radius: 4px;
height: 30px;
line-height: 30px;
margin-top: 4px;
padding: 0 10px;
color: #a5a9bc;
}
.setting-person-bind {
width: 40px;
height: 40px;
background: #a5a9bc;
border-radius: 4px;
color: #fff;
font-size: 30px;
text-align: center;
}
.setting-person-bind-existed {
background: #008cee;
}
.setting-person-bind-button {
height: 40px;
width: 72px;
background: #f0f5ff;
border-radius: 4px;
padding: 0 8px;
text-align: center;
cursor: pointer;
}
}
}
</style>
<style lang="less">
.setting-person-right .ant-form-item {
margin-bottom: 12px;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -1,66 +1,61 @@
<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="$t('login.loginHeader')">
<a-form-item>
<a-input
size="large"
type="text"
:placeholder="$t('login.loginName')"
v-decorator="[
'username',
{rules: [{ required: true, message: $t('login.loginNameRequired') }, { validator: handleUsernameOrEmail }], validateTrigger: 'change'}
]"
>
<a-icon slot="prefix" type="user" :style="{ color: 'rgba(0,0,0,.25)' }"/>
</a-input>
</a-form-item>
<div class="ops-login">
<div class="ops-login-left">
<span>维易科技<br />让运维更简单</span>
</div>
<div class="ops-login-right">
<img src="../../assets/logo_VECMDB.png" />
<a-form
id="formLogin"
ref="formLogin"
:form="form"
@submit="handleSubmit"
hideRequiredMark
:colon="false">
<a-form-item label="用户名/邮箱">
<a-input
size="large"
type="text"
class="ops-input"
v-decorator="[
'username',
{
rules: [{ required: true, message: '请输入用户名或邮箱' }, { validator: handleUsernameOrEmail }],
validateTrigger: 'change',
},
]"
>
</a-input>
</a-form-item>
<a-form-item>
<a-input
size="large"
type="password"
autocomplete="false"
:placeholder="$t('login.password')"
v-decorator="[
'password',
{rules: [{ required: true, message: $t('login.passwordRequired') }], 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 label="密码">
<a-input
size="large"
type="password"
autocomplete="false"
class="ops-input"
v-decorator="['password', { rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur' }]"
>
</a-input>
</a-form-item>
<a-form-item>
<a-checkbox v-decorator="['rememberMe']">{{ $t('login.autoLogin') }}</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"
>{{ $t('button.submit') }}</a-button>
</a-form-item>
</a-form>
<a-form-item>
<a-checkbox v-decorator="['rememberMe', { valuePropName: 'checked' }]">自动登录</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>
</div>
</template>
@@ -70,7 +65,7 @@ import { mapActions } from 'vuex'
import { timeFix } from '@/utils/util'
export default {
data () {
data() {
return {
customActiveKey: 'tab1',
loginBtn: false,
@@ -84,16 +79,15 @@ export default {
loginBtn: false,
// login type: 0 email, 1 username, 2 telephone
loginType: 0,
smsSendBtn: false
}
smsSendBtn: false,
},
}
},
created () {
},
created() {},
methods: {
...mapActions(['Login', 'Logout']),
// handler
handleUsernameOrEmail (rule, value, callback) {
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)) {
@@ -103,17 +97,13 @@ export default {
}
callback()
},
handleTabClick (key) {
this.customActiveKey = key
// this.form.resetFields()
},
handleSubmit (e) {
handleSubmit(e) {
e.preventDefault()
const {
form: { validateFields },
state,
customActiveKey,
Login
Login,
} = this
state.loginBtn = true
@@ -122,13 +112,13 @@ export default {
validateFields(validateFieldsKey, { force: true }, (err, values) => {
if (!err) {
console.log('login form', values)
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
})
@@ -140,69 +130,54 @@ export default {
})
},
loginSuccess (res) {
loginSuccess(res) {
console.log(res)
this.$router.push({ path: this.$route.query.redirect })
// 延迟 1 秒显示欢迎信息
setTimeout(() => {
this.$notification.success({
message: this.$t('login.welcome'),
description: `${timeFix()}` + this.$t('login.welcomeBack')
message: '欢迎',
description: `${timeFix()}欢迎回来`,
})
}, 1000)
},
requestFailed (err) {
this.$notification['error']({
message: this.$t('tip.error'),
description: ((err.response || {}).data || {}).message || this.$t('tip.requestFailed'),
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;
}
.ops-login {
width: 100%;
height: 100%;
display: flex;
min-width: 1000px;
overflow-x: auto;
.ops-login-left {
position: relative;
width: 50%;
background: url('../../assets/login_bg.png') no-repeat;
background-position: center;
background-size: cover;
> span {
color: white;
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%);
font-size: 1.75vw;
text-align: center;
}
.register {
float: right;
}
.ops-login-right {
width: 50%;
position: relative;
padding: 10%;
> img {
width: 70%;
margin-left: 15%;
}
.login-button {
width: 100%;
}
}
}

View File

@@ -0,0 +1,23 @@
<template>
<h1>{{ msg }}</h1>
</template>
<script>
import config from '@/config/setting'
import appConfig from '@/config/app'
export default {
name: 'Logout',
data() {
return {
msg: '正在退出,请稍后',
}
},
mounted() {
if (config.useSSO) {
window.location.href = appConfig.ssoLogoutURL
} else {
}
},
}
</script>

View File

@@ -1,34 +0,0 @@
<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>