Files
im-uniapp/pages/workbench/hrm/detail/detail.vue

367 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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"
@early-end="handleTravelEarlyEnd"
></component>
<view class="detail-action-bar" v-if="canTravelEarlyEnd">
<button class="btn early-end-btn" @click="handleTravelEarlyEnd">提前结束</button>
</view>
<view class="seal-inline-card" v-if="canSealApprove">
<view class="seal-inline-head">
<text class="seal-inline-title">用印签章审批</text>
<text class="seal-inline-subtitle">当前申请已绑定印章{{ sealTypeLabel }}</text>
</view>
<view class="seal-form-row">
<text class="seal-form-label">签章页码</text>
<input class="seal-form-input" type="number" v-model="sealStampForm.pageNo" :placeholder="sealPageHint" />
</view>
<view class="seal-form-row seal-form-row--info">
<text class="seal-form-label">PDF页数</text>
<text class="seal-form-value">{{ sealPageTotal || '-' }}</text>
</view>
<view class="seal-form-actions">
<button class="btn approve-btn" @click="submitSealStamp">签章通过</button>
</view>
</view>
<view class="approval-btn-bar" v-if="canApprove && bizType !== 'seal'">
<button class="btn reject-btn" @click="handleReject">驳回</button>
<button class="btn approve-btn" @click="handleApprove">通过</button>
</view>
</view>
</template>
<script>
import HRMLeaveDetail from '@/components/hrm/detailPanels/leave.vue'
import HRMReimburseDetail from '@/components/hrm/detailPanels/reimburse.vue'
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,
rejectFlowTask,
getFlowTaskDetailByBiz,
} from '@/api/hrm/flow';
import { earlyEndTravelReq } from '@/api/hrm/travel';
import { stampSealJava, getSealPdfPages } from '@/api/hrm/seal';
export default {
components: {
HRMLeaveDetail,
HRMReimburseDetail,
HRMSealDetail,
HRMTravelDetail,
HRMAppropriationDetail
},
data() {
return {
bizId: undefined,
bizType: undefined,
detailData: null,
bizTypeComponentMap: {
leave: 'HRMLeaveDetail',
reimburse: 'HRMReimburseDetail',
seal: 'HRMSealDetail',
travel: 'HRMTravelDetail',
appropriation: 'HRMAppropriationDetail'
},
sealStampPresets: [
{ key: 'left-bottom', label: '山东福安德信息科技有限公司采购部专用章FAD201400201.png', preview: 'http://49.232.154.205:10900/fad-oa/files%2Fstamp%2F山东福安德信息科技有限公司采购部专用章FAD201400201.png' },
{ key: 'center-bottom', label: '山东福安德信息科技有限公司采购部专用章FAD201400202.png', preview: 'http://49.232.154.205:10900/fad-oa/files%2Fstamp%2F山东福安德信息科技有限公司采购部专用章FAD201400202.png' },
{ key: 'right-bottom', label: '山东福安德信息科技有限公司采购部专用章FAD201400401.png', preview: 'http://49.232.154.205:10900/fad-oa/files%2Fstamp%2F山东福安德信息科技有限公司采购部专用章FAD201400401.png' }
],
sealStampForm: {
position: 'right-bottom',
pageNo: 1
},
sealPageTotal: 0
}
},
computed: {
currentDetailComponent() {
return this.bizTypeComponentMap[this.bizType] || '';
},
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)
},
canSealApprove() {
return this.bizType === 'seal' && this.canApprove;
},
sealPageHint() {
return this.sealPageTotal > 0 ? `1-${this.sealPageTotal}` : '1-1';
},
sealTypeLabel() {
return this.detailData?.sealType || '-';
},
canTravelEarlyEnd() {
return this.bizType === 'travel' && this.detailData?.actualEndTime == null;
},
assigneeText() {
return this.currentTask?.assigneeNickName || this.currentTask?.assigneeUserName || this.currentTask?.assigneeUserId || '-';
}
},
watch: {
bizId: {
immediate: true,
handler(newVal) {
if (!newVal || !this.bizType) return;
getFlowTaskDetailByBiz(this.bizType, newVal)
.then(res => {
this.detailData = res.data || null;
})
.catch(err => {
console.error('获取审批详情失败:', err);
});
}
}
},
methods: {
statusText(status) {
const map = {
pending: '审批中',
running: '流转中',
approved: '已通过',
rejected: '已驳回',
reject: '已驳回',
withdraw: '已撤回',
withdrawn: '已撤回',
finished: '已结束'
};
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: '是否确定通过该审批?',
success: (res) => {
if (res.confirm) {
approveFlowTask(this.currentTask.taskId)
.then(() => {
uni.showToast({ title: '审批通过成功' });
setTimeout(() => uni.navigateBack(), 1500);
})
.catch(err => {
uni.showToast({ title: '审批通过失败', icon: 'none' });
console.error('审批通过失败:', err);
});
}
}
});
},
handleReject() {
if (!this.currentTask?.taskId) {
uni.showToast({ title: '暂无审批任务', icon: 'none' });
return;
}
uni.showModal({
title: '确认驳回',
content: '是否确定驳回该审批?',
success: (res) => {
if (res.confirm) {
rejectFlowTask(this.currentTask.taskId)
.then(() => {
uni.showToast({ title: '审批驳回成功' });
setTimeout(() => uni.navigateBack(), 1500);
})
.catch(err => {
uni.showToast({ title: '审批驳回失败', icon: 'none' });
console.error('审批驳回失败:', err);
});
}
}
});
},
async fetchSealPageTotal() {
try {
const res = await getSealPdfPages(this.bizId);
this.sealPageTotal = Number(res?.data || 0);
} catch (e) {
this.sealPageTotal = 0;
}
},
submitSealStamp() {
const pageNo = Number(this.sealStampForm.pageNo) || 1;
const payload = {
pageNo,
xPx: 120,
yPx: 120,
viewportWidth: 750,
viewportHeight: 1334
};
stampSealJava(this.bizId, payload)
.then(() => {
uni.showToast({ title: '签章成功', icon: 'none' });
setTimeout(() => uni.navigateBack(), 1000);
})
.catch(err => {
console.error('签章失败:', err);
uni.showToast({ title: '签章失败', icon: 'none' });
});
},
handleTravelEarlyEnd() {
if (!this.canTravelEarlyEnd) {
uni.showToast({ title: '当前出差已结束', icon: 'none' });
return;
}
const bizId = this.bizId;
uni.showModal({
title: '提前结束出差',
content: '确认提前结束该出差吗?',
success: (res) => {
if (!res.confirm) return;
earlyEndTravelReq(bizId)
.then(() => {
uni.showToast({ title: '提前结束成功', icon: 'none' });
setTimeout(() => uni.navigateBack(), 1200);
})
.catch(err => {
console.error('提前结束失败:', err);
uni.showToast({ title: '提前结束失败', icon: 'none' });
});
}
});
}
},
onLoad(options) {
this.bizId = options.bizId || '';
this.bizType = options.bizType || '';
if (!this.bizId || !this.bizType) {
uni.showToast({ title: '参数缺失,无法加载详情', icon: 'none' });
}
if (this.bizType === 'seal') {
this.fetchSealPageTotal();
}
}
}
</script>
<style scoped>
.approval-detail-page {
min-height: 100vh;
padding: 24rpx 24rpx 140rpx;
background: #f7f8fa;
}
.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;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.summary-title, .card-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2937;
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-size: 24rpx;
background: #eef2ff;
color: #4f46e5;
}
.summary-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.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; }
.detail-action-bar { display: flex; justify-content: center; margin: 24rpx 0; }
.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; }
.early-end-btn { flex: none; min-width: 260rpx; background-color: #007aff; color: #fff; }
.reject-btn { background-color: #fff; color: #ff4757; border: 1px solid #ff4757; }
.approve-btn { background-color: #007aff; color: #fff; }
</style>