办公V3
This commit is contained in:
432
klp-ui/src/views/hrm/requests/apply.vue
Normal file
432
klp-ui/src/views/hrm/requests/apply.vue
Normal 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>
|
||||
Reference in New Issue
Block a user