449 lines
14 KiB
Vue
449 lines
14 KiB
Vue
<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>
|