640 lines
16 KiB
Vue
640 lines
16 KiB
Vue
<template>
|
|
<view class="hrm-page">
|
|
<view class="page-spacer"></view>
|
|
<view class="page-nav">
|
|
<view class="nav-tabs nav-tabs--top">
|
|
<view class="nav-tab" :class="{ active: activeTopTab === 'approval' }" @click="switchTopTab('approval')">我的审批</view>
|
|
<view class="nav-tab" :class="{ active: activeTopTab === 'apply' }" @click="switchTopTab('apply')">发起申请</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="activeTopTab === 'approval'" class="tab-panel lower-panel">
|
|
<view class="sub-tabs sub-tabs--underline">
|
|
<view class="sub-tab" :class="{ active: activeApprovalTab === 'todo' }" @click="switchApprovalTab('todo')">
|
|
<view class="tab-label-row">
|
|
<text>待处理</text>
|
|
<view v-if="summary.todo > 0" class="tab-badge">
|
|
<view class="tab-dot"></view>
|
|
<text class="tab-badge-num">{{ summary.todo }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="sub-tab" :class="{ active: activeApprovalTab === 'done' }" @click="switchApprovalTab('done')">已处理</view>
|
|
<view class="sub-tab" :class="{ active: activeApprovalTab === 'cc' }" @click="switchApprovalTab('cc')">抄送我</view>
|
|
</view>
|
|
|
|
<view class="type-row">
|
|
<scroll-view class="type-scroll" scroll-x show-scrollbar="false">
|
|
<view class="type-tabs">
|
|
<view class="type-tab" :class="{ active: bizTypeIndex === 0 }" @click="setBizType(0)">全部</view>
|
|
<view class="type-tab" v-for="(item, index) in bizTypeList.slice(1)" :key="item.value" :class="{ active: bizTypeIndex === index + 1 }" @click="setBizType(index + 1)">{{ item.label }}</view>
|
|
</view>
|
|
</scroll-view>
|
|
<view class="refresh-row" @click="refreshCurrentList">
|
|
<uni-icons type="refreshempty" size="18" color="#1677ff"></uni-icons>
|
|
</view>
|
|
</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="currentList.length === 0" class="empty-container">
|
|
<uni-icons type="empty" size="60" color="#909399"></uni-icons>
|
|
<view class="empty-text">{{ emptyText }}</view>
|
|
</view>
|
|
|
|
<view v-else class="list-item" v-for="(item, index) in currentList" :key="item.taskId || item.instId || index" @click="goDetail(item)" @longpress="showActionMenu(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>
|
|
{{ formatApplicant(item) }}
|
|
</view>
|
|
<view class="request-info">{{ formatRequestInfo(item) }}</view>
|
|
<view class="time-info">{{ formatDate(item.createTime || item.startTime) }}</view>
|
|
</view>
|
|
<view class="status-tag" :class="statusType(item.status)">{{ statusText(item.status) }}</view>
|
|
</view>
|
|
</scroll-view>
|
|
</view>
|
|
|
|
<view v-else class="tab-panel apply-panel lower-panel">
|
|
|
|
<view class="apply-list">
|
|
<view class="apply-item" v-for="item in applyTypes" :key="item.key" @click="goCreate(item.key)">
|
|
<view class="apply-item-main">
|
|
<view class="apply-item-title">{{ item.title }}</view>
|
|
<view class="apply-item-desc">{{ item.desc }}</view>
|
|
</view>
|
|
<uni-icons type="right" size="18" color="#111827"></uni-icons>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { getUserProfile } from '@/api/oa/user.js';
|
|
import { approveFlowTask, listMyFlowInstance, listTodoFlowTask, listDoneFlowTask, rejectFlowTask } from '@/api/hrm/flow';
|
|
|
|
export default {
|
|
name: 'HrmApproval',
|
|
data() {
|
|
return {
|
|
activeTopTab: 'approval',
|
|
activeApprovalTab: 'todo',
|
|
loading: false,
|
|
todoList: [],
|
|
doneList: [],
|
|
ccList: [],
|
|
summary: {
|
|
todo: 0,
|
|
done: 0,
|
|
cc: 0
|
|
},
|
|
query: {
|
|
bizType: ''
|
|
},
|
|
bizTypeList: [
|
|
{ label: '全部', value: '' },
|
|
{ label: '请假', value: 'leave' },
|
|
{ label: '出差', value: 'travel' },
|
|
{ label: '用印', value: 'seal' },
|
|
{ label: '报销', value: 'reimburse' },
|
|
{ label: '拨款', value: 'appropriation' }
|
|
],
|
|
bizTypeIndex: 0,
|
|
currentOaUserId: '',
|
|
roleGroup: [],
|
|
applyTypes: [
|
|
{ key: 'leave', title: '请假申请', desc: '发起请假流程,支持多种请假类型' },
|
|
{ key: 'travel', title: '出差申请', desc: '发起出差流程,支持附件与审批流转' },
|
|
{ key: 'seal', title: '用印申请', desc: '发起用印流程,上传文件并提交审批' },
|
|
{ key: 'reimburse', title: '报销申请', desc: '发起报销流程,填写金额与凭证' },
|
|
{ key: 'appropriation', title: '拨款申请', desc: '发起拨款流程,填写用途与金额' }
|
|
]
|
|
}
|
|
},
|
|
onShow() {
|
|
this.fetchUserProfile().then(() => {
|
|
this.refreshCurrentList();
|
|
});
|
|
},
|
|
computed: {
|
|
currentList() {
|
|
if (this.activeApprovalTab === 'todo') return this.todoList;
|
|
if (this.activeApprovalTab === 'done') return this.doneList;
|
|
return this.ccList;
|
|
},
|
|
emptyText() {
|
|
const map = {
|
|
todo: '暂无待处理审批',
|
|
done: '暂无已处理记录',
|
|
cc: '暂无抄送消息'
|
|
};
|
|
return map[this.activeApprovalTab] || '暂无数据';
|
|
}
|
|
},
|
|
methods: {
|
|
goBack() {
|
|
uni.navigateBack({ delta: 1 });
|
|
},
|
|
async fetchUserProfile() {
|
|
try {
|
|
const response = await getUserProfile();
|
|
const user = response?.data?.user || {};
|
|
const roles = user.roles?.map(item => item.roleKey) || [];
|
|
this.$store.commit('oa/SET_STATE', user);
|
|
this.currentOaUserId = user.userId || '';
|
|
this.roleGroup = roles;
|
|
return user;
|
|
} catch (error) {
|
|
console.error('获取用户个人信息失败:', error);
|
|
this.roleGroup = [];
|
|
return null;
|
|
}
|
|
},
|
|
switchTopTab(tab) {
|
|
this.activeTopTab = tab;
|
|
if (tab === 'approval') {
|
|
this.refreshCurrentList();
|
|
}
|
|
},
|
|
switchApprovalTab(tab) {
|
|
this.activeApprovalTab = tab;
|
|
this.refreshCurrentList();
|
|
},
|
|
refreshCurrentList() {
|
|
if (this.activeTopTab !== 'approval') return;
|
|
if (this.activeApprovalTab === 'todo') {
|
|
this.loadTodoList();
|
|
return;
|
|
}
|
|
if (this.activeApprovalTab === 'done') {
|
|
this.loadDoneList();
|
|
return;
|
|
}
|
|
this.loadCcList();
|
|
},
|
|
setBizType(index) {
|
|
this.bizTypeIndex = index;
|
|
this.query.bizType = this.bizTypeList[index].value;
|
|
this.refreshCurrentList();
|
|
},
|
|
async loadTodoList() {
|
|
this.loading = true;
|
|
try {
|
|
const res = await listTodoFlowTask(this.currentOaUserId);
|
|
let list = res.data || [];
|
|
if (this.query.bizType) list = list.filter(item => item.bizType === this.query.bizType);
|
|
this.todoList = list;
|
|
this.summary.todo = list.length;
|
|
} catch (err) {
|
|
console.error('加载待办任务失败:', err);
|
|
this.todoList = [];
|
|
this.summary.todo = 0;
|
|
uni.showToast({ title: '加载待办任务失败', icon: 'none' });
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async loadDoneList() {
|
|
this.loading = true;
|
|
try {
|
|
const res = await listDoneFlowTask();
|
|
let list = (res.rows || res.data || []);
|
|
if (this.query.bizType) list = list.filter(item => item.bizType === this.query.bizType);
|
|
this.doneList = list;
|
|
this.summary.done = list.length;
|
|
} catch (err) {
|
|
console.error('加载已处理记录失败:', err);
|
|
this.doneList = [];
|
|
this.summary.done = 0;
|
|
uni.showToast({ title: '加载已处理记录失败', icon: 'none' });
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async loadCcList() {
|
|
this.loading = true;
|
|
try {
|
|
const res = await listMyFlowInstance({ pageNum: 1, pageSize: 200 });
|
|
let list = (res.rows || res.data || []).filter(item => item.ccFlag === 1 || item.isCc === 1 || item.readFlag === 0);
|
|
if (this.query.bizType) list = list.filter(item => item.bizType === this.query.bizType);
|
|
this.ccList = list;
|
|
this.summary.cc = list.length;
|
|
} catch (err) {
|
|
console.error('加载抄送记录失败:', err);
|
|
this.ccList = [];
|
|
this.summary.cc = 0;
|
|
uni.showToast({ title: '加载抄送记录失败', icon: 'none' });
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
getBizTypeText(type) {
|
|
const map = { leave: '请假', travel: '出差', seal: '用印', reimburse: '报销', appropriation: '拨款' };
|
|
return map[type] || type || '-';
|
|
},
|
|
getBizTypeTagType(type) {
|
|
const map = { leave: 'primary', travel: 'success', seal: 'warning', reimburse: 'danger', appropriation: 'danger' };
|
|
return map[type] || 'info';
|
|
},
|
|
statusText(status) {
|
|
const map = { pending: '待审批', draft: '草稿', approved: '已通过', rejected: '已驳回', running: '审批中', finished: '已完成', revoked: '已撤销' };
|
|
return map[status] || status || '-';
|
|
},
|
|
statusType(status) {
|
|
const map = { pending: 'warning', running: 'warning', draft: 'info', approved: 'success', rejected: 'danger', finished: 'success', revoked: '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())}`;
|
|
},
|
|
formatEmpLabel(emp) {
|
|
if (!emp) return '未指定';
|
|
const name = emp.empName || emp.nickName || emp.userName || '';
|
|
const dept = emp.deptName ? ` · ${emp.deptName}` : '';
|
|
return `${name || '员工'}${dept}`.trim();
|
|
},
|
|
formatApplicant(task) {
|
|
if (!task) return '未指定';
|
|
const biz = task.bizData || {};
|
|
return biz.empName || biz.applicantName || task.empName || task.applicantName || task.assigneeNickName || task.userName || '未指定';
|
|
},
|
|
formatRequestInfo(task) {
|
|
if (!task) return '-';
|
|
const biz = task.bizData || {};
|
|
if (task.bizType === 'leave') return `${biz.leaveType || '请假'} · ${this.formatDuration(biz)}`;
|
|
if (task.bizType === 'travel') return `${biz.travelType || '出差'} · ${biz.destination || ''}`;
|
|
if (task.bizType === 'seal') return `${biz.sealType || '用印'} · ${biz.applyFileIds ? '已上传文件' : '未上传'}`;
|
|
if (task.bizType === 'reimburse') return `${biz.reimburseType || '报销'} · 金额: ${(biz.totalAmount ?? 0)}元`;
|
|
if (task.bizType === 'appropriation') return `${biz.appropriationType || '拨款'} · 金额: ${(biz.amount ?? 0)}元`;
|
|
return task.bizTitle || '-';
|
|
},
|
|
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 '-';
|
|
},
|
|
goDetail(task) {
|
|
if (!task) return;
|
|
const bizId = task.bizId || task.instId;
|
|
const bizType = task.bizType;
|
|
if (!bizType || !bizId) {
|
|
uni.showToast({ title: '缺少业务信息,无法打开详情', icon: 'none' });
|
|
return;
|
|
}
|
|
uni.navigateTo({ url: `/pages/workbench/hrm/detail/detail?bizId=${bizId}&bizType=${bizType}` });
|
|
},
|
|
goCreate(type) {
|
|
const map = {
|
|
leave: '/pages/workbench/hrm/leave/leave',
|
|
travel: '/pages/workbench/hrm/travel/travel',
|
|
seal: '/pages/workbench/hrm/seal/seal',
|
|
reimburse: '/pages/workbench/hrm/reimburse/reimburse',
|
|
appropriation: '/pages/workbench/hrm/apply/apply'
|
|
};
|
|
uni.navigateTo({ url: map[type] || '/pages/workbench/hrm/apply/apply' });
|
|
},
|
|
goTravelEarlyEnd() {
|
|
uni.showToast({ title: '请进入出差详情后发起提前结束', icon: 'none' });
|
|
},
|
|
showActionMenu(task) {
|
|
if (this.activeApprovalTab !== 'todo') {
|
|
this.goDetail(task);
|
|
return;
|
|
}
|
|
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.refreshCurrentList();
|
|
});
|
|
break;
|
|
case 2:
|
|
rejectFlowTask(task.taskId).then(() => {
|
|
uni.showToast({ title: '已驳回', icon: 'none' });
|
|
this.refreshCurrentList();
|
|
});
|
|
break;
|
|
|
|
}
|
|
}
|
|
});
|
|
},
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.hrm-page {
|
|
min-height: 100vh;
|
|
background: #fff;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.page-spacer {
|
|
height: var(--status-bar-height, 44px);
|
|
background: #fff;
|
|
}
|
|
|
|
.page-nav {
|
|
position: sticky;
|
|
top: var(--status-bar-height, 44px);
|
|
z-index: 10;
|
|
background: #fff;
|
|
padding: 12rpx 16rpx 0;
|
|
border-bottom: 1rpx solid #eef2f7;
|
|
}
|
|
|
|
.nav-tabs {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 42rpx;
|
|
}
|
|
|
|
.nav-tabs--top {
|
|
padding-bottom: 8rpx;
|
|
}
|
|
|
|
.nav-tab {
|
|
position: relative;
|
|
padding: 10rpx 0 14rpx;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.nav-tab.active {
|
|
color: #1677ff;
|
|
}
|
|
|
|
.nav-tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 3rpx;
|
|
background: #1677ff;
|
|
}
|
|
|
|
.tab-panel {
|
|
padding: 0 16rpx;
|
|
}
|
|
|
|
.lower-panel {
|
|
padding-top: 14rpx;
|
|
}
|
|
|
|
.sub-tabs {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
gap: 32rpx;
|
|
margin: 4rpx 0 14rpx;
|
|
padding-bottom: 8rpx;
|
|
}
|
|
|
|
.sub-tab {
|
|
position: relative;
|
|
padding: 8rpx 0 14rpx;
|
|
font-size: 28rpx;
|
|
font-weight: 500;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.sub-tab.active {
|
|
color: #1677ff;
|
|
}
|
|
|
|
.sub-tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 3rpx;
|
|
background: #1677ff;
|
|
}
|
|
|
|
.tab-label-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.tab-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4rpx;
|
|
}
|
|
|
|
.tab-dot {
|
|
width: 12rpx;
|
|
height: 12rpx;
|
|
border-radius: 50%;
|
|
background: #ff4d4f;
|
|
box-shadow: 0 0 0 6rpx rgba(255, 77, 79, 0.14);
|
|
}
|
|
|
|
.tab-badge-num {
|
|
min-width: 24rpx;
|
|
height: 24rpx;
|
|
line-height: 24rpx;
|
|
padding: 0 6rpx;
|
|
border-radius: 999rpx;
|
|
background: #ff4d4f;
|
|
color: #fff;
|
|
font-size: 18rpx;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
}
|
|
|
|
.type-strip,
|
|
.type-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
padding: 2rpx 0 12rpx;
|
|
}
|
|
|
|
.type-scroll {
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.type-tabs {
|
|
display: inline-flex;
|
|
gap: 20rpx;
|
|
min-width: 100%;
|
|
align-items: center;
|
|
}
|
|
|
|
.type-tab {
|
|
position: relative;
|
|
flex: 0 0 auto;
|
|
padding: 10rpx 0 12rpx;
|
|
font-size: 26rpx;
|
|
font-weight: 500;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.type-tab.active {
|
|
color: #1677ff;
|
|
}
|
|
|
|
.type-tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
height: 3rpx;
|
|
background: #1677ff;
|
|
}
|
|
|
|
.refresh-row {
|
|
width: 52rpx;
|
|
height: 52rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.approval-list {
|
|
height: calc(100vh - 196rpx);
|
|
padding: 0 16rpx;
|
|
}
|
|
|
|
.loading-container,
|
|
.empty-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.loading-container {
|
|
height: 180rpx;
|
|
}
|
|
|
|
.empty-container {
|
|
flex-direction: column;
|
|
padding: 110rpx 0;
|
|
}
|
|
|
|
.empty-text {
|
|
margin-top: 16rpx;
|
|
font-size: 26rpx;
|
|
color: #98a2b3;
|
|
}
|
|
|
|
.list-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12rpx;
|
|
padding: 16rpx 0;
|
|
margin-bottom: 0;
|
|
background: #fff;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.item-tag,
|
|
.status-tag {
|
|
padding: 0;
|
|
border-radius: 0;
|
|
font-size: 22rpx;
|
|
color: #6b7280;
|
|
background: transparent !important;
|
|
}
|
|
|
|
.item-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.applicant {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
}
|
|
|
|
.request-info { font-size: 26rpx; color: #6b7280; }
|
|
.time-info { font-size: 24rpx; color: #9ca3af; }
|
|
|
|
.apply-panel {
|
|
padding-top: 16rpx;
|
|
}
|
|
|
|
.apply-feature-card {
|
|
height: 120rpx;
|
|
margin: 0 0 16rpx;
|
|
padding: 0 24rpx;
|
|
background: linear-gradient(135deg, #eef6ff 0%, #f7fbff 100%);
|
|
border: 1rpx solid #dbeafe;
|
|
border-radius: 16rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.apply-list {
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
overflow: hidden;
|
|
box-shadow: 0 6rpx 20rpx rgba(17, 24, 39, 0.05);
|
|
}
|
|
|
|
.apply-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16rpx;
|
|
padding: 24rpx;
|
|
border-bottom: 1rpx solid #f3f4f6;
|
|
}
|
|
|
|
.apply-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.apply-item-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.apply-item-title {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
color: #111827;
|
|
}
|
|
|
|
.apply-item-desc {
|
|
margin-top: 8rpx;
|
|
font-size: 24rpx;
|
|
line-height: 1.5;
|
|
color: #6b7280;
|
|
}
|
|
|
|
</style> |