增强办公,新增审批,缺少电子签章功能

This commit is contained in:
2026-04-20 15:56:29 +08:00
parent a026ef7a54
commit cd71a3a457
18 changed files with 1791 additions and 537 deletions

View File

@@ -1,13 +1,58 @@
<template>
<view class="approval-detail-page">
<!-- 动态渲染不同类型的审批详情组件 -->
<view class="flow-summary-card" v-if="detailData">
<view class="summary-head">
<text class="summary-title">审批信息</text>
<text class="status-tag" :class="statusClass(detailData.flowStatus)">{{ statusText(detailData.flowStatus) }}</text>
</view>
<view class="summary-grid">
<view class="summary-item">
<text class="label">是否通过</text>
<text class="value">{{ detailData.approved ? '已通过' : (detailData.flowStatus === 'rejected' ? '已驳回' : '待审批') }}</text>
</view>
<view class="summary-item">
<text class="label">当前状态</text>
<text class="value">{{ statusText(detailData.flowStatus) }}</text>
</view>
<view class="summary-item">
<text class="label">当前节点</text>
<text class="value">{{ detailData.currentNodeName || detailData.currentNodeId || '-' }}</text>
</view>
<view class="summary-item">
<text class="label">当前审批人</text>
<text class="value">{{ assigneeText }}</text>
</view>
</view>
</view>
<view class="flow-history-card" v-if="detailData && detailData.actionTimeline && detailData.actionTimeline.length">
<view class="card-title">审批流程</view>
<view class="timeline-item" v-for="(item, index) in detailData.actionTimeline" :key="item.actionId || index">
<view class="timeline-dot" :class="item.action"></view>
<view class="timeline-line" v-if="index !== detailData.actionTimeline.length - 1"></view>
<view class="timeline-content">
<view class="timeline-header">
<text class="timeline-name">{{ item.actionUserName || '未知审批人' }}</text>
<text class="timeline-time">{{ formatTime(item.createTime) }}</text>
</view>
<view class="timeline-meta">
<text>节点{{ item.nodeName || item.nodeId || '-' }}</text>
<text>动作{{ item.actionText || item.action || '-' }}</text>
</view>
<view class="timeline-meta">
<text>任务状态{{ statusText(item.taskStatus) }}</text>
</view>
<view class="timeline-remark" v-if="item.remark">意见{{ item.remark }}</view>
</view>
</view>
</view>
<component
:is="currentDetailComponent"
:bizId="bizId"
v-if="bizId && bizType"
></component>
<!-- 底部固定的审批操作按钮栏 -->
<view class="approval-btn-bar" v-if="canApprove">
<button class="btn reject-btn" @click="handleReject">驳回</button>
<button class="btn approve-btn" @click="handleApprove">通过</button>
@@ -21,17 +66,14 @@
import HRMSealDetail from '@/components/hrm/detailPanels/seal.vue'
import HRMTravelDetail from '@/components/hrm/detailPanels/travel.vue'
import HRMAppropriationDetail from '@/components/hrm/detailPanels/appropriation.vue'
import {
approveFlowTask,
listTodoFlowTask,
rejectFlowTask,
getTodoTaskByBiz,
getFlowTaskDetailByBiz,
} from '@/api/hrm/flow';
export default {
components: {
// 注册所有详情组件,供动态组件使用
HRMLeaveDetail,
HRMReimburseDetail,
HRMSealDetail,
@@ -42,73 +84,82 @@
return {
bizId: undefined,
bizType: undefined,
currentTask: undefined,
// 映射bizType到对应的组件名需和你的bizType实际值匹配可自行调整
detailData: null,
bizTypeComponentMap: {
leave: 'HRMLeaveDetail', // 请假
reimburse: 'HRMReimburseDetail', // 报销
seal: 'HRMSealDetail', // 用章
travel: 'HRMTravelDetail' ,// 差旅
leave: 'HRMLeaveDetail',
reimburse: 'HRMReimburseDetail',
seal: 'HRMSealDetail',
travel: 'HRMTravelDetail',
appropriation: 'HRMAppropriationDetail'
}
}
},
computed: {
// 计算属性根据bizType获取当前要渲染的组件名
currentDetailComponent() {
return this.bizTypeComponentMap[this.bizType] || '';
},
canApprove() {
console.log(this.currentTask, this.$store.getters.storeOaName, this.$store.getters.storeOaId)
return this.currentTask && this.currentTask.status === 'pending' &&
(this.currentTask?.assigneeUserName === this.$store.getters.storeOaName
|| this.currentTask?.assigneeUserId === this.$store.getters.storeOaId)
currentTask() {
return this.detailData?.currentTask || null;
},
canApprove() {
return this.currentTask && this.currentTask.status === 'pending' &&
(this.currentTask?.assigneeUserName === this.$store.getters.storeOaName
|| this.currentTask?.assigneeUserId === this.$store.getters.storeOaId)
},
assigneeText() {
return this.currentTask?.assigneeNickName || this.currentTask?.assigneeUserName || this.currentTask?.assigneeUserId || '-';
}
},
watch: {
bizId: {
immediate: true, // 页面加载时立即执行(原代码缺失,导致首次赋值不触发)
immediate: true,
handler(newVal) {
if (!newVal || !this.bizType) return;
// 获取当前审批任务信息
getTodoTaskByBiz(this.bizType, newVal)
getFlowTaskDetailByBiz(this.bizType, newVal)
.then(res => {
this.currentTask = res.data;
this.detailData = res.data || null;
})
.catch(err => {
uni.showToast({
title: '获取审批信息失败',
icon: 'none'
});
console.error('获取审批任务失败:', err);
console.error('获取审批详情失败:', err);
});
}
}
},
methods: {
/**
* 审批通过
*/
statusText(status) {
const map = {
pending: '审批中',
running: '流转中',
approved: '已通过',
rejected: '已驳回',
reject: '已驳回',
withdraw: '已撤回',
withdrawn: '已撤回'
};
return map[status] || status || '-';
},
statusClass(status) {
return status || 'default';
},
formatTime(time) {
if (!time) return '-';
const d = new Date(time);
return Number.isNaN(d.getTime()) ? time : `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
},
handleApprove() {
if (!this.currentTask?.taskId) {
uni.showToast({ title: '暂无审批任务', icon: 'none' });
return;
}
// 二次确认
uni.showModal({
title: '确认通过',
content: '是否确定通过该审批?',
// 箭头函数保留this指向
success: (res) => {
if (res.confirm) {
approveFlowTask(this.currentTask.taskId)
.then(() => {
uni.showToast({ title: '审批通过成功' });
// 成功后返回上一页(可根据需求调整)
setTimeout(() => {
uni.navigateBack();
}, 1500);
setTimeout(() => uni.navigateBack(), 1500);
})
.catch(err => {
uni.showToast({ title: '审批通过失败', icon: 'none' });
@@ -118,17 +169,11 @@
}
});
},
/**
* 审批驳回
*/
handleReject() {
if (!this.currentTask?.taskId) {
uni.showToast({ title: '暂无审批任务', icon: 'none' });
return;
}
// 二次确认(可扩展:添加驳回理由输入框)
uni.showModal({
title: '确认驳回',
content: '是否确定驳回该审批?',
@@ -137,10 +182,7 @@
rejectFlowTask(this.currentTask.taskId)
.then(() => {
uni.showToast({ title: '审批驳回成功' });
// 成功后返回上一页(可根据需求调整)
setTimeout(() => {
uni.navigateBack();
}, 1500);
setTimeout(() => uni.navigateBack(), 1500);
})
.catch(err => {
uni.showToast({ title: '审批驳回失败', icon: 'none' });
@@ -151,14 +193,9 @@
});
}
},
onLoad(options) {
console.log('页面入参:', options);
// 校验入参避免undefined
this.bizId = options.bizId || '';
this.bizType = options.bizType || '';
// 入参缺失提示
if (!this.bizId || !this.bizType) {
uni.showToast({ title: '参数缺失,无法加载详情', icon: 'none' });
}
@@ -167,46 +204,60 @@
</script>
<style scoped>
/* 页面容器 */
.approval-detail-page {
min-height: 100vh;
padding-bottom: 120rpx; /* 给底部按钮栏留空间 */
padding: 24rpx 24rpx 140rpx;
background: #f7f8fa;
}
/* 底部审批按钮栏 */
.approval-btn-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
.flow-summary-card, .flow-history-card {
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.04);
}
.summary-head {
display: flex;
padding: 20rpx;
background-color: #fff;
border-top: 1px solid #eee;
z-index: 99;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
/* 按钮通用样式 */
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
.summary-title, .card-title {
font-size: 32rpx;
border: none;
margin: 0 10rpx;
font-weight: 600;
color: #1f2937;
}
/* 驳回按钮样式 */
.reject-btn {
background-color: #fff;
color: #ff4757;
border: 1px solid #ff4757;
.status-tag {
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-size: 24rpx;
background: #eef2ff;
color: #4f46e5;
}
/* 通过按钮样式 */
.approve-btn {
background-color: #007aff;
color: #fff;
.summary-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
</style>
.summary-item {
width: calc(50% - 8rpx);
background: #f8fafc;
border-radius: 16rpx;
padding: 20rpx;
}
.label { display:block; color:#6b7280; font-size:24rpx; margin-bottom: 8rpx; }
.value { display:block; color:#111827; font-size:28rpx; font-weight:500; }
.timeline-item { position: relative; display:flex; padding-top: 24rpx; }
.timeline-dot { width: 18rpx; height:18rpx; border-radius:50%; background:#cbd5e1; margin-right: 16rpx; margin-top: 8rpx; flex-shrink:0; }
.timeline-dot.approve, .timeline-dot.approved, .timeline-dot.running { background:#22c55e; }
.timeline-dot.reject, .timeline-dot.rejected { background:#ef4444; }
.timeline-line { position:absolute; left: 8rpx; top: 36rpx; bottom:-4rpx; width:2rpx; background:#e5e7eb; }
.timeline-content { flex:1; padding-bottom: 8rpx; }
.timeline-header, .timeline-meta { display:flex; justify-content:space-between; gap: 16rpx; font-size:24rpx; color:#6b7280; }
.timeline-name { color:#111827; font-weight:600; }
.timeline-remark { margin-top: 8rpx; color:#374151; font-size:26rpx; }
.approval-btn-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx; background-color: #fff; border-top: 1px solid #eee; z-index: 99; }
.btn { flex: 1; height: 88rpx; line-height: 88rpx; border-radius: 44rpx; font-size: 32rpx; border: none; margin: 0 10rpx; }
.reject-btn { background-color: #fff; color: #ff4757; border: 1px solid #ff4757; }
.approve-btn { background-color: #007aff; color: #fff; }
</style>