feat: 审批模版管理 (#964)

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
This commit is contained in:
fit2bot
2021-08-25 19:01:45 +08:00
committed by GitHub
parent ea9316f0a0
commit 5aa3226132
12 changed files with 566 additions and 5 deletions

View File

@@ -23,5 +23,5 @@ VUE_APP_LOGOUT_PATH = '/core/auth/logout/'
# Dev server for core proxy
VUE_APP_CORE_HOST = 'http://localhost:8080'
VUE_APP_CORE_WS = 'ws://localhost:8070'
VUE_APP_CORE_WS = 'ws://localhost:8080'
VUE_APP_ENV = 'development'

View File

@@ -230,6 +230,8 @@
"ReLogin": "重新登录"
},
"common": {
"DateUpdated": "更新日期",
"ApprovaLevel": "审批信息",
"MFAVerify": "验证 MFA",
"ViewSecret": "查看密码",
"ConnectWebSocketError": "连接 WebSocket 失败",
@@ -547,6 +549,9 @@
},
"route": {
"": "",
"TicketFlow": "工单流",
"TicketFlowCreate": "创建审批流",
"TicketFlowUpdate": "更新审批流",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
@@ -661,6 +666,10 @@
"TicketDetail": "工单详情",
"TicketCreate": "创建工单",
"Tickets": "工单管理",
"Templates": "模版管理",
"TemplateDetail": "模版详情",
"TemplateCreate": "创建模版",
"TemplateUpdate": "更新模版",
"UserCreate": "创建用户",
"UserDetail": "用户详情",
"UserFirstLogin": "首次登录",
@@ -899,6 +908,12 @@
"setting": "设置"
},
"tickets": {
"OneAssigneeType": "一级受理人类型",
"OneAssignee": "一级受理人",
"TwoAssigneeType": "二级受理人类型",
"TwoAssignee": "二级受理人",
"ApprovalLevel": "审批级别",
"FlowDetail": "流程详情",
"PermissionName": "授权规则名称",
"Accept": "同意",
"AssignedMe": "待我审批",
@@ -916,7 +931,6 @@
"status": "状态",
"title": "标题",
"action": "动作",
"IPGroup": "IP 组",
"type": "类型",
"user": "用户",
"Status": "状态",
@@ -1183,6 +1197,9 @@
"Log": "日志",
"DeleteReleasedAssets": "删除已释放资产"
},
"Template": {
"Template": "模版管理"
},
"Corporation": "公司",
"Edition": "版本",
"Execute": "执行",

View File

@@ -12,6 +12,7 @@
<template v-for="item in submenu">
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name" :disabled="item.disabled">
<span slot="label">
<i v-if="item.icon" class="fa " :class="item.icon" />
{{ item.title }}
<slot name="badge" :tab="item.name" />
</span>

View File

@@ -1,4 +1,5 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'tickets',
@@ -55,5 +56,35 @@ export default [
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'flows',
name: 'TicketFlowList',
component: empty,
meta: { title: i18n.t('route.TicketFlow'), icon: 'check-square-o', activeMenu: '/tickets/tickets' },
hidden: true,
children: [
{
path: 'create',
name: 'TicketFlowCreate',
component: () => import('@/views/tickets/TicketFlow/FlowCreateUpdate'),
meta: { title: i18n.t('route.TicketFlowCreate') },
hidden: true
},
{
path: ':id/update',
name: 'TicketFlowUpdate',
component: () => import('@/views/tickets/TicketFlow/FlowCreateUpdate'),
meta: { title: i18n.t('route.TicketFlowUpdate') },
hidden: true
}
]
},
{
path: 'tickets/flow/:id',
name: 'FlowDetail',
component: () => import('@/views/tickets/TicketFlow/Detail/index'),
meta: { title: i18n.t('route.TicketFlow'), activeMenu: '/tickets/tickets' },
hidden: true
}
]

View File

