Files
fad_oa/ruoyi-ui/src/views/hrm/myApply.vue

324 lines
11 KiB
Vue

<template>
<div class="my-apply-page">
<section class="stats-strip">
<el-card shadow="never" class="stat-card">
<div class="stat-label">总申请</div>
<div class="stat-value">{{ summary.total }}</div>
</el-card>
<el-card shadow="never" class="stat-card">
<div class="stat-label">审批中</div>
<div class="stat-value warning">{{ summary.running }}</div>
</el-card>
<el-card shadow="never" class="stat-card">
<div class="stat-label">已通过</div>
<div class="stat-value success">{{ summary.approved }}</div>
</el-card>
</section>
<section class="filters">
<el-card shadow="never" class="filter-card">
<div class="filter-row">
<el-select v-model="query.bizType" placeholder="申请类型" clearable size="small" style="width: 130px" @change="handleFilterChange">
<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-option label="拨款" value="appropriation" />
</el-select>
<el-select v-model="query.status" placeholder="状态" clearable size="small" style="width: 130px" @change="handleFilterChange">
<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-option label="已撤销" value="revoked" />
</el-select>
<el-input v-model="query.keyword" placeholder="输入申请标题/备注关键词" size="small" clearable style="width: 260px" @keyup.enter.native="handleFilterChange" />
<el-button type="primary" size="small" icon="el-icon-search" @click="handleFilterChange">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</div>
</el-card>
</section>
<section class="content">
<el-card shadow="never" class="table-card">
<div slot="header" class="card-header">
<span>申请列表</span>
<el-button size="mini" icon="el-icon-refresh" @click="loadList">刷新</el-button>
</div>
<el-table :data="list" v-loading="loading" stripe @row-dblclick="goDetail" empty-text="暂无申请记录">
<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="240" show-overflow-tooltip>
<template slot-scope="scope">
{{ getRowTitle(scope.row) }}
</template>
</el-table-column>
<el-table-column label="发起人" min-width="140" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.nickName || '-' }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="110">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row)" size="small">{{ statusText(scope.row) }}</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="180" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="goDetail(scope.row)">详情</el-button>
<el-button
v-if="showEarlyEndButton(scope.row)"
type="text"
size="mini"
style="color: #e6a23c"
:loading="earlyEndLoadingId === scope.row.bizId"
@click.stop="handleEarlyEnd(scope.row)"
>
提前结束
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
:current-page="query.pageNum"
:page-size="query.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</section>
</div>
</template>
<script>
import { listMyApply } from '@/api/hrm/myApply'
import { earlyEndTravel } from '@/api/hrm/travel'
import applyTypeMinix from '@/views/hrm/minix/applyTypeMinix.js'
export default {
name: 'HrmMyApply',
mixins: [applyTypeMinix],
data () {
return {
loading: false,
earlyEndLoadingId: null,
list: [],
total: 0,
summary: {
total: 0,
running: 0,
approved: 0
},
query: {
pageNum: 1,
pageSize: 50,
bizType: '',
status: '',
keyword: ''
}
}
},
created () {
this.loadList()
},
methods: {
getRowTitle (row) {
return row.title || row.remark || '-'
},
isTravelCompleted (row) {
if (!row || row.bizType !== 'travel') return false
const endTime = row.endTime ? new Date(row.endTime).getTime() : 0
const now = Date.now()
return Boolean(row.actualEndTime) || (endTime && endTime <= now)
},
showEarlyEndButton (row) {
if (!row || row.bizType !== 'travel') return false
if (row.actualEndTime) return false
const endTime = row.endTime ? new Date(row.endTime).getTime() : 0
return row.status === 'approved' && endTime > Date.now()
},
statusText (row) {
const status = row?.status
if (row?.bizType === 'travel' && status === 'approved') {
return this.isTravelCompleted(row) ? '已完成' : '进行中'
}
const map = { draft: '草稿', running: '审批中', pending: '审批中', approved: '已通过', rejected: '已驳回', revoked: '已撤销', finished: '已完成' }
return map[status] || status || '-'
},
statusType (row) {
const status = row?.status
if (row?.bizType === 'travel' && status === 'approved') {
return this.isTravelCompleted(row) ? 'success' : 'warning'
}
const map = { draft: 'info', running: 'warning', pending: 'warning', approved: 'success', rejected: 'danger', revoked: '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())}`
},
handleFilterChange () {
this.query.pageNum = 1
this.loadList()
},
resetQuery () {
this.query = { pageNum: 1, pageSize: 10, bizType: '', status: '', keyword: '' }
this.loadList()
},
async loadList () {
this.loading = true
try {
const res = await listMyApply({
pageNum: this.query.pageNum,
pageSize: this.query.pageSize,
bizType: this.query.bizType || undefined,
status: this.query.status || undefined,
keyword: this.query.keyword || undefined
})
this.list = res.rows || []
this.total = res.total || 0
this.summary.total = res.total || 0
this.summary.running = (res.rows || []).filter(i => ['running', 'pending'].includes(i.status) || (i.bizType === 'travel' && i.status === 'approved' && !this.isTravelCompleted(i))).length
this.summary.approved = (res.rows || []).filter(i => i.status === 'approved' && !(i.bizType === 'travel' && !this.isTravelCompleted(i))).length
} catch (err) {
console.error('加载我的申请失败:', err)
this.$message.error('加载我的申请失败')
this.list = []
this.total = 0
} finally {
this.loading = false
}
},
handleSizeChange (val) {
this.query.pageSize = val
this.query.pageNum = 1
this.loadList()
},
handleCurrentChange (val) {
this.query.pageNum = val
this.loadList()
},
async handleEarlyEnd (row) {
this.$confirm('确认提前结束本次出差吗?结束后的实际时间将记录为当前时间。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.earlyEndLoadingId = row.bizId
try {
await earlyEndTravel(row.bizId)
this.$message.success('提前结束成功')
await this.loadList()
} catch (error) {
this.$message.error(error.message || '提前结束失败')
} finally {
this.earlyEndLoadingId = null
}
}).catch(() => {})
},
goDetail (row) {
if (!row) return
const routeMap = {
leave: '/hrm/HrmLeaveDetail',
travel: '/hrm/HrmTravelDetail',
seal: '/hrm/HrmSealDetail',
reimburse: '/hrm/HrmReimburseDetail',
appropriation: '/hrm/HrmAppropriationDetail'
}
const path = routeMap[row.bizType]
if (!path) {
this.$message.warning('暂不支持该申请类型的详情页')
return
}
this.$router.push({ path, query: { bizId: row.bizId } })
}
}
}
</script>
<style lang="scss" scoped>
.my-apply-page {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.stats-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.stat-card {
border: 1px solid #e6e8ed;
border-radius: 10px;
background: #fff;
text-align: center;
}
.stat-label {
font-size: 12px;
color: #8a8f99;
}
.stat-value {
margin-top: 4px;
font-size: 22px;
font-weight: 800;
color: #2b2f36;
}
.warning { color: #e6a23c; }
.success { color: #67c23a; }
.filter-card,
.table-card {
border: 1px solid #d7d9df;
border-radius: 10px;
}
.filter-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 700;
color: #2b2f36;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
@media (max-width: 1200px) {
.stats-strip {
grid-template-columns: 1fr;
}
}
</style>