12 changed files with 519 additions and 70 deletions
@ -0,0 +1,442 @@ |
|||||
|
<!-- |
||||
|
@ 作者: 秦东 |
||||
|
@ 时间: 2023-11-17 16:43:55 |
||||
|
@ 备注: 工作流步进图 |
||||
|
--> |
||||
|
<script lang='ts' setup> |
||||
|
import { PropType, reactive, ref, toRefs, computed, watch, onMounted } from "vue"; |
||||
|
import SvgIcon from "@/components/SvgIcon/index.vue"; |
||||
|
import OrgUserPage from "@/views/public/orguser/orguser.vue" |
||||
|
import OrgAllUserPage from "@/views/public/orguser/orgalluser.vue" |
||||
|
|
||||
|
import squareUrlOne from "@/assets/images/1.png" |
||||
|
import squareUrlTwo from "@/assets/images/2.png" |
||||
|
|
||||
|
interface OperatorItem { |
||||
|
id: string | number; |
||||
|
icon?: string; |
||||
|
iconbase64?: string; |
||||
|
departmentname?: string; |
||||
|
postname?: string; |
||||
|
name?: string; |
||||
|
log?: Array<{ |
||||
|
state: number; |
||||
|
cause?: string; |
||||
|
time?: string; |
||||
|
}>; |
||||
|
} |
||||
|
|
||||
|
interface FlowItem { |
||||
|
step: number; |
||||
|
type: number; |
||||
|
nodeName?: string; |
||||
|
operator?: OperatorItem[]; |
||||
|
customNode?: string; |
||||
|
runscope?: number; |
||||
|
pendpers?: any[]; |
||||
|
runtype?: number; |
||||
|
} |
||||
|
|
||||
|
const state = reactive({ |
||||
|
circleUrl:squareUrlTwo, |
||||
|
squareUrl: squareUrlOne, |
||||
|
sizeList: ['small', '', 'large'] as const, |
||||
|
}) |
||||
|
|
||||
|
const flowLoading = ref(false) |
||||
|
const { circleUrl, squareUrl, sizeList } = toRefs(state) |
||||
|
const openOrClose = ref(false) |
||||
|
const openclosebox = ref(false) |
||||
|
const props = defineProps({ |
||||
|
flowMap: { |
||||
|
type: Array as PropType<FlowItem[]>, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
nodeKey:{ |
||||
|
type:String, |
||||
|
default:"" |
||||
|
}, |
||||
|
currentProgress:{ |
||||
|
type:Number, |
||||
|
default:0 |
||||
|
} |
||||
|
, |
||||
|
nextStep:{ |
||||
|
type:Number, |
||||
|
default:0 |
||||
|
} |
||||
|
}) |
||||
|
const ifSendFlow = ref(false) |
||||
|
const presetPersonnel = ref<any>([]); //预设选择人 |
||||
|
const selectedPeople = ref<any>([]); //已选择人 |
||||
|
const flowMaps = ref<FlowItem[]>(); |
||||
|
const emits = defineEmits(["update:flowMap"]); |
||||
|
const flowList = computed({ |
||||
|
get: () => props.flowMap, |
||||
|
set: (val: FlowItem[]) => { |
||||
|
emits("update:flowMap", val); |
||||
|
}, |
||||
|
}); |
||||
|
watch(()=>props.flowMap,(val: FlowItem[])=>{ |
||||
|
|
||||
|
emits("update:flowMap", val); |
||||
|
}) |
||||
|
onMounted(()=>{ |
||||
|
|
||||
|
}) |
||||
|
//判断是否有增加人员按钮 |
||||
|
const judgeAddUser = (val: FlowItem):boolean =>{ |
||||
|
// console.log("val----->",val) |
||||
|
if(val.customNode == "beginnode"){ |
||||
|
val.runscope = val.runscope!=0?val.runscope:1 |
||||
|
return true |
||||
|
} |
||||
|
switch(val.runtype){ |
||||
|
case 1: |
||||
|
val.runscope = val.runscope!=0?val.runscope:1 |
||||
|
return true |
||||
|
break; |
||||
|
case 3: |
||||
|
if(val.customNode == props.nodeKey){ |
||||
|
val.runscope = val.runscope!=0?val.runscope:1 |
||||
|
return true |
||||
|
} |
||||
|
break; |
||||
|
case 4: |
||||
|
if(val.runscope==1){ |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
// if(!val.operator){ |
||||
|
// val.runscope = val.runscope!=0?val.runscope:1 |
||||
|
// return true |
||||
|
// }else if(val.operator.length < 1){ |
||||
|
// val.runscope = val.runscope!=0?val.runscope:1 |
||||
|
// return true |
||||
|
// } |
||||
|
if(val.runscope != 0){ |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
let zhiXingStep = 1; |
||||
|
//添加操作人 |
||||
|
const addPeople = (val: FlowItem) =>{ |
||||
|
zhiXingStep = val.step |
||||
|
presetPersonnel.value = val.pendpers |
||||
|
selectedPeople.value = val.operator |
||||
|
if(val.runscope == 1){ |
||||
|
openclosebox.value = true |
||||
|
}else{ |
||||
|
openOrClose.value = true |
||||
|
} |
||||
|
|
||||
|
// console.log("PresetPersonnel.value--------1-------->",val) |
||||
|
// console.log("PresetPersonnel.value--------2-------->",val.pendpers) |
||||
|
// console.log("PresetPersonnel.value--------3-------->",val.operator) |
||||
|
// console.log("PresetPersonnel.value--------4-------->",selectedPeople.value) |
||||
|
} |
||||
|
//更新节点操作人 |
||||
|
const updateNode = (val: any) =>{ |
||||
|
// console.log("P更新节点操作人",zhiXingStep,val) |
||||
|
flowList.value.forEach((item: FlowItem) =>{ |
||||
|
if(item.step == zhiXingStep){ |
||||
|
// console.log("P更新节点操作人---1-->",item.step , zhiXingStep) |
||||
|
item.operator = val |
||||
|
} |
||||
|
}) |
||||
|
// console.log("P更新节点操作人--2--->",flowList) |
||||
|
} |
||||
|
/** |
||||
|
@ 作者: 秦东 |
||||
|
@ 时间: 2026-01-29 16:37:56 |
||||
|
@ 功能: 判断审批节点样式 |
||||
|
*/ |
||||
|
const judgeNodeClass = (val:number,stepVal:number):string => { |
||||
|
// console.log("判断审批节点样式",val,stepVal) |
||||
|
switch(val){ |
||||
|
case 1: |
||||
|
if(props.currentProgress == stepVal){ |
||||
|
return "in-progress" |
||||
|
}else{ |
||||
|
return 'completed' |
||||
|
} |
||||
|
case 2: |
||||
|
return 'pending' |
||||
|
case 3: |
||||
|
// return 'in-progress' |
||||
|
if(props.currentProgress == stepVal){ |
||||
|
return "in-progress" |
||||
|
}else{ |
||||
|
return 'in-executor' |
||||
|
} |
||||
|
default: |
||||
|
if(props.currentProgress == stepVal){ |
||||
|
return "in-progress" |
||||
|
}else{ |
||||
|
return 'completed' |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
return 'completed' |
||||
|
} |
||||
|
</script> |
||||
|
<template> |
||||
|
<el-card shadow="always"> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<div class="svgBox"><SvgIcon icon-class="liuChengBiaoDan" size="25" /></div> |
||||
|
审批流程 |
||||
|
</div> |
||||
|
</template> |
||||
|
<el-scrollbar v-loading="flowLoading" element-loading-text="Loading..." element-loading-background="rgba(122, 122, 122, 0.8)" :class="ifSendFlow?'flowBody':'flowBodyNofoot'"> |
||||
|
<div class="approval-steps"> |
||||
|
<div v-for="item in flowList" :key="item.step" :class="['step', judgeNodeClass(item.type,item.step)]"> |
||||
|
<div class="step-icon"> |
||||
|
<SvgIcon v-if="item.type==0" icon-class="faqiren" size="25" /> |
||||
|
<SvgIcon v-if="item.type==1" icon-class="spr" size="25" /> |
||||
|
<SvgIcon v-if="item.type==2" icon-class="csr" size="25" /> |
||||
|
<SvgIcon v-if="item.type==3" icon-class="zxr" size="25" /> |
||||
|
<!--SvgIcon v-else icon-class="shenpi" size="25" /--> |
||||
|
</div> |
||||
|
<div class="step-content"> |
||||
|
<h3 class="step-title">{{item.nodeName}}</h3><div v-for="items in item.operator" :key="items.id" class="flowLogBox"> |
||||
|
|
||||
|
<div > |
||||
|
<el-avatar v-if="items.icon!=''" shape="square" fit="cover" :src="items.icon" style="width: 40px;" class="avatarBox" /> |
||||
|
<el-avatar v-else-if="items.iconbase64!=''" shape="square" fit="cover" :src="items.iconbase64" style="width: 40px;" class="avatarBox" /> |
||||
|
<el-avatar v-else shape="square" fit="cover" :src="squareUrl" style="width: 40px;" class="avatarBox" /> |
||||
|
</div> |
||||
|
<div> |
||||
|
<div> |
||||
|
<el-text size="small">{{ items.departmentname }}</el-text> |
||||
|
<el-text size="small"><span v-if="items.departmentname"> - </span>{{ items.postname }}</el-text> |
||||
|
<el-text size="small"><span v-if="items.departmentname||items.postname"> - </span>{{ items.name }}</el-text> |
||||
|
</div> |
||||
|
<div v-for="(logItem,logIndex) in items.log" :key="logIndex" > |
||||
|
<div v-if="logItem.state==2" class="step-info"> |
||||
|
<el-text v-if="logItem.cause" type="success" size="small">{{logItem.cause}}</el-text> |
||||
|
<el-text v-else type="success" size="small" >已同意</el-text> |
||||
|
</div> |
||||
|
<div v-if="logItem.state==3" class="step-info"> |
||||
|
<el-text v-if="logItem.cause" type="danger" size="small">{{logItem.cause}}</el-text> |
||||
|
<el-text v-else type="danger" size="small" >已驳回</el-text> |
||||
|
</div> |
||||
|
<div v-else class="step-info"> |
||||
|
<el-text v-if="logItem.cause" size="small">{{logItem.cause}}</el-text> |
||||
|
<el-text v-else size="small" >未操作</el-text> |
||||
|
</div> |
||||
|
<div class="step-time">{{ logItem.time }}</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div v-if="judgeAddUser(item)" class="addUser" @click="addPeople(item)"> |
||||
|
<svg-icon icon-class="addxuxian" size="50" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-scrollbar> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</el-card> |
||||
|
|
||||
|
|
||||
|
<OrgUserPage v-if="openOrClose" v-model:openclose="openOrClose" :preset-personnel="presetPersonnel" :selected-people="selectedPeople" @update-node="updateNode" /> |
||||
|
<OrgAllUserPage v-if="openclosebox" v-model:openclosebox="openclosebox" :selected-people="selectedPeople" @update-node="updateNode" /> |
||||
|
|
||||
|
</template> |
||||
|
<style lang='scss' scoped> |
||||
|
.card-header{ |
||||
|
font-size: 1.4rem; |
||||
|
font-weight: 700; |
||||
|
color: #0020C2; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
.flowBody{ |
||||
|
padding: 10px 15px; |
||||
|
height: calc(100vh - 225px); |
||||
|
} |
||||
|
.flowBodyNofoot{ |
||||
|
padding: 10px 15px; |
||||
|
height: calc(100vh - 105px); |
||||
|
} |
||||
|
.svgBox{ |
||||
|
background: linear-gradient(135deg, #0020C2, #4d6cff); |
||||
|
color: white; |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
font-size: 1.2rem; |
||||
|
} |
||||
|
.bootemWorkFlow{ |
||||
|
width: 100%; |
||||
|
text-align: center; |
||||
|
.bootemWorkFlowBut{ |
||||
|
width: 100%; |
||||
|
padding: 10px 0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
padding: 14px 16px; |
||||
|
border-radius: 10px; |
||||
|
font-weight: 600; |
||||
|
font-size: 1rem; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
border: none; |
||||
|
} |
||||
|
.btn:hover { |
||||
|
transform: translateY(-2px); |
||||
|
box-shadow: 0 5px 15px rgba(0, 32, 194, 0.15); |
||||
|
} |
||||
|
.approve-btn { |
||||
|
background: linear-gradient(135deg, #00cc66, #33dd88); |
||||
|
color: white; |
||||
|
} |
||||
|
.reject-btn { |
||||
|
background: linear-gradient(135deg, #ff3366, #ff5588); |
||||
|
color: white; |
||||
|
} |
||||
|
.approval-steps { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
.step { |
||||
|
display: flex; |
||||
|
margin-bottom: 25px; |
||||
|
position: relative; |
||||
|
margin-left: 7px; |
||||
|
margin-top: 7px; |
||||
|
} |
||||
|
|
||||
|
.step:not(:last-child)::after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
left: 20px; |
||||
|
top: 40px; |
||||
|
width: 2px; |
||||
|
height: calc(100% + 5px); |
||||
|
background: linear-gradient(to bottom, rgba(0, 32, 194, 0.2), rgba(0, 32, 194, 0.05)); |
||||
|
} |
||||
|
|
||||
|
.step-icon { |
||||
|
width: 40px; |
||||
|
height: 40px; |
||||
|
border-radius: 50%; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
margin-right: 5px; |
||||
|
z-index: 1; |
||||
|
flex-shrink: 0; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
.step.completed .step-icon { |
||||
|
background: linear-gradient(135deg, #0020C2, #4d6cff); |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.step.in-progress .step-icon { |
||||
|
background: linear-gradient(135deg, #ffcc00, #ffdd55); |
||||
|
color: #333; |
||||
|
animation: pulse 2s infinite; |
||||
|
} |
||||
|
|
||||
|
.step.in-executor .step-icon { |
||||
|
background: linear-gradient(135deg, #67C23A, #95f764); |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.step.pending .step-icon { |
||||
|
background: #f0f4ff; |
||||
|
color: #a0a6c9; |
||||
|
} |
||||
|
|
||||
|
.step-content { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.step-title { |
||||
|
font-weight: 600; |
||||
|
margin-bottom: 5px; |
||||
|
color: #0020C2; |
||||
|
} |
||||
|
|
||||
|
.step.completed .step-title { |
||||
|
color: #4d6cff; |
||||
|
} |
||||
|
|
||||
|
.step.in-progress .step-title { |
||||
|
color: #ff9900; |
||||
|
} |
||||
|
.step.in-executor .step-title { |
||||
|
color: #67C23A; |
||||
|
} |
||||
|
|
||||
|
.step.pending .step-title { |
||||
|
color: #a0a6c9; |
||||
|
} |
||||
|
|
||||
|
.step-info { |
||||
|
font-size: 0.9rem; |
||||
|
color: #666; |
||||
|
margin-top: 0px; |
||||
|
} |
||||
|
|
||||
|
.step-time { |
||||
|
font-size: 0.8rem; |
||||
|
color: #888; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
/* 响应式设计 */ |
||||
|
@media (max-width: 1200px) { |
||||
|
.container { |
||||
|
grid-template-columns: 1fr; |
||||
|
grid-template-rows: auto auto auto; |
||||
|
height: auto; |
||||
|
gap: 15px; |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
min-height: 400px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes pulse { |
||||
|
0% { box-shadow: 0 0 0 0 rgba(255, 204, 0, 0.7); } |
||||
|
70% { box-shadow: 0 0 0 10px rgba(255, 204, 0, 0); } |
||||
|
100% { box-shadow: 0 0 0 0 rgba(255, 204, 0, 0); } |
||||
|
} |
||||
|
.flowLogBox{ |
||||
|
display: grid; |
||||
|
grid-template-columns: 45px 1fr; |
||||
|
} |
||||
|
.addUser{ |
||||
|
display:block; |
||||
|
cursor:pointer; |
||||
|
} |
||||
|
.avatarBox{ |
||||
|
border: 1px solid rgba(255, 255, 255, 0.5); |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue