一.工单系统需求分析
二.工单系统后端
(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:devopsdatawebvueAdmin-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=" 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=" 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 22: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=" 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=" 60%;"/> </el-form-item> <el-form-item label="工单内容:" prop="order_contents"> <el-input :rows="8" v-model="form.order_contents" type="textarea" style=" 60%;"/> </el-form-item> <el-form-item label="指派给:" prop="assign_to"> <el-select v-model="form.assign_to" filterable placeholder="请选择工单处理人" style=" 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