This commit is contained in:
2025-12-30 13:47:53 +08:00
parent f1637501b2
commit a623c5673f
137 changed files with 11031 additions and 4043 deletions

View File

@@ -0,0 +1,432 @@
<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="pending" />
<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.procDefKey)">{{ getTypeText(scope.row.procDefKey) }}</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">
{{ 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.procStatus)">{{ statusText(scope.row.procStatus) }}</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 } 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()
},
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 = { pending: '审批中', draft: '草稿', approved: '已通过', rejected: '已驳回', finished: '已完成' }
return map[status] || status || '-'
},
statusType(status) {
if (!status) return 'info'
const map = { pending: '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.businessKey) {
this.$message.warning('缺少businessKey无法打开详情')
return
}
const [type, bizId] = row.businessKey.split(':')
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/pending/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>