三.运维平台之工单系统
一.工单系统需求分析
二.工单系统后端
(python36env) [vagrant@CentOS devops]$ django-admin startapp workorder (python36env) [vagrant@CentOS devops]$ mv workorder apps/ settings.py: INSTALLED_APPS = [ ..... \'workorder\', ....... ]
(1)apps/workorder/models.py:
from django.db import models from users.models import User class WorkOrder(models.Model): TYPE = ( (0, \'数据库\'), (1, \'WEB服务\'), (2, \'计划任务\'), (3, \'配置文件\'), (4, \'其它\'), ) STATUS = ( (0, \'申请\'), (1, \'处理中\'), (2, \'完成\'), (3, \'失败\'), ) title = models.CharField(max_length=100, verbose_name=u\'工单标题\') type = models.IntegerField(choices=TYPE, default=0, verbose_name=u\'工单类型\') order_contents = models.TextField(verbose_name=\'工单内容\') applicant = models.ForeignKey(User, verbose_name=u\'申请人\', related_name=\'work_order_applicant\',on_delete=models.CASCADE) assign_to = models.ForeignKey(User, verbose_name=u\'指派给\',on_delete=models.CASCADE) final_processor = models.ForeignKey(User, null=True, blank=True, verbose_name=u\'最终处理人\', related_name=\'final_processor\',on_delete=models.CASCADE) status = models.IntegerField(choices=STATUS, default=0, verbose_name=u\'工单状态\') result_desc = models.TextField(verbose_name=u\'处理结果\', blank=True, null=True) apply_time = models.DateTimeField(auto_now_add=True, verbose_name=u\'申请时间\') complete_time = models.DateTimeField(auto_now=True, verbose_name=u\'处理完成时间\') def __str__(self): return self.title class Meta: verbose_name = \'工单\' verbose_name_plural = verbose_name ordering = [\'-complete_time\']
(python36env) [vagrant@CentOS devops]$ python manage.py makemigrations workorder
(python36env) [vagrant@CentOS devops]$ python manage.py migrate workorder
(2)workorder/serializers.py:
from rest_framework import serializers from django.contrib.auth import get_user_model from .models import WorkOrder from datetime import datetime User = get_user_model() class WorkOrderSerializer(serializers.ModelSerializer): """ 工单序列化类 """ # 获取当前登陆用户,并将其赋值给数据库中对应的字段 applicant = serializers.HiddenField( default=serializers.CurrentUserDefault()) # 后端格式时间 # apply_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) # complete_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) class Meta: model = WorkOrder fields = "__all__" def to_representation(self, instance): applicant_obj = instance.applicant assign_to_obj = instance.assign_to final_processor_obj = instance.final_processor type_value = instance.get_type_display() # print("new:", type_value) # print("old:", instance.type) status_value = instance.get_status_display() ret = super(WorkOrderSerializer, self).to_representation(instance) ret[\'type\'] = { "id": instance.type, "name": type_value } ret[\'status\'] = { "id": instance.status, "name": status_value } ret["applicant"] = { "id": applicant_obj.id, "name": applicant_obj.name }, ret["assign_to"] = { "id": assign_to_obj.id, "name": assign_to_obj.name }, if final_processor_obj: ret["final_processor"] = { "id": final_processor_obj.id, "name": final_processor_obj.name }, # print(ret) return ret # def create(self, validated_data): # applicant = self.context[\'request\'].user #获取用户信息 # validated_data[\'applicant\'] = applicant # print(validated_data) # instance = self.Meta.model.objects.create(**validated_data) # instance.save() # return instance # def update(self, instance, validated_data): # final_processor = self.context[\'request\'].user # print(validated_data) # validated_data[\'final_processor\'] = final_processor # instance = self.Meta.model.objects.filter(id=instance.id).update(**validated_data) # return instance
(3)workorder/views.py:
from django.contrib.auth import get_user_model from rest_framework import viewsets, mixins,permissions, status from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from rest_framework import filters from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authentication import TokenAuthentication,BasicAuthentication,SessionAuthentication from rest_framework_jwt.authentication import JSONWebTokenAuthentication import time from .serializers import WorkOrderSerializer from .models import WorkOrder User = get_user_model() class Pagination(PageNumberPagination): page_size = 10 page_size_query_param = \'page_size\' page_query_param = "page" max_page_size = 100 class WorkOrderViewset(viewsets.ModelViewSet): """ create: 创建工单 list: 获取工单列表 retrieve: 获取工单信息 update: 更新更新信息 delete: 删除用户 """ authentication_classes = (JSONWebTokenAuthentication, TokenAuthentication, SessionAuthentication, BasicAuthentication) permission_classes = (permissions.IsAuthenticated, permissions.DjangoModelPermissions) queryset = WorkOrder.objects.all() serializer_class = WorkOrderSerializer pagination_class = Pagination filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) search_fields = (\'title\', \'order_contents\') ordering_fields = (\'id\',) def get_queryset(self): status = self.request.GET.get(\'status\', None) applicant = self.request.user # 获取当前登陆用户所有组的信息, RBAC 用户-->组-->权限 role = applicant.groups.all().values(\'name\') # print(role) role_name = [r[\'name\'] for r in role] # print(role_name) queryset = super(WorkOrderViewset, self).get_queryset() # 判断传来的status值判断是申请列表还是历史列表 if status and int(status) == 1: queryset = queryset.filter(status__lte=int(status)) elif status and int(status) == 2: queryset = queryset.filter(status__gte=int(status)) else: pass # 判断登陆用户是否是管理员,是则显示所有工单,否则只显示自己的 if "sa" not in role_name: queryset = queryset.filter(applicant=applicant) return queryset def partial_update(self, request, *args, **kwargs): pk = int(kwargs.get("pk")) print(pk) final_processor = self.request.user data = request.data data[\'final_processor\'] = final_processor data[\'complete_time\'] = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime(time.time())) print(data) WorkOrder.objects.filter(pk=pk).update(**data) return Response(status=status.HTTP_204_NO_CONTENT)
(4)workorder/filters.py
import django_filters from django.contrib.auth import get_user_model from.models import WorkOrder User = get_user_model() class WorkOrderFilter(django_filters.rest_framework.FilterSet): """ 工单过滤类 """ class Meta: model = WorkOrder fields = [\'title\']
(5)workorder/router.py:
from rest_framework.routers import DefaultRouter from .views import WorkOrderViewset workorder_router = DefaultRouter() workorder_router.register(r\'workorder\', WorkOrderViewset, basename="workorder")
(6)devops/urls.py
from workorder.router import workorder_router router.registry.extend(workorder_router.registry)
效果如图:
三.工单前端处理
安装插件:
F:\devops\data\web\vueAdmin-template>npm install moment –save
1.工单基础页面实现
(1)src/api/workorder/workorder.js
import request from \'@/utils/request\' // 获取工单列表 export function getWorkOrderList(params) { return request({ url: \'/api/workorder/\', method: \'get\', params }) } // 创建工单 export function createWorkOrder(data) { return request({ url: \'/api/workorder/\', method: \'post\', data }) } // 修改工单 export function updateWorkOrder(id, data) { return request({ url: \'/api/workorder/\' + id + \'/\', method: \'patch\', data }) } // 删除工单 export function deleteWorkOrder(id) { return request({ url: \'/api/workorder/\' + id + \'/\', method: \'delete\' }) }
(2)src/router/index.js
{ path: \'/workorder\', component: Layout, name: \'workorder\', meta: { title: \'工单系统\', icon: \'form\' }, children: [ { path: \'apply\', name: \'工单申请\', meta: { title: \'工单申请\', icon: \'form\' } }, { path: \'list\', name: \'申请列表\', component: () => import(\'@/views/workorder/list/index\'), meta: { title: \'申请列表\', icon: \'table\' } }, { path: \'history\', name: \'工单历史\', meta: { title: \'工单历史\', icon: \'table\' } } ] },
(3)views/workorder/list/index.vue
<template> <div class="workorder"> <div> <!--搜索--> <el-col :span="8"> <el-input v-model="params.search" placeholder="搜索" @keyup.enter.native="searchClick"> <el-button slot="append" icon="el-icon-search" @click="searchClick"/> </el-input> </el-col> </div> <!--表格--> <order-list :value="workorders" @rate="handleRate" @edit="handleEdit" @delete="handleDelete"/> <!--模态窗任务进度--> <el-dialog :title="currentValue.title" :visible.sync="dialogVisibleForRate" width="30%"> <div style="height: 300px;"> <el-steps :active="active" direction="vertical" finish-status="success" > <el-step v-for="(item,index) in rate" :title="item.title" :description="item.description" :key="index" /> </el-steps> </div> </el-dialog> <!--模态窗工单处理--> <el-dialog :visible.sync="dialogVisibleForEdit" title="工单处理" width="50%"> <order-form ref="workorderForm" :form="currentValue" @submit="handleSubmitEdit" @cancel="handleCancelEdit"/> </el-dialog> <!--分页--> <center> <el-pagination :page-size="pagesize" :total="totalNum" background layout="total, prev, pager, next, jumper" @current-change="handleCurrentChange"/> </center> </div> </template> <script> import { getWorkOrderList, updateWorkOrder } from \'@/api/workorder/workorder\' import OrderList from \'./table\' export default { name: \'Workorder\', components: { OrderList }, data() { return { dialogVisibleForEdit: false, dialogVisibleForRate: false, currentValue: {}, workorders: [], totalNum: 0, pagesize: 10, active: 1, apply: {}, assign: {}, final_processor: {}, rate: [], params: { page: 1, search: \'\', status: 1 } } }, created() { this.fetchData() }, methods: { fetchData() { getWorkOrderList(this.params).then( res => { this.workorders = res.results console.log(this.workorders) this.totalNum = res.count }) }, handleCurrentChange(val) { this.params.page = val this.fetchData() }, searchClick() { this.fetchData() }, /* 流程进度处理函数 */ handleRate(value) { this.currentValue = { ...value } console.log(value) this.dialogVisibleForRate = true this.rate = [] this.final_processor = {} this.apply[\'title\'] = \'任务申请: \' + value.applicant[0].name + \': \' + value.apply_time this.assign[\'title\'] = \'任务分配: \' + value.assign_to[0].name if (value.final_processor) { this.final_processor[\'title\'] = \'任务领取: \' + value.final_processor[0].name + \': \' + value.complete_time this.active = 3 } this.rate.push(this.apply) this.rate.push(this.assign) this.rate.push(this.final_processor) }, /* 处理工单,弹出模态窗、提交数据、取消 */ handleEdit(value) { this.currentValue = { ...value } console.log(this.currentValue) const data = { \'status\': 1 } const id = this.currentValue.id updateWorkOrder(id, data).then(res => { this.$message({ message: \'接受工单\', type: \'success\' }) this.dialogVisibleForEdit = true this.fetchData() }) }, handleSubmitEdit(value) { const { id, ...params } = value // 很神奇,能把表单的值拆解成自己想要的样子 console.log(params) const data = { \'status\': 2, \'result_desc\': params.result_desc } updateWorkOrder(id, data).then(res => { this.$message({ message: \'更新成功\', type: \'success\' }) this.handleCancelEdit() this.fetchData() }) }, handleCancelEdit() { this.dialogVisibleForEdit = false this.$refs.workorderForm.$refs.form.resetFields() }, /* 取消 */ handleDelete(id) { const data = { \'status\': 3 } updateWorkOrder(id, data).then(res => { this.$message({ message: \'取消成功\', type: \'success\' }) this.fetchData() }, err => { console.log(err.message) }) } } } </script> <style lang=\'scss\' scoped> .workorder { padding: 10px; } </style>
(4)views/workorder/list/table.vue
<template> <div class="workorder-list"> <el-table :data="value" border stripe style="width: 100%"> <el-table-column type="expand"> <template slot-scope="props"> <span><pre>工单详情:{{ props.row.order_contents }}</pre></span> </template> </el-table-column> <el-table-column label="工单类型" prop="type.name"/> <el-table-column label="工单标题" prop="title"/> <el-table-column label="申请人" prop="applicant[0].name"/> <el-table-column label="工单状态" prop="status.name"/> <el-table-column label="任务进度" align="center"> <template slot-scope="scope"> <el-button type="text" size="small" @click="handleRate(scope.row)"> {{ \'任务进度\' }} </el-button> </template> </el-table-column> <el-table-column :formatter="dateFormat" label="申请时间" prop="apply_time" /> <el-table-column label="操作"> <template slot-scope="scope"> <el-button size="mini" type="primary" @click="handleEdit(scope.row)">处理</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.row)">取消</el-button> </template> </el-table-column> </el-table> </div> </template> <script> import moment from \'moment\' export default { name: \'OrderList\', props: { value: { type: Array, default: function() { return [] } } }, methods: { /* 点击编辑按钮,将子组件的事件传递给父组件 */ handleEdit(value) { this.$emit(\'edit\', value) }, /* 流程进度 */ handleRate(value) { this.$emit(\'rate\', value) }, /* 删除 */ handleDelete(workorder) { const id = workorder.id const name = workorder.title this.$confirm(`此操作将删除: ${name}, 是否继续?`, \'提示\', { confirmButtonText: \'确定\', cancelButtonText: \'取消\', type: \'warning\' }).then(() => { this.$emit(\'delete\', id) }).catch(() => { this.$message({ type: \'info\', message: \'已取消删除\' }) }) }, dateFormat: function(row, column) { const date = row[column.property] if (date === undefined) { return \'\' } return moment(date).format(\'YYYY-MM-DD HH:mm:ss\') } } } </script> <style lang=\'scss\'> </style>
效果如图:
2.历史工单
(1)src/router/index.js
{ path: \'history\', name: \'工单历史\', component: () => import(\'@/views/workorder/history/index\'), meta: { title: \'工单历史\', icon: \'table\' } }
(1)workorder/history/index.vue
<template> <div class="workorder"> <div> <!--搜索--> <el-col :span="8"> <el-input v-model="params.search" placeholder="搜索" @keyup.enter.native="searchClick"> <el-button slot="append" icon="el-icon-search" @click="searchClick"/> </el-input> </el-col> </div> <!--表格--> <order-list :value="workorders"/> <!--分页--> <center> <el-pagination :page-size="pagesize" :total="totalNum" background layout="total, prev, pager, next, jumper" @current-change="handleCurrentChange"/> </center> </div> </template> <script> import { getWorkOrderList } from \'@/api/workorder/workorder\' import OrderList from \'./table\' export default { name: \'Workorder\', components: { OrderList }, data() { return { workorders: [], totalNum: 0, pagesize: 10, params: { page: 1, status: 2 } } }, created() { this.fetchData() }, methods: { fetchData() { getWorkOrderList(this.params).then( res => { this.workorders = res.results console.log(this.workorders) this.totalNum = res.count }) }, handleCurrentChange(val) { this.params.page = val this.fetchData() }, searchClick() { this.fetchData() } } } </script> <style lang=\'scss\' scoped> .workorder { padding: 10px; } </style>
(3)workorder/table.vue
<template> <div class="workorder-list"> <el-table :data="value" border stripe style="width: 100%"> <el-table-column type="expand"> <template slot-scope="props"> <span>工单详情:{{ props.row.order_contents }}</span> <br> <span>处理结果:{{ props.row.result_desc }}</span> </template> </el-table-column> <el-table-column label="工单类型" prop="type.name"/> <el-table-column label="工单标题" prop="title"/> <el-table-column label="申请人" prop="applicant[0].name"/> <el-table-column label="指派人" prop="assign_to[0].name"/> <el-table-column label="处理人" prop="final_processor[0].name"/> <el-table-column label="状态" prop="status.name"/> <el-table-column :formatter="dateFormat" label="申请时间" prop="apply_time"/> <el-table-column :formatter="dateFormat" label="完成时间" prop="complete_time"/> </el-table> </div> </template> <script> import moment from \'moment\' export default { name: \'OrderList\', props: { value: { type: Array, default: function() { return [] } } }, methods: { dateFormat: function(row, column) { var date = row[column.property] if (date === undefined) { return \'\' } return moment(date).format(\'YYYY-MM-DD HH:mm:ss\') } } } </script> <style lang=\'scss\'> .workorder-list {} </style>
效果图:
点击工单处理时报如错:
jango.core.exceptions.ValidationError: [\'’2020-07-21\t22:55:55‘ 必须为合法的日期时间格式,请使用 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] 格式。\']
解决:
注掉workorder/views.py: # data[\'complete_time\'] = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime(time.time()))
并打开workorder/serializers.py:
apply_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
complete_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
3.工单状态流转及步骤条实现
(1)workorder/list/form.vue
<template> <div class="workorder-form"> <el-form ref="form" :model="form" :rules="rules" label-width="100px" class="demo-form"> <el-form-item label="工单标题" prop="title"> <el-input v-model="form.title" readonly/> </el-form-item> <el-form-item label="工单内容" prop="order_contents"> <el-input v-model="form.order_contents" type="textarea" rows="8" readonly/> </el-form-item> <el-form-item label="处理结果" prop="result_desc"> <el-input v-model="form.result_desc"/> </el-form-item> <el-form-item> <div class="btn-wrapper"> <el-button size="small" @click="cancel">取消</el-button> <el-button size="small" type="primary" @click="submitForm">保存</el-button> </div> </el-form-item> </el-form> </div> </template> <script> export default { name: \'OrderForm\', props: { form: { // 接受父组件传递过来的值渲染表单 type: Object, default() { return { title: \'\', order_contents: \'\', result_desc: \'\' } } } }, data() { return { rules: { result_desc: [ { required: true, message: \'请输入处理结果\', trigger: \'blur\' } ] } } }, methods: { submitForm() { this.$refs.form.validate(valid => { if (!valid) { return } this.$emit(\'submit\', this.form) }) }, cancel() { this.$emit(\'cancel\') } } } </script> <style lang=\'scss\' scoped> .workorder-form { position: relative; display: block; .btn-wrapper{ text-align: right; } } </style>
(2)workorder/list/index.vue
... import OrderForm from \'./form\' .... export default { name: \'Workorder\', components: { OrderList, OrderForm },
效果如图:
点击工单处理时如下报错:[Vue warn]: Invalid prop: type check failed for prop “rows”. Expected Number, got String.
解决办法:
form.vue:中改成如下 <el-input :rows="8" v-model="form.order_contents" type="textarea" readonly/>
最终效果如下图:
4.工单申请
(1)workorder/apply/index.vue
<template> <div class="apply"> <el-form ref="form" :model="form" :rules="rules" label-width="180px"> <el-form-item label="工单类型:" prop="type"> <el-select v-model="form.type" placeholder="请选择工单类型" style="width: 60%;"> <el-option v-for="item in type_list" :key="item.index" :label="item.name" :value="item.id"/> </el-select> </el-form-item> <el-form-item label="工单标题:" prop="title"> <el-input v-model="form.title" style="width: 60%;"/> </el-form-item> <el-form-item label="工单内容:" prop="order_contents"> <el-input :rows="8" v-model="form.order_contents" type="textarea" style="width: 60%;"/> </el-form-item> <el-form-item label="指派给:" prop="assign_to"> <el-select v-model="form.assign_to" filterable placeholder="请选择工单处理人" style="width: 60%;"> <el-option v-for="item in sa_list" :key="item.index" :label="item.name" :value="item.id"/> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">Create</el-button> <el-button @click="onCancel">Cancel</el-button> </el-form-item> </el-form> </div> </template> <script> import { getGroupMemberList } from \'@/api/group\' import { createWorkOrder } from \'@/api/workorder/workorder\' export default { data() { return { form: { type: \'\', title: \'\', order_contents: \'\', assign_to: \'\' }, rules: { type: [ { required: true, message: \'请输入工单类型\', trigger: \'blur\' } ], title: [ { required: true, message: \'请输人工单标题\', trigger: \'blur\' } ], order_contents: [ { required: true, message: \'请输人工单内容\', trigger: \'blur\' } ], assign_to: [ { required: true, message: \'请输人工单指派人\', trigger: \'blur\' } ] }, sa_list: [], type_list: [{ \'id\': 0, \'name\': \'数据库\' }, { \'id\': 1, \'name\': \'计划任务\' }, { \'id\': 2, \'name\': \'配置文件\' }, { \'id\': 3, \'name\': \'其他\' }], state: 0 } }, watch: { state() { getGroupMemberList(6).then(res => { this.sa_list = res.members console.log(this.sa_list) }) } }, created() { this.state = 1 }, methods: { onSubmit() { this.$message(\'submit!\') this.$refs.form.validate((valid) => { if (!valid) { return } const params = Object.assign({}, this.form) console.log(params) createWorkOrder(params).then(res => { this.$message({ message: \'创建成功\', type: \'success\' }) this.$router.push({ path: \'/workorder/list\' }) }) }) }, onCancel() { this.$message({ message: \'cancel!\', type: \'warning\' }) } } } </script> <style scoped> .apply{ margin-top:2cm; } </style>
(2)src/router/index.js
import Vue from \'vue\' import Router from \'vue-router\' // in development-env not use lazy-loading, because lazy-loading too many pages will cause webpack hot update too slow. so only in production use lazy-loading; // detail: https://panjiachen.github.io/vue-element-admin-site/#/lazy-loading Vue.use(Router) /* Layout */ import Layout from \'../views/layout/Layout\' /** * hidden: true if `hidden:true` will not show in the sidebar(default is false) * alwaysShow: true if set true, will always show the root menu, whatever its child routes length * if not set alwaysShow, only more than one route under the children * it will becomes nested mode, otherwise not show the root menu * redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb * name:\'router-name\' the name is used by <keep-alive> (must set!!!) * meta : { title: \'title\' the name show in submenu and breadcrumb (recommend set) icon: \'svg-name\' the icon show in the sidebar, } **/ export const constantRouterMap = [ { path: \'/login\', component: () => import(\'@/views/login/index\'), hidden: true }, { path: \'/404\', component: () => import(\'@/views/404\'), hidden: true }, { path: \'/\', component: Layout, redirect: \'/dashboard\', name: \'Dashboard\', children: [{ path: \'dashboard\', component: () => import(\'@/views/dashboard/index\'), meta: { title: \'dashboard\', icon: \'example\' } }] }, { path: \'/users\', component: Layout, name: \'users\', meta: { title: \'用户管理\', icon: \'example\' }, children: [ { path: \'user\', name: \'user\', component: () => import(\'@/views/users/user\'), meta: { title: \'用户\' } }, { path: \'groups\', name: \'groups\', permission: \'resources.add_ip\', component: () => import(\'@/views/groups\'), meta: { title: \'用户组\' } } ] }, { path: \'/workorder\', component: Layout, name: \'workorder\', meta: { title: \'工单系统\', icon: \'form\' }, children: [ { path: \'apply\', name: \'工单申请\', component: () => import(\'@/views/workorder/apply/index\'), meta: { title: \'工单申请\', icon: \'form\' } }, { path: \'list\', name: \'申请列表\', component: () => import(\'@/views/workorder/list/index\'), meta: { title: \'申请列表\', icon: \'table\' } }, { path: \'history\', name: \'工单历史\', component: () => import(\'@/views/workorder/history/index\'), meta: { title: \'工单历史\', icon: \'table\' } } ] }, { path: \'*\', redirect: \'/404\', hidden: true } ] export default new Router({ mode: \'history\', scrollBehavior: () => ({ y: 0 }), routes: constantRouterMap })
效果如下图:但是却获取不到用户列表—no data
1