Files
klp-oa/klp-ui/src/views/hrm/requests/apply.vue
砂糖 b0ee494434 feat(flow): 添加流程实例更新功能并禁用撤回操作
添加updateFlowInstance API用于更新流程实例
在所有详情页面禁用撤回功能
修改审批状态从pending到running
在抄送页面添加详情跳转功能
2026-01-05 14:38:22 +08:00

449 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>
<div class="hrm-page">
<!-- 发起申请模块 -->
<section class="apply-section">
<div class="section-header">
<h3 class="section-title">发起申请</h3>
<p class="section-desc">选择申请类型快速发起新的申请流程</p>
</div>
<div class="apply-cards">
<el-card
v-for="item in applyTypes"
:key="item.key"
class="apply-card"
shadow="hover"
@click.native="goCreate(item.key)"
>
<div class="card-content">
<div class="card-title">{{ item.title }}</div>
<div class="card-desc">{{ item.desc }}</div>
</div>
<div class="card-action">
<el-button type="primary" size="small" icon="el-icon-plus">发起</el-button>
</div>
</el-card>
</div>
</section>
<!-- 申请历史模块 -->
<section class="history-section">
<div class="section-header">
<h3 class="section-title">我的申请</h3>
<div class="section-actions">
<el-select
v-model="historyQuery.type"
size="small"
placeholder="申请类型"
clearable
style="width: 120px; margin-right: 8px"
@change="loadHistory"
>
<el-option label="全部" value="" />
<el-option label="请假" value="leave" />
<el-option label="出差" value="travel" />
<el-option label="用印" value="seal" />
<el-option label="报销" value="reimburse" />
</el-select>
<el-select
v-model="historyQuery.status"
size="small"
placeholder="状态"
clearable
style="width: 120px; margin-right: 8px"
@change="loadHistory"
>
<el-option label="全部" value="" />
<el-option label="草稿" value="draft" />
<el-option label="审批中" value="running" />
<el-option label="已通过" value="approved" />
<el-option label="已驳回" value="rejected" />
</el-select>
<el-button size="small" icon="el-icon-refresh" @click="loadHistory">刷新</el-button>
</div>
</div>
<el-card class="history-card" shadow="never">
<el-table
:data="historyList"
v-loading="historyLoading"
stripe
@row-dblclick="handleRowClick"
>
<el-table-column label="申请类型" min-width="100">
<template slot-scope="scope">
<el-tag :type="getTypeTagType(scope.row.bizType)">{{ getTypeText(scope.row.bizType) }}</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="申请人" min-width="120">
<template slot-scope="scope">
{{ formatEmpDisplay(scope.row.startUserId) }}
</template>
</el-table-column> -->
<el-table-column label="类型/目的" min-width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" placeholder="请输入类型/目的便于审批或查看" @change="handleRemarkChange(scope.row)"></el-input>
<!-- {{ getTypeDetail(scope.row) }} -->
</template>
</el-table-column>
<!-- <el-table-column label="开始时间" prop="startTime" min-width="160">
<template slot-scope="scope">{{ formatDate(scope.row.startTime) }}</template>
</el-table-column>
<el-table-column label="结束时间" prop="endTime" min-width="160">
<template slot-scope="scope">{{ formatDate(scope.row.endTime) }}</template>
</el-table-column>
<el-table-column label="时长" min-width="100">
<template slot-scope="scope">{{ formatDuration(scope.row) }}</template>
</el-table-column> -->
<el-table-column label="状态" prop="status" min-width="110">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)">{{ statusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="申请时间" prop="createTime" min-width="160">
<template slot-scope="scope">{{ formatDate(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" min-width="120" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="goDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
:current-page="historyQuery.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="historyQuery.pageSize"
:total="historyTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</section>
</div>
</template>
<script>
import { listMyFlowInstance, updateFlowInstance } from '@/api/hrm/flow'
import { getEmployeeByUserId } from '@/api/hrm/employee'
export default {
name: 'HrmApply',
data() {
return {
currentEmp: null,
applyTypes: [
{
key: 'leave',
title: '请假申请',
desc: '申请各类假期,包括年假、病假、事假等',
icon: 'el-icon-calendar',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
key: 'travel',
title: '出差申请',
desc: '申请出差,包括目的地、时间、费用等信息',
icon: 'el-icon-location',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
key: 'seal',
title: '用印申请',
desc: '申请使用印章,上传文件并指定盖章位置',
icon: 'el-icon-stamp',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
key: 'reimburse',
title: '报销申请',
desc: '申请费用报销,包括单据、理由、金额等信息',
icon: 'el-icon-money',
color: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
}
],
historyList: [],
historyLoading: false,
historyTotal: 0,
historyQuery: {
pageNum: 1,
pageSize: 10,
type: '',
status: ''
}
}
},
created() {
this.loadCurrentEmployee()
},
methods: {
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()
},
handleRemarkChange(row) {
updateFlowInstance(row).then(response => {
if (response.code === 200) {
this.$message({
message: '更新成功',
type: 'success'
})
} else {
this.$message({
message: `更新失败: ${response.msg || '未知错误'}`,
type: 'error'
})
}
})
},
formatEmpDisplay(userId) {
// The API returns startUserId, which is a user ID, not an empId.
// We can display the current user's name if the ID matches.
if (this.currentEmp && String(this.currentEmp.userId) === String(userId)) {
return this.formatEmpLabel(this.currentEmp)
}
return userId ? `用户ID:${userId}` : '未指定'
},
formatDuration(row) {
if (row.hours) return `${row.hours}h`
if (row.startTime && row.endTime) {
const ms = new Date(row.endTime).getTime() - new Date(row.startTime).getTime()
if (ms > 0) return `${(ms / 3600000).toFixed(1)}h`
}
return '-'
},
statusText(status) {
const map = { running: '审批中', draft: '草稿', approved: '已通过', rejected: '已驳回', finished: '已完成' }
return map[status] || status || '-'
},
statusType(status) {
if (!status) return 'info'
const map = { running: 'warning', draft: 'info', approved: 'success', rejected: 'danger', finished: 'success' }
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())}`
},
getTypeText(type) {
const map = { leave: '请假', travel: '出差', seal: '用印', reimburse: '报销' }
return map[type] || type || '-'
},
getTypeTagType(type) {
const map = { leave: 'primary', travel: 'success', seal: 'warning', reimburse: 'danger' }
return map[type] || 'info'
},
getTypeDetail(row) {
// This detail might need to be fetched or be part of the process variables
// For now, we display the process name as a fallback.
return row.procDefName || '-'
},
goCreate(key) {
const pathMap = {
leave: '/hrm/HrmLeaveRequest',
travel: '/hrm/HrmTravelRequest',
seal: '/hrm/HrmSealRequest',
reimburse: '/hrm/HrmReimburseRequest'
}
const path = pathMap[key]
if (path) {
this.$router.push(path)
} else {
this.$message.warning('该申请类型暂未实现')
}
},
goDetail(row) {
if (!row || !row.bizId || !row.bizType) {
this.$message.warning('缺少businessKey无法打开详情')
return
}
const { bizId, bizType: type } = row
if (!bizId) {
this.$message.warning('businessKey格式不正确无法解析业务ID')
return
}
const pathMap = {
leave: '/hrm/HrmLeaveDetail',
travel: '/hrm/HrmTravelDetail',
seal: '/hrm/HrmSealDetail',
reimburse: '/hrm/HrmReimburseDetail'
}
const routePath = pathMap[type]
if (routePath) {
this.$router.push({
path: routePath,
query: { bizId: bizId }
})
} else {
this.$message.warning('无法确定申请类型对应的详情页面')
}
},
handleRowClick(row) {
this.goDetail(row)
},
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() {
this.historyLoading = true
try {
const params = {
pageNum: this.historyQuery.pageNum,
pageSize: this.historyQuery.pageSize,
bizType: this.historyQuery.type || undefined, // 业务类型leave/travel/seal/reimburse
status: this.historyQuery.status || undefined // 状态draft/running/approved/rejected
}
const res = await listMyFlowInstance(params)
if (res.code === 200) {
this.historyList = res.rows || []
this.historyTotal = res.total || 0
} else {
this.historyList = []
this.historyTotal = 0
this.$message.error(res.msg || '加载申请历史失败')
}
} catch (err) {
console.error('加载申请历史失败:', err)
this.$message.error('加载申请历史失败')
this.historyList = []
this.historyTotal = 0
} finally {
this.historyLoading = false
}
},
handleSizeChange(val) {
this.historyQuery.pageSize = val
this.historyQuery.pageNum = 1
this.loadHistory()
},
handlePageChange(val) {
this.historyQuery.pageNum = val
this.loadHistory()
}
}
}
</script>
<style lang="scss" scoped>
.hrm-page {
padding: 20px 24px 32px;
background: #f8f9fb;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.section-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.section-desc {
margin: 4px 0 0;
font-size: 13px;
color: #909399;
}
.section-actions {
display: flex;
align-items: center;
}
}
.apply-section {
margin-bottom: 32px;
}
.apply-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.apply-card {
cursor: pointer;
transition: all 0.3s;
border: 1px solid #e6e8ed;
border-radius: 12px;
padding: 20px;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: #409eff;
}
.card-content {
margin-bottom: 16px;
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 13px;
color: #909399;
line-height: 1.5;
}
}
.card-action {
display: flex;
justify-content: flex-end;
}
}
.history-section {
.history-card {
border: 1px solid #d7d9df;
border-radius: 10px;
background: #fff;
}
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
@media (max-width: 1600px) {
.apply-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.apply-cards {
grid-template-columns: 1fr;
}
}
</style>