feat:工作流 - 前端进度 70%

This commit is contained in:
weifs 2023-04-18 15:54:36 +08:00
parent c13aa8d7a4
commit 836d4f8209
5 changed files with 336 additions and 191 deletions

View File

@ -63,7 +63,7 @@ class WorkflowController extends AbstractController
*/
public function procdef__all()
{
User::auth('admin');
User::auth();
$data['name'] = Request::input('name');
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);

View File

@ -2,35 +2,35 @@
<div class="review-details">
<div class="review-details-box">
<h2 class="review-details-title">
<span>{{data.proc_def_name}}</span>
<span>{{datas.proc_def_name}}</span>
<Tag v-if="datas.state == 0" color="cyan">{{$L('待审批')}}</Tag>
<Tag v-if="datas.state == 1" color="cyan">{{$L('审批中')}}</Tag>
<Tag v-if="datas.state == 2" color="green">{{$L('已通过')}}</Tag>
<Tag v-if="datas.state == 3" color="red">{{$L('已拒绝')}}</Tag>
<Tag v-if="datas.state == 4" color="red">{{$L('已撤回')}}</Tag>
</h2>
<h3 class="review-details-subtitle"><Avatar :src="data.userimg" size="24"/><span>请假名字</span></h3>
<h3 class="review-details-subtitle"><span>{{$L('提交于')}} {{data.start_time}}</span></h3>
<h3 class="review-details-subtitle"><Avatar :src="datas.userimg" size="24"/><span>{{datas.start_user_name}}</span></h3>
<h3 class="review-details-subtitle"><span>{{$L('提交于')}} {{datas.start_time}}</span></h3>
<Divider/>
<div class="review-details-text">
<div class="review-details-text" v-if="(datas.proc_def_name || '').indexOf('班') == -1">
<h4>{{$L('假期类型')}}</h4>
<p>{{data.var?.type}}</p>
<p>{{datas.var?.type}}</p>
</div>
<div class="review-details-text">
<h4>{{$L('开始时间')}}</h4>
<p>{{data.var?.start_time}}</p>
<p>{{datas.var?.start_time}}</p>
</div>
<div class="review-details-text">
<h4>{{$L('结束时间')}}</h4>
<p>{{data.var?.end_time}}</p>
<p>{{datas.var?.end_time}}</p>
</div>
<div class="review-details-text">
<h4>{{ $L('时长') }}小时</h4>
<p>{{ $L('1天') }}</p>
<h4>{{ $L('时长') }}{{getTimeDifference(datas.var?.start_time,datas.var?.end_time)['unit']}}</h4>
<p>{{ getTimeDifference(datas.var?.start_time,datas.var?.end_time)['time'] }}</p>
</div>
<div class="review-details-text">
<h4>{{$L('请假事由')}}</h4>
<p>{{data.var?.description}}</p>
<p>{{datas.var?.description}}</p>
</div>
<Divider/>
<h3 class="review-details-subtitle">{{$L('审批记录')}}</h3>
@ -39,7 +39,7 @@
<TimelineItem v-for="(item,key) in datas.node_infos" :key="key" v-if="item.type == 'starter'" color="green">
<p class="timeline-title">{{$L('提交')}}</p>
<div style="display: flex;">
<Avatar :src="data.userimg" size="38"/>
<Avatar :src="data.userimg || datas.userimg" size="38"/>
<div style="margin-left: 10px;flex: 1;">
<p class="review-process-name">{{item.approver}}</p>
<p class="review-process-state">{{$L('已提交')}}</p>
@ -56,7 +56,7 @@
>
<p class="timeline-title">{{$L('审批')}}</p>
<div style="display: flex;">
<Avatar :src="item.node_user_list[0]?.userimg" size="38"/>
<Avatar :src="item.node_user_list && item.node_user_list[0]?.userimg" size="38"/>
<div style="margin-left: 10px;flex: 1;">
<p class="review-process-name">{{item.approver}}</p>
<p class="review-process-state" style="color: #6d6d6d;" v-if="!item.identitylink">待审批</p>
@ -108,6 +108,7 @@
</Timeline>
</div>
<div class="review-operation" v-if="datas.state<=1">
<div style="flex: 1;"></div>
<Button type="success" v-if="(datas.candidate || '').split(',').indexOf(userId + '') != -1" @click="approve(1)">{{$L('同意')}}</Button>
<Button type="error" v-if="(datas.candidate || '').split(',').indexOf(userId + '') != -1" @click="approve(2)">{{$L('拒绝')}}</Button>
<Button type="warning" v-if="userId == datas.start_user_id" @click="revocation">{{$L('撤销')}}</Button>
@ -144,6 +145,12 @@ export default {
deep: true
},
},
mounted() {
if(this.$route.query.id){
this.data.id = this.$route.query.id;
this.getInfo()
}
},
methods:{
//
getTimeAgo(time,type) {
@ -154,13 +161,34 @@ export default {
} else if (timeDiff < 3600) {
const minutes = Math.floor(timeDiff / 60);
return type == 2 ? `${minutes}${this.$L('分钟')}` : `${minutes} ${this.$L('分钟前')}`;
} else {
} else if(timeDiff < 3600 * 24) {
const hours = Math.floor(timeDiff / 3600);
return type == 2 ? `${hours}${this.$L('小时')}` : `${hours} ${this.$L('小时前')}`;
} else {
const days = Math.floor(timeDiff / 3600 / 24);
return type == 2 ? `${days}${this.$L('天')}` : `${days} ${this.$L('天')}`;
}
},
//
getTimeDifference(startTime,endTime) {
const currentTime = new Date(endTime);
const timeDiff = (currentTime - new Date(startTime)) / 1000; // convert to seconds
if (timeDiff < 60) {
return {time:timeDiff,unit:this.$L('秒')};
} else if (timeDiff < 3600) {
const minutes = Math.floor(timeDiff / 60);
return {time:minutes,unit:this.$L('分钟')};
} else if(timeDiff < 3600 * 24) {
const hours = Math.floor(timeDiff / 3600);
return {time:hours,unit:this.$L('小时')};
} else {
const days = Math.floor(timeDiff / 3600 / 24);
return {time:days,unit:this.$L('天')};
}
},
//
getInfo(){
this.datas = this.data
this.$store.dispatch("call", {
method: 'get',
url: 'workflow/process/detail',
@ -197,7 +225,7 @@ export default {
this.$store.dispatch("call", {
url: 'workflow/task/complete',
data: {
task_id: this.data.task_id,
task_id: this.datas.task_id,
pass: type == 1,
comment: desc,
}
@ -221,8 +249,8 @@ export default {
this.$store.dispatch("call", {
url: 'workflow/task/withdraw',
data: {
task_id: this.data.task_id,
proc_inst_id: this.data.id,
task_id: this.datas.task_id,
proc_inst_id: this.datas.id,
}
}).then(({msg}) => {
resolve();

View File

@ -7,15 +7,14 @@
<div class="review-nav">
<h1>{{$L('审批中心')}}</h1>
</div>
<Button :loading="loadIng > 0" type="primary" @click="" style="margin-right:10px;">{{$L('发起请假')}}</Button>
<Button :loading="loadIng > 0" type="primary" @click="">{{$L('加班申请')}}</Button>
<Button v-for="item in procdefList" :loading="loadIng > 0" type="primary" @click="initiate(item)" style="margin-right:10px;">{{item.name}}</Button>
</div>
<Tabs :value="tabsValue" @on-click="tabsClick" style="margin: 0 20px;height: 100%;">
<TabPane :label="$L('待办') + (backlogList.length > 0 ? ('('+backlogList.length+')') : '')" name="backlog" style="height: 100%;">
<TabPane :label="$L('待办') + (backlogTotal > 0 ? ('('+backlogTotal+')') : '')" name="backlog" style="height: 100%;">
<div class="review-main-search">
<div style="display: flex;gap: 10px;">
<Select v-model="approvalType" style="width: 150px;">
<Select v-model="approvalType" @on-change="tabsClick('')" style="width: 150px;">
<Option v-for="item in approvalList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</div>
@ -26,18 +25,18 @@
<div class="review-main-list">
<div @click.stop="clickList(item,key)" v-for="(item,key) in backlogList">
<list :class="{ 'review-list-active': item._active }" :data="item"></list>
</div>
</div>
</div>
</div>
<div class="review-main-right">
<listDetails :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
<listDetails v-if="!detailsShow" :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
</div>
</div>
</TabPane>
<TabPane label="已办" name="done">
<div class="review-main-search">
<div style="display: flex;gap: 10px;">
<Select v-model="approvalType" style="width: 150px;">
<Select v-model="approvalType" @on-change="tabsClick('')" style="width: 150px;">
<Option v-for="item in approvalList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</div>
@ -46,13 +45,13 @@
<div v-else class="review-mains">
<div class="review-main-left">
<div class="review-main-list">
<div @click.stop="clickList(item,key)" v-for="(item,key) in doneList">
<div @click.stop="clickList(item,key)" v-for="(item,key) in doneList" >
<list :class="{ 'review-list-active': item._active }" :data="item"></list>
</div>
</div>
</div>
<div class="review-main-right">
<listDetails :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
<listDetails v-if="!detailsShow" :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
</div>
</div>
</TabPane>
@ -60,7 +59,7 @@
<div class="review-main-search">
<div class="review-main-search">
<div style="display: flex;gap: 10px;">
<Select v-model="approvalType" style="width: 150px;">
<Select v-model="approvalType" @on-change="tabsClick('')" style="width: 150px;">
<Option v-for="item in approvalList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</div>
@ -76,17 +75,17 @@
</div>
</div>
<div class="review-main-right">
<listDetails :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
<listDetails v-if="!detailsShow" :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
</div>
</div>
</TabPane>
<TabPane :label="$L('已发起')" name="initiated">
<div class="review-main-search">
<div style="display: flex;gap: 10px;">
<Select v-model="approvalType" style="width: 150px;">
<Select v-model="approvalType" @on-change="tabsClick('')" style="width: 150px;">
<Option v-for="item in approvalList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
<Select v-model="searchState" style="width: 150px;">
<Select v-model="searchState" @on-change="tabsClick('')" style="width: 150px;">
<Option v-for="item in searchStateList" :value="item.value" :key="item.value">{{ item.label }}</Option>
</Select>
</div>
@ -101,33 +100,71 @@
</div>
</div>
<div class="review-main-right">
<listDetails :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
<listDetails v-if="!detailsShow" :data="details" @approve="tabsClick" @revocation="tabsClick"></listDetails>
</div>
</div>
</TabPane>
</Tabs>
</div>
<!--详情-->
<DrawerOverlay v-model="detailsShow" placement="right" :size="600">
<listDetails v-if="detailsShow" :data="details" @approve="tabsClick" @revocation="tabsClick" style="height: 100%;border-radius: 10px;"></listDetails>
</DrawerOverlay>
<!--发起-->
<Modal v-model="addShow" :title="$L(addTitle)" :mask-closable="false">
<Form ref="initiateRef" :model="addData" :rules="addRule" label-width="auto" @submit.native.prevent>
<FormItem v-if="(addTitle || '').indexOf('班') == -1" prop="type" :label="$L('假期类型')">
<Select v-model="addData.type" :placeholder="$L('请选择')">
<Option v-for="(item, index) in selectTypes" :value="item" :key="index">{{ item }}</Option>
</Select>
</FormItem>
<FormItem prop="startTime" :label="$L('开始时间')">
<DatePicker type="datetime" format="yyyy-MM-dd HH:mm"
v-model="addData.startTime"
:value="addData.startTime"
@on-change="(e)=>{ addData.startTime = e }"
:placeholder="$L('请选择开始时间')" style="width: 100%"
></DatePicker>
</FormItem>
<FormItem prop="endTime" :label="$L('结束时间')">
<DatePicker type="datetime" format="yyyy-MM-dd HH:mm"
v-model="addData.endTime"
@on-change="(e)=>{ addData.endTime = e }"
:placeholder="$L('请选择结束时间')"
style="width: 100%"
></DatePicker>
</FormItem>
<FormItem prop="description" :label="$L('事由')">
<Input type="textarea" v-model="addData.description"></Input>
</FormItem>
</Form>
<div slot="footer" class="adaption">
<Button type="default" @click="addShow=false">{{$L('取消')}}</Button>
<Button type="primary" :loading="loadIng > 0" @click="onInitiate">{{$L('确认')}}</Button>
</div>
</Modal>
</div>
</template>
<script>
import list from "./list.vue";
import listDetails from "./details.vue";
import DrawerOverlay from "../../../components/DrawerOverlay";
export default {
components:{list,listDetails},
components:{list,listDetails,DrawerOverlay},
name: "review",
data(){
return{
timeChose:this.$L('所有时间'),
list: [],
procdefList: [],
page: 1,
pageSize: 250,
total: 0,
noText: '',
loadIng:false,
tabsValue:"",
//
@ -135,7 +172,7 @@ export default {
approvalList:[
{value:"all",label:"全部审批"},
{value:"请假",label:"请假"},
{value:"加班",label:"加班"},
{value:"加班申请",label:"加班申请"},
],
searchState:"all",
searchStateList:[
@ -147,45 +184,48 @@ export default {
{value:4,label:"已撤回"}
],
//
backlogTotal:0,
backlogList: [],
doneList:[],
notifyList:[],
initiatedList: [],
details:{}
//
details:{},
detailsShow:false,
//
addTitle:'',
addShow:false,
addData: {
type: '',
startTime:"",
endTime:"",
},
addRule: {
type: { type: 'string',required: true, message: this.$L('请选择假期类型!'), trigger: 'change' },
startTime: { type: 'string',required: true, message: this.$L('请选择开始时间!'), trigger: 'change' },
endTime:{ type: 'string',required: true, message: this.$L('请选择结束时间!'), trigger: 'change' },
description:{ type: 'string',required: true, message: this.$L('请选择结束时间!'), trigger: 'change' },
},
selectTypes:["年假","事假","病假","调休","产假","陪产假","婚假","例假","丧假","哺乳假"]
}
},
mounted() {
this.tabsValue = "initiated"
this.tabsClick()
this.getProcdef()
this.getBacklogList()
},
methods:{
changeTime(e){
switch (e) {
case 'all':
this.timeChose = this.$L('所有时间');
return;
case '24':
this.timeChose = this.$L('最近24小时');
return;
case '7':
this.timeChose = this.$L('最近7天');
return;
case '30':
this.timeChose = this.$L('最近30天');
return;
case 'customize':
this.timeChose = this.$L('自定义时间');
return;
}
},
// tab
tabsClick(val){
this.tabsValue = val || this.tabsValue
// if(this.tabsValue == 'backlog'){
if(val!=""){
this.approvalType = this.searchState = "all"
}
if(this.tabsValue == 'backlog'){
this.getBacklogList();
// }
}
if(this.tabsValue == 'done'){
this.getDoneList();
}
@ -204,7 +244,32 @@ export default {
this.notifyList.map(h=>{ h._active = false; })
this.initiatedList.map(h=>{ h._active = false; })
item._active = true;
this.details = item
//
if( window.innerWidth < 425 ){
this.goForward({name: 'manage-review-details', query: { id: item.id } });
return;
}
if( window.innerWidth < 1010 ){
this.detailsShow = true;
}
this.details = {}
this.$nextTick(()=>{
this.details = item
})
},
//
getProcdef(){
this.$store.dispatch("call", {
url: 'workflow/procdef/all',
method: 'post',
}).then(({data}) => {
this.procdefList = data.rows || [];
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
},
//
@ -215,12 +280,16 @@ export default {
data: {
page:this.page,
page_size: this.pageSize,
proc_def_name: this.approvalType == 'all' ? '' : this.approvalType,
}
}).then(({data}) => {
this.backlogList = data.rows.map((h,index)=>{
h._active = index == 0;
return h;
})
if(this.approvalType == 'all'){
this.backlogTotal = this.backlogList.length
}
if(this.tabsValue == 'backlog'){
this.$nextTick(()=>{
this.details = this.backlogList[0] || {}
@ -241,6 +310,7 @@ export default {
data: {
page:this.page,
page_size: this.pageSize,
proc_def_name: this.approvalType == 'all' ? '' : this.approvalType,
}
}).then(({data}) => {
this.doneList = data.rows.map((h,index)=>{
@ -267,6 +337,7 @@ export default {
data: {
page:this.page,
page_size: this.pageSize,
proc_def_name: this.approvalType == 'all' ? '' : this.approvalType,
}
}).then(({data}) => {
this.notifyList = data.rows.map((h,index)=>{
@ -288,11 +359,13 @@ export default {
//
getInitiatedList(){
this.$store.dispatch("call", {
method: 'get',
url: 'workflow/process/startByMyself',
method: 'post',
url: 'workflow/process/startByMyselfAll',
data: {
page:this.page,
page: this.page,
page_size: this.pageSize,
proc_def_name: this.approvalType == 'all' ? '' : this.approvalType,
state: this.searchState == 'all' ? '' : this.searchState
}
}).then(({data}) => {
this.initiatedList = data.rows.map((h,index)=>{
@ -309,6 +382,39 @@ export default {
}).finally(_ => {
this.loadIng--;
});
},
//
initiate(item){
this.addTitle = item.name;
this.addShow = true;
},
//
onInitiate(){
this.$refs.initiateRef.validate((valid) => {
if (valid) {
this.loadIng++;
this.$store.dispatch("call", {
url: 'workflow/process/start',
data: {
proc_name:this.addTitle,
department_id:1,
var: JSON.stringify(this.addData)
},
method: 'post',
}).then(({data, msg}) => {
$A.messageSuccess(msg);
this.addShow = false;
this.$refs.initiateRef.resetFields();
this.tabsClick();
}).catch(({msg}) => {
$A.modalError(msg);
}).finally(_ => {
this.loadIng--;
});
}
});
}

View File

@ -34,6 +34,11 @@ export default [
path: 'review',
component: () => import('./pages/manage/review/index.vue'),
},
{
name: 'manage-review-details',
path: 'review/details',
component: () => import('./pages/manage/review/details.vue'),
},
{
name: 'manage-setting',
path: 'setting',

View File

@ -51,13 +51,15 @@
display: flex;
flex-direction: column;
flex: 0 0 auto;
max-width: 360px;
width: 100%;
position: absolute;
left: 0;
top: 0;
bottom: 12px;
max-width: 360px;
width: 100%;
@media (max-width: 1010px) {
max-width: 100%;
}
.review-main-list{
display: flex;
flex-direction: column;
@ -121,132 +123,136 @@
flex: 1 1 auto;
display: flex;
margin: 0 0 12px 12px;
.review-details{
flex: 1 1 auto;
display: flex;
flex-direction: column;
border-radius: 8px;
border: 1px solid #eeeeee;
.review-details-box{
flex: 1 1 auto;
padding: 24px;
overflow-y: scroll;
.review-details-title{
display: flex;
align-items: center;
.ivu-tag{
margin-left: 8px;
}
}
.review-details-subtitle{
margin-top: 8px;
display: flex;
.ivu-avatar{
margin-right: 8px;
}
> span{
font-size: 14px;
}
}
.timeline-title{
font-weight: bold;
padding-bottom: 10px;
}
.review-process-name{
margin-bottom: 4px;
}
.review-process-state{
font-size: 12px;
color: #19be6b;
}
.review-process-right{
text-align: right;
}
.review-details-text{
margin-bottom: 12px;
> h4{
color: #999;
}
> p{
font-size: 14px;
margin-top: 2px;
font-weight: 500;
}
}
.review-details-text:nth-last-child(1){
margin-bottom: 0;
}
.review-details-process{
display: flex;
flex-direction: column;
margin-top: 16px;
position: relative;
.review-details-line{
position: absolute;
left: 23px;
top: 5px;
bottom: 5px;
width: 3px;
background: #19be6b;
z-index: 1;
}
.review-process{
display: flex;
justify-content: space-between;
position: relative;
z-index: 2;
margin-bottom: 32px;
.review-process-left{
display: flex;
align-items: center;
.review-process-text{
display: flex;
flex-direction: column;
margin-left: 8px;
}
}
}
.review-process:nth-last-child(1){
margin-bottom: 0;
}
}
.review-copy{
margin-top: 8px;
display: flex;
.review-copy-member{
display: flex;
align-items: center;
background: #F4F4F5;
padding:2px 8px;
border-radius: 20px;
.ivu-avatar{
margin-right: 4px;
}
}
}
}
.review-details-box::-webkit-scrollbar {
display: none;
}
.review-operation{
flex: 0 0 auto;
height: 60px;
padding: 0 24px;
border-top: 1px solid #F4F4F5;
display: flex;
align-items: center;
gap: 10px;
}
@media (max-width: 1010px) {
display: none;
}
}
}
}
}
.review-details{
flex: 1 1 auto;
display: flex;
flex-direction: column;
border-radius: 8px;
border: 1px solid #eeeeee;
background: #fff;
.review-details-box{
flex: 1 1 auto;
padding: 24px;
overflow-y: scroll;
.review-details-title{
display: flex;
align-items: center;
.ivu-tag{
margin-left: 8px;
}
}
.review-details-subtitle{
margin-top: 8px;
display: flex;
.ivu-avatar{
margin-right: 8px;
}
> span{
font-size: 14px;
}
}
.timeline-title{
font-weight: bold;
padding-bottom: 10px;
}
// .review-process-name{
// margin-bottom: 4px;
// }
.review-process-state{
font-size: 12px;
color: #19be6b;
}
.review-process-right{
text-align: right;
}
.review-details-text{
margin-bottom: 12px;
> h4{
color: #999;
}
> p{
font-size: 14px;
margin-top: 2px;
font-weight: 500;
}
}
.review-details-text:nth-last-child(1){
margin-bottom: 0;
}
.review-details-process{
display: flex;
flex-direction: column;
margin-top: 16px;
position: relative;
.review-details-line{
position: absolute;
left: 23px;
top: 5px;
bottom: 5px;
width: 3px;
background: #19be6b;
z-index: 1;
}
.review-process{
display: flex;
justify-content: space-between;
position: relative;
z-index: 2;
margin-bottom: 32px;
.review-process-left{
display: flex;
align-items: center;
.review-process-text{
display: flex;
flex-direction: column;
margin-left: 8px;
}
}
}
.review-process:nth-last-child(1){
margin-bottom: 0;
}
}
.review-copy{
margin-top: 8px;
display: flex;
.review-copy-member{
display: flex;
align-items: center;
background: #F4F4F5;
padding:2px 8px;
border-radius: 20px;
.ivu-avatar{
margin-right: 4px;
}
}
}
}
.review-details-box::-webkit-scrollbar {
display: none;
}
.review-operation{
flex: 0 0 auto;
height: 55px;
padding: 0 24px;
border-top: 1px solid #F4F4F5;
display: flex;
align-items: center;
gap: 10px;
}
}