@@ -0,0 +1,115 @@
<template>
<GenericTicketDetail
:object="object"
:detail-card-items="detailCardItems"
:special-card-items="specialCardItems"
/>
</template>
<script>
import { formatTime, getDateTimeStamp } from '@/utils/index'
import { toSafeLocalDateStr } from '@/utils/common'
import GenericTicketDetail from '@/views/tickets/TicketFlow/components/GenericTicketDetail'
export default {
name: '',
components: { GenericTicketDetail },
props: {
object: {
type: Object,
default: () => ({})
}
},
data() {
return {
comments: ''
}
},
computed: {
detailCardItems() {
return [
{
key: this.$t('tickets.type'),
value: this.object.type_display
},
{
key: this.$t('tickets.ApprovalLevel'),
value: this.object.approval_level + '级'
},
{
key: this.$t('common.CreatedBy'),
value: this.object.created_by
},
{
key: this.$t('common.dateCreated'),
value: toSafeLocalDateStr(this.object.date_created)
},
{
key: this.$t('common.DateUpdated'),
value: toSafeLocalDateStr(this.object.date_updated)
}
]
},
specialCardItems() {
// eslint-disable-next-line no-unused-vars
const approvalData = [
{
key: this.$t('tickets.OneAssigneeType'),
value: ''
},
{
key: this.$t('tickets.OneAssignee'),
value: ''
},
{
key: this.$t('tickets.TwoAssigneeType'),
value: ''
},
{
key: this.$t('tickets.TwoAssignee'),
value: ''
}]
this.object.rules.forEach((item, index) => {
console.log(item, index)
if (index === 0) {
approvalData[0].value = item.strategy_display
approvalData[1].value = item.assignees_display.join(',')
} else {
approvalData[2].value = item.strategy_display
approvalData[3].value = item.assignees_display.join(',')
}
})
return approvalData.slice(0, this.object.rules.length * 2)
}
},
methods: {
formatTime(dateStr) {
return formatTime(getDateTimeStamp(dateStr))
},
toSafeLocalDateStr(dataStr) {
return toSafeLocalDateStr(dataStr)
},
reloadPage() {
window.location.reload()
}
}
}
</script>
<style scoped>
.assets{
margin-top: 14px;
}
.feed-activity-list .feed-element {
border-bottom: 1px solid #e7eaec;
}
.feed-element > .pull-left {
margin-right: 10px;
}
.feed-element .header-avatar {
width: 38px;
height: 38px;
}
.box {
margin-bottom: 15px;
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<GenericDetailPage :object.sync="ticket" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<component :is="config.activeMenu" :object="ticket" />
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage, TabPage } from '@/layout/components'
import TicketDetail from './TicketDetail'
export default {
components: {
GenericDetailPage,
TicketDetail,
TabPage
},
data() {
return {
ticket: { user_display: '', type_display: '', status: '', assignees_display: '', date_created: '' },
config: {
activeMenu: 'TicketDetail',
url: '',
submenu: [
{
title: this.$t('route.TicketDetail'),
name: 'TicketDetail'
}
],
actions: {
detailApiUrl: `/api/v1/tickets/flows/${this.$route.params.id}/`
},
getObjectName: this.getObjectName,
hasRightSide: false
}
}
},
mounted() {
},
methods: {
getObjectName() {
return this.ticket.id
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,60 @@
<template>
<GenericCreateUpdatePage :initial="initial" v-bind="$data" />
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import FlowRuleField from './FlowRuleField'
export default {
name: 'FlowCreateUpdate',
components: {
GenericCreateUpdatePage
},
data() {
return {
loading: true,
fields: [
[this.$t('common.Basic'), ['type']],
[this.$t('common.ApprovaLevel'), ['approval_level', 'rules']]
],
fieldsMeta: {
rules: {
label: '审批流程',
component: FlowRuleField,
el: {
level: 1
},
hidden: (form) => {
this.fieldsMeta.rules.el.level = form['approval_level']
}
}
},
getUrl() {
const params = this.$route.params
let url = `/api/v1/tickets/flows/`
if (params.id) {
url = `${url}${params.id}/`
}
return `${url}`
},
updateSuccessNextRoute: { name: 'TicketList' },
createSuccessNextRoute: { name: 'TicketList' }
}
},
computed: {
initial() {
return this.$route.query
}
},
mounted() {
if (this.$store.state.users.profile.user_all_orgs.length > 0) {
this.initial.org_id = this.$store.state.users.profile.user_all_orgs[0].id
}
this.loading = false
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,97 @@
<template>
<!-- 自定义组件 my-input -->
<div>
<div v-for="(item, i) of data.slice(0, level)" :key="i" style="margin-bottom: 10px">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>{{ i + 1 + '级审批' }}</span>
</div>
<el-radio-group v-model="item.strategy" @change="onChange()">
<el-radio label="super">超级管理员</el-radio>
<el-radio label="admin">组织用户</el-radio>
<el-radio label="super_admin">超级管理员+组织用户</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
<br>
<Select2 v-show="item.strategy === 'custom'" v-model="item.assignees" v-bind="select2Option" @change="onChange()" />
</el-card>
</div>
</div>
</template>
<script>
import Select2 from '@/components/FormFields/Select2'
export default {
components: {
Select2
},
props: {
value: {
type: [String, Array],
default: () => []
},
level: {
type: Number,
default: 1
}
},
data() {
return {
vm: this,
data: [],
initData: [
{
strategy: 'super',
assignees_read_only: []
},
{
strategy: 'super',
assignees_read_only: []
}
],
select2Option: {
url: '/api/v1/users/users/'
},
fields: [
]
}
},
computed: {
},
watch: {
level(value) {
console.log('Value is: ', value)
}
},
mounted() {
this.data = this.value.concat(this.initData)
this.data.forEach(item => {
item.assignees = item.assignees_read_only
delete item.assignees_read_only
})
this.$emit('input', this.data.slice(0, this.level))
},
methods: {
onChange() {
this.$emit('input', this.data.slice(0, this.level))
}
}
}
</script>
<style>
.text {
font-size: 14px;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
}
.item {
padding: 18px 0;
}
.box-card {
width: 600px;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<GenericListTable ref="GenericListTable" :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import { GenericListTable } from '@/layout/components'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'TicketFlow',
components: {
GenericListTable
},
data() {
const vm = this
return {
tableConfig: {
url: '/api/v1/tickets/flows/',
columns: [
'type_display', 'created_by',
'date_created', 'date_updated', 'actions'
],
columnsShow: {
min: ['actions'],
default: [
'type_display', 'created_by',
'date_created', 'date_updated', 'actions'
]
},
columnsMeta: {
type_display: {
formatter: DetailFormatter,
formatterArgs: {
route: 'FlowDetail'
}
},
actions: {
prop: 'actions',
formatterArgs: {
hasClone: false,
hasDelete: false,
onClone: ({ row }) => {
vm.$router.push({ name: 'TicketFlowUpdate', query: { type: row.type, clone_from: row.id }})
},
onUpdate: ({ row }) => {
vm.$router.push({ name: 'TicketFlowUpdate', params: { id: row.id }})
}
}
}
}
},
headerActions: {
hasLeftActions: false,
hasRightActions: false
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,77 @@
<template>
<IBox class="box">
<div slot="header" class="clearfix ibox-title">
<i class="fa fa-info-circle" /> {{ title }}
</div>
<div class="content">
<el-row :gutter="10">
<el-col v-for="item in detailCardItems" :key="'card-' + item.key" :span="12">
<el-row class="item">
<el-col :span="6">
<div :style="{ 'text-align': 'align' }" class="item-label">
<label>{{ item.key }}: </label>
</div>
</el-col>
<el-col :span="18">
<div class="item-text">
<ItemValue v-bind="item" />
</div>
</el-col>
</el-row>
</el-col>
</el-row>
<el-divider v-if="specialCardItems.length > 0" />
<el-row :gutter="10">
<el-col v-for="item in specialCardItems" :key="'card-' + item.key" :span="24">
<el-row class="item">
<el-col :span="6">
<div :style="{ 'text-align': 'align' }" class="item-label">
<label>{{ item.key }}: </label>
</div>
</el-col>
<el-col :span="18">
<div class="item-text">
<ItemValue v-bind="item" />
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</IBox>
</template>
<script>
import ItemValue from '@/components/DetailCard/ItemValue'
import IBox from '@/components/IBox'
export default {
name: 'Details',
components: { ItemValue, IBox },
props: {
specialCardItems: {
type: Array,
default: () => ([])
},
detailCardItems: {
type: Array,
default: () => ([])
},
title: {
type: String,
default: ''
}
},
data() {
return {}
}
}
</script>
<style lang='less' scoped>
.box {
margin-bottom: 15px;
}
.content {
font-size: 13px;
line-height: 2.5;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<el-row>
<el-col :span="17">
<Details :detail-card-items="detailCardItems" :title="$t('common.BasicInfo')" />
<Details v-if="specialCardItems.length > 0" :detail-card-items="specialCardItems" :title="$t('common.ApprovaLevel')" />
<slot id="MoreDetails" />
</el-col>
</el-row>
</template>
<script>
import Details from './Details'
export default {
name: 'GenericTicketDetail',
components: { Details },
props: {
object: {
type: Object,
default: () => ({})
},
specialCardItems: {
type: Array,
default: () => ([])
},
detailCardItems: {
type: Array,
default: () => ([])
}
},
data() {
return {}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,6 +1,13 @@
<template>
<TabPage :active-menu.sync="config.activeMenu" :submenu="config.submenu">
<el-badge v-if="props.tab === 'AssignedTicketList'" slot="badge" slot-scope="props" :value="getBadgeValue(props)" size="mini" type="primary" />
<el-badge
v-if="props.tab === 'AssignedTicketList'"
slot="badge"
slot-scope="props"
:value="getBadgeValue(props)"
size="mini"
type="primary"
/>
<keep-alive>
<component :is="config.activeMenu" />
</keep-alive>
@@ -10,16 +17,18 @@
<script>
import { TabPage } from '@/layout/components'
import { mapGetters } from 'vuex'
import { getTicketOpenCount } from '@/api/ticket'
import AssignedTicketList from './AssignedTicketList'
import MyTicketList from './MyTicketList'
import { getTicketOpenCount } from '@/api/ticket'
import TicketFlow from './TicketFlow/TicketFlow'
export default {
name: 'Index',
components: {
TabPage,
AssignedTicketList,
MyTicketList
MyTicketList,
TicketFlow
},
data() {
return {
@@ -34,6 +43,11 @@ export default {
{
title: this.$t('tickets.AssignedMe'),
name: 'AssignedTicketList'
},
{
title: '流程设置',
icon: 'fa-gear',
name: 'TicketFlow'
}
]
}