Files
im-uniapp/pages/workbench/hrm/apply/apply.vue
砂糖 955d20413b feat(HRM): 新增HRM办公审批模块及相关功能组件
新增HRM办公审批模块,包括审批中心、抄送我的、我的申请等功能页面和组件。主要变更包括:

1. 添加审批相关API接口文件
2. 新增审批详情展示组件
3. 实现审批流程操作功能
4. 添加Vuex状态管理
5. 新增相关静态资源图片
6. 配置页面路由
7. 实现审批列表展示和筛选功能
8. 添加审批操作弹窗和状态管理
2026-02-05 10:42:50 +08:00

627 lines
14 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="hrm-page">
<!-- 筛选栏 -->
<view class="filter-bar">
<view style="display: flex; align-items: center; gap: 16rpx; flex: 1">
<span class="filter-label">申请类型</span>
<picker @change="handleBizTypeChange" :value="bizTypeIndex" :range="bizTypeList" range-key="label">
<view class="picker-text">{{ bizTypeList[bizTypeIndex].label }}</view>
</picker>
</view>
<button class="refresh-btn" size="mini" @click="loadTodoList">
刷新
</button>
</view>
<!-- 待审批列表 -->
<scroll-view class="approval-list" scroll-y>
<!-- 加载中 -->
<view v-if="loading" class="loading-container">
<uni-load-more type="loading" color="#409EFF"></uni-load-more>
</view>
<!-- 空数据 -->
<view v-else-if="todoList.length === 0" class="empty-container">
<uni-icons type="empty" size="60" color="#909399"></uni-icons>
<view class="empty-text">暂无待审批任务</view>
</view>
<!-- 列表项 -->
<view v-else class="list-item" v-for="(item, index) in todoList" :key="index" @click="goDetail(item)">
<!-- 申请类型标签 -->
<view class="item-tag" :class="getBizTypeTagType(item.bizType)">
{{ getBizTypeText(item.bizType) }}
</view>
<!-- 核心信息 -->
<view class="item-main">
<view class="applicant">
<uni-icons type="user" size="14" color="#8a8f99"></uni-icons>
{{ item.bizTitle || '暂未标识' }}
</view>
<!-- <view class="request-info">{{ formatRequestInfo(item) }}</view> -->
<!-- <view class="node-info">当前节点{{ formatNodeName(item) }}</view> -->
<view class="time-info">{{ formatDate(item.createTime) }}</view>
</view>
<!-- 状态标签 -->
<view class="status-tag" :class="statusType(item.status)">
{{ statusText(item.status) }}
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { listMyFlowInstance } from '@/api/hrm/flow';
export default {
name: 'HrmApproval',
data() {
return {
// 员工列表
employees: [],
// 待审批列表
todoList: [],
loading: false,
todoCount: 0,
todayCount: 0,
// 筛选条件
query: {
bizType: ''
},
// 申请类型筛选器配置
bizTypeList: [{
label: '全部',
value: ''
},
{
label: '请假',
value: 'leave'
},
{
label: '出差',
value: 'travel'
},
{
label: '用印',
value: 'seal'
},
{
label: '报销',
value: 'reimburse'
}
],
bizTypeIndex: 0,
// 审批操作弹窗
actionDialog: {
visible: false,
title: '',
type: '', // approve/reject
task: null
},
actionForm: {
remark: ''
},
actionSubmitting: false
}
},
onLoad() {
this.loadEmployees();
},
onShow() {
this.loadHistory();
},
methods: {
// async loadCurrentEmployee () {
// try {
// const userId = this.$store?.state?.user?.id
// if (!userId) {
// this.$message.warning('无法获取当前用户信息,请重新登录')
// this.loadHistory() // Still try to load history if user is not found
// return
// }
// const res = await getEmployeeByUserId(userId)
// if (res.code === 200 && res.data) {
// this.currentEmp = res.data
// } else {
// this.$message.warning('未找到当前用户对应的员工信息')
// }
// } catch (err) {
// console.error('加载员工信息失败', err)
// this.$message.error('加载员工信息失败')
// } finally {
// this.loadHistory()
// }
// },
async loadHistory() {
try {
const params = {
pageNum: 1,
pageSize: 100,
// bizType: this.historyQuery.type || undefined, // 业务类型leave/travel/seal/reimburse
}
const res = await listMyFlowInstance(params)
if (res.code === 200) {
this.todoList = res.rows || []
this.todoCount = res.total || 0
} else {
this.historyList = []
this.historyTotal = 0
}
} catch (err) {
console.error('加载申请历史失败:', err)
this.historyList = []
this.historyTotal = 0
}
},
// 格式化员工信息展示
formatEmpLabel(emp) {
if (!emp) return '';
const name = emp.empName || emp.nickName || emp.userName || '';
const no = emp.empNo ? ` · ${emp.empNo}` : '';
const dept = emp.deptName ? ` · ${emp.deptName}` : '';
return `${name || '员工'}${no}${dept}`.trim();
},
// 格式化申请人信息
formatApplicant(task) {
if (!task.bizData) return '加载中...';
const empId = task.bizData.empId;
const emp = this.employees.find(e => String(e.empId) === String(empId));
if (emp) {
return this.formatEmpLabel(emp);
}
return empId ? `员工ID:${empId}` : '未指定';
},
// 格式化申请信息
formatRequestInfo(task) {
if (!task.bizData) return '加载中...';
const biz = task.bizData;
if (task.bizType === 'leave') {
return `${biz.leaveType || '请假'} · ${this.formatDuration(biz)}`;
} else if (task.bizType === 'travel') {
return `${biz.travelType || '出差'} · ${biz.destination || ''}`;
} else if (task.bizType === 'seal') {
return `${biz.sealType || '用印'} · ${biz.applyFileIds ? '已上传文件' : '未上传'}`;
} else if (task.bizType === 'reimburse') {
const amt = biz.totalAmount != null ? biz.totalAmount : 0;
return `${biz.reimburseType || '报销'} · 金额: ${amt}`;
}
return '-';
},
// 格式化节点名称
formatNodeName(task) {
return `节点 #${task.nodeId}`;
},
// 获取申请类型文本
getBizTypeText(type) {
const map = {
leave: '请假',
travel: '出差',
seal: '用印',
reimburse: '报销'
};
return map[type] || type || '-';
},
// 获取申请类型标签样式
getBizTypeTagType(type) {
const map = {
leave: 'primary',
travel: 'success',
seal: 'warning',
reimburse: 'danger'
};
return map[type] || 'info';
},
// 获取状态文本
statusText(status) {
const map = {
pending: '待审批',
running: '待审批',
draft: '草稿',
approved: '已通过',
rejected: '已驳回'
};
return map[status] || status || '-';
},
// 获取状态标签样式
statusType(status) {
if (!status) return 'info';
const map = {
pending: 'warning',
running: 'warning',
draft: 'info',
approved: 'success',
rejected: 'danger'
};
return map[status] || 'info';
},
// 格式化日期
formatDate(val) {
if (!val) return '';
const d = new Date(val);
const p = n => (n < 10 ? `0${n}` : n);
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
},
// 格式化时长
formatDuration(biz) {
if (biz.hours) return `${biz.hours}h`;
if (biz.startTime && biz.endTime) {
const ms = new Date(biz.endTime).getTime() - new Date(biz.startTime).getTime();
if (ms > 0) return `${(ms / 3600000).toFixed(1)}h`;
}
return '-';
},
// 加载员工列表
loadEmployees() {
listEmployee({
pageNum: 1,
pageSize: 1000
}).then(res => {
this.employees = res.rows || res.data || [];
}).catch(() => {
this.employees = [];
});
},
// 加载待办列表
async loadTodoList() {
this.loading = true;
try {
const userId = this.$store?.getters.storeOaId;
console.log(this.$store?.getters.storeOaId)
if (!userId) {
uni.showToast({
title: '无法获取当前用户信息,请重新登录',
icon: 'error'
});
this.loading = false;
return;
}
const res = await listTodoFlowTask(userId);
let list = res.data || [];
// 前端过滤 bizType
if (this.query.bizType) {
list = list.filter(item => item.bizType === this.query.bizType);
}
this.todoList = list;
this.todoCount = list.length;
this.todayCount = 0; // 可根据实际需求实现今日处理数量统计
} catch (err) {
console.error('加载待办任务失败:', err);
uni.showToast({
title: '加载待办任务失败',
icon: 'error'
});
this.todoList = [];
this.todoCount = 0;
} finally {
this.loading = false;
}
},
// 跳转详情页
goDetail(task) {
if (!task || !task.bizId) {
uni.showToast({
title: '缺少bizId无法打开详情',
icon: 'none'
});
return;
}
if (!task || !task.bizType) {
uni.showToast({
title: '未知的申请类型',
icon: 'none'
});
return;
}
uni.navigateTo({
url: `/pages/workbench/hrm/detail/detail?bizId=${task.bizId}&bizType=${task.bizType}`
});
},
// 申请类型筛选变更
handleBizTypeChange(e) {
this.bizTypeIndex = e.detail.value;
this.query.bizType = this.bizTypeList[this.bizTypeIndex].value;
this.loadTodoList();
},
// 长按弹出操作菜单
showActionMenu(task) {
uni.showActionSheet({
itemList: ['查看详情', '审批通过', '审批驳回'],
success: (res) => {
switch (res.tapIndex) {
case 0: // 查看详情
this.goDetail(task);
break;
case 1: // 审批通过
approveFlowTask(task.taskId).then(() => {
uni.showToast({
title: '已通过审批',
icon: 'none'
})
this.loadTodoList()
})
break;
case 2: // 审批驳回
rejectFlowTask(task.taskId).then(() => {
uni.showToast({
title: '已驳回',
icon: 'none'
})
this.loadTodoList()
})
break;
}
},
fail: (res) => {
console.log('取消操作:', res);
}
});
},
// 提交审批操作
submitAction() {
if (!this.actionDialog.task) return;
this.actionSubmitting = true;
const {
task,
type
} = this.actionDialog;
const action = type === 'approve' ? approveFlowTask : rejectFlowTask;
action(task.taskId, {
remark: this.actionForm.remark
})
.then(() => {
uni.showToast({
title: type === 'approve' ? '审批通过' : '已驳回',
icon: 'success'
});
this.actionDialog.visible = false;
this.loadTodoList();
})
.catch(err => {
console.error('审批操作失败:', err);
uni.showToast({
title: '操作失败',
icon: 'error'
});
})
.finally(() => {
this.actionSubmitting = false;
this.actionForm.remark = '';
});
}
}
}
</script>
<style lang="scss" scoped>
.hrm-page {
min-height: 100vh;
background-color: #f8f9fb;
padding: 16rpx;
}
// 顶部统计栏
.summary-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
padding: 20rpx;
margin-bottom: 16rpx;
border: 1px solid #e6e8ed;
border-radius: 10rpx;
background: #ffffff;
.summary-left {
.page-title {
font-size: 32rpx;
font-weight: 800;
color: #2b2f36;
line-height: 1.2;
}
.page-desc {
margin-top: 8rpx;
font-size: 24rpx;
color: #8a8f99;
}
}
.summary-right {
display: flex;
gap: 16rpx;
.metric {
min-width: 100rpx;
padding: 12rpx 16rpx;
border: 1px solid #e6e8ed;
border-radius: 10rpx;
background: #fcfdff;
text-align: center;
.metric-value {
font-size: 32rpx;
font-weight: 800;
color: #2b2f36;
line-height: 1.2;
}
.metric-label {
margin-top: 4rpx;
font-size: 24rpx;
color: #8a8f99;
}
}
}
}
// 筛选栏
.filter-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx;
background: #fff;
border-radius: 8rpx;
margin-bottom: 16rpx;
box-sizing: border-box;
.filter-label {
font-size: 28rpx;
color: #333;
}
.picker-text {
flex: 1;
font-size: 28rpx;
color: #666;
padding: 8rpx 16rpx;
border: 1px solid #e6e8ed;
border-radius: 6rpx;
}
.refresh-btn {
background-color: #409eff;
color: #fff;
border: none;
border-radius: 6rpx;
font-size: 24rpx;
padding: 8rpx 16rpx;
}
}
// 审批列表
.approval-list {
height: calc(100vh - 160rpx);
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
}
.empty-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 100rpx 0;
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #909399;
}
}
.list-item {
display: flex;
align-items: flex-start;
gap: 16rpx;
padding: 20rpx;
margin-bottom: 12rpx;
background: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.item-tag {
padding: 6rpx 12rpx;
border-radius: 4rpx;
font-size: 22rpx;
color: #fff;
&.primary {
background-color: #409eff;
}
&.success {
background-color: #67c23a;
}
&.warning {
background-color: #e6a23c;
}
&.danger {
background-color: #f56c6c;
}
&.info {
background-color: #909399;
}
}
.item-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.applicant {
font-size: 28rpx;
color: #333;
display: flex;
align-items: center;
gap: 8rpx;
}
.request-info {
font-size: 26rpx;
color: #666;
}
.node-info {
font-size: 24rpx;
color: #888;
}
.time-info {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
}
.status-tag {
padding: 6rpx 12rpx;
border-radius: 4rpx;
font-size: 22rpx;
color: #fff;
&.warning {
background-color: #e6a23c;
}
&.info {
background-color: #909399;
}
&.success {
background-color: #67c23a;
}
&.danger {
background-color: #f56c6c;
}
}
}
}
// 审批意见输入框
.remark-textarea {
width: 100%;
min-height: 120rpx;
padding: 16rpx;
border: 1px solid #e6e8ed;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
</style>