Files
fad_oa/ruoyi-ui/src/views/hrm/requests/appropriationDetail.vue

570 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>
<BizDetailContainer :bizId="currentBizId" bizType="appropriation" :preview="preview">
<template slot-scope="{ detail }">
<div>
<!-- 拨款金额信息 -->
<div class="block-title">拨款金额信息</div>
<el-card class="inner-card" shadow="never">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="拨款总金额">
<span class="cost-text-large">{{ detail.amount ? '¥' + detail.amount : '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="拨款类型">{{ detail.appropriationType || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ formatDate(detail.createTime) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(detail.updateTime) }}</el-descriptions-item>
</el-descriptions>
</el-card>
<div class="block-title">费用与收款信息</div>
<el-card class="inner-card" shadow="never">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="收款人">{{ detail.payeeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="开户行">{{ detail.bankName || '-' }}</el-descriptions-item>
<el-descriptions-item label="银行账号">{{ detail.bankAccount || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 拨款单据 -->
<div class="block-title">拨款单据</div>
<el-card class="inner-card" shadow="never">
<div class="hint-text">请上传相关拨款单据发票收据凭证等</div>
<file-preview v-model="detail.accessoryApplyIds"></file-preview>
</el-card>
<!-- 拨款理由说明 -->
<div class="block-title">拨款理由说明</div>
<el-card class="inner-card" shadow="never">
<div class="reason-section">
<div class="reason-label">拨款事由</div>
<div class="reason-content">{{ detail.reason || '未填写' }}</div>
</div>
<div v-if="detail.remark" class="reason-section">
<div class="reason-label">备注说明</div>
<div class="reason-content">{{ detail.remark }}</div>
</div>
</el-card>
</div>
</template>
</BizDetailContainer>
</template>
<script>
import { getAppropriationReq, listEmployee } from '@/api/hrm';
import { approveFlowTask, listAssignTask, listFlowAction, queryInstanceByBiz, rejectFlowTask, withdrawFlowTask } from '@/api/hrm/flow';
import { listByIds } from '@/api/system/oss';
import FilePreview from "@/components/FilePreview/index.vue";
import BizDetailContainer from "@/views/hrm/components/BizDetailContainer/index.vue";
export default {
name: 'AppropriationDetail',
props: {
bizId: { type: [String, Number], default: null },
embedded: { type: Boolean, default: false },
preview: { type: Boolean, default: false }
},
components: {
FilePreview,
BizDetailContainer
},
data () {
return {
loading: false,
actionLoading: false,
detail: {
bizId: null,
empId: null,
totalAmount: null,
reimburseType: null,
reason: null,
remark: null,
accessoryApplyIds: null,
status: 'draft',
createTime: null,
updateTime: null
},
employees: [],
currentTask: null,
flowHistory: [],
approveForm: {
comment: ''
},
attachmentList: [],
attachmentLoading: false,
assignTasks: [],
flowInstance: {}
}
},
computed: {
currentBizId () {
return this.bizId || this.$route?.params?.bizId || this.$route?.query?.bizId || this.$route?.params?.id
},
applicantText () {
const empId = this.detail.empId
const emp = this.employees.find(e => String(e.empId) === String(empId))
if (emp) {
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()
}
return empId ? `员工ID:${empId}` : '-'
},
canApprove () {
// 只有待审批状态且是当前用户待审批的才能审批
console.log(this.currentTask, this.$store.getters.name, this.$store.getters.id)
return this.detail.status === 'pending' && (this.currentTask?.assigneeUserName === this.$store.getters.name || this.currentTask?.assigneeUserId === this.$store.getters.id)
},
canWithdraw () {
return false
}
},
created () {
this.loadEmployees()
this.loadDetail()
},
methods: {
statusText (status) {
const statusMap = {
'draft': '草稿',
'pending': '审批中',
'approved': '已通过',
'rejected': '已驳回',
'withdrawn': '已撤回',
'revoked': '已撤销'
}
return statusMap[status] || status || '未知'
},
statusType (status) {
const typeMap = {
'draft': 'info',
'pending': 'warning',
'approved': 'success',
'rejected': 'danger',
'withdrawn': 'info',
'revoked': 'danger'
}
return typeMap[status] || 'info'
},
loadEmployees () {
listEmployee({ pageNum: 1, pageSize: 1000 }).then(res => {
this.employees = res.rows || res.data || []
})
},
async loadDetail () {
const bizId = this.currentBizId
if (!bizId) return
this.loading = true
try {
// 调用拨款详情接口
const res = await getAppropriationReq(bizId)
this.detail = res.data || {}
// 加载流程实例信息
await this.loadFlowInstance()
await Promise.all([
this.loadCurrentTask(),
this.loadFlowHistory(),
this.loadAttachments()
])
} catch (error) {
console.error('加载详情失败:', error)
this.$message.error('加载详情失败')
} finally {
this.loading = false
}
},
async loadFlowInstance () {
if (!this.detail.instId) {
// 如果没有instId尝试通过bizType和bizId查询
try {
const res = await queryInstanceByBiz('appropriation', this.currentBizId)
const instances = res.data || []
if (instances.length > 0) {
this.flowInstance = instances[0]
this.detail.instId = instances[0].instId
// 加载流程节点信息
// await this.loadFlowNodes()
// 根据当前节点ID查找节点信息
// if (this.flowInstance.currentNodeId) {
// this.currentNode = this.flowNodes.find(n => n.nodeId === this.flowInstance.currentNodeId) || null
// }
}
} catch (e) {
console.error('加载流程实例失败:', e)
}
} else {
// 如果有instId直接加载
try {
const res = await getFlowInstance(this.detail.instId)
this.flowInstance = res.data || null
// 加载流程节点信息
await this.loadFlowNodes()
// 根据当前节点ID查找节点信息
if (this.flowInstance && this.flowInstance.currentNodeId) {
this.currentNode = this.flowNodes.find(n => n.nodeId === this.flowInstance.currentNodeId) || null
}
} catch (e) {
console.error('加载流程实例失败:', e)
}
}
},
async loadCurrentTask () {
const bizId = this.currentBizId
if (!bizId) return null
try {
const res = await listAssignTask(this.flowInstance.instId)
this.assignTasks = res?.data || []
} catch (error) {
this.assignTasks = []
}
},
async loadFlowHistory () {
const instId = this.detail?.instId
if (!instId) {
this.flowHistory = []
return
}
try {
const res = await listFlowAction({ instId, pageNum: 1, pageSize: 200 })
this.flowHistory = res.rows || res.data || []
} catch (error) {
this.flowHistory = []
}
},
async loadAttachments () {
const fileIds = this.detail.accessoryApplyIds || this.detail.applyFileIds
if (!fileIds) {
this.attachmentList = []
return
}
const ids = String(fileIds).split(',').map(id => id.trim()).filter(Boolean)
if (ids.length === 0) {
this.attachmentList = []
return
}
this.attachmentLoading = true
try {
const res = await listByIds(ids)
this.attachmentList = res.data || []
} catch (e) {
this.$message.error('加载附件失败:' + (e.message || '未知错误'))
this.attachmentList = []
} finally {
this.attachmentLoading = false
}
},
formatFileSize (bytes) {
if (!bytes) return '-'
const units = ['B', 'KB', 'MB', 'GB']
let size = Number(bytes)
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
},
previewFile (file) {
if (file.url) {
window.open(file.url, '_blank')
} else {
this.$message.warning('文件URL不存在')
}
},
downloadFile (ossId) {
window.open(`/system/oss/download/${ossId}`, '_blank')
},
async handleApprove () {
await this.handleAction('approve', '通过')
},
async handleReject () {
await this.handleAction('reject', '驳回')
},
async handleWithdraw () {
await this.handleAction('withdraw', '撤回')
},
async handleAction (action, actionName) {
if (!this.currentTask?.taskId) {
this.$message.warning('未找到待办任务')
return
}
try {
await this.$confirm(`确定${actionName}该申请吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
this.actionLoading = true
const payload = { remark: this.approveForm.comment }
if (action === 'approve') {
await approveFlowTask(this.currentTask.taskId, payload)
} else if (action === 'reject') {
await rejectFlowTask(this.currentTask.taskId, payload)
} else if (action === 'withdraw') {
await withdrawFlowTask(this.currentTask.taskId, payload)
}
this.$message.success(`${actionName}成功`)
await this.loadDetail()
} catch (error) {
if (error !== 'cancel') {
console.error(`${actionName}失败:`, error)
this.$message.error(error.message || `${actionName}失败`)
}
} finally {
this.actionLoading = false
}
},
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())}`
},
getActionText (action) {
const map = {
'submit': '提交申请',
'approve': '通过',
'reject': '驳回',
'withdraw': '撤回',
'cancel': '取消',
'revvoke': '撤销'
}
return map[action] || action
},
getTimelineType (action) {
const map = {
'submit': 'primary',
'approve': 'success',
'reject': 'danger',
'withdraw': 'info',
'cancel': 'info',
'revoke': 'danger'
}
return map[action] || 'info'
}
}
}
</script>
<style lang="scss" scoped>
.request-page {
padding: 16px 20px 32px;
background: #f8f9fb;
}
.form-card {
max-width: 980px;
margin: 0 auto;
border: 1px solid #d7d9df;
border-radius: 12px;
background: #ffffff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 700;
color: #2b2f36;
}
.actions {
display: flex;
gap: 8px;
}
.detail-loading {
min-height: 300px;
}
.form-summary {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 16px;
border: 1px solid #e6e8ed;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%);
}
.summary-title {
font-size: 16px;
font-weight: 800;
color: #2b2f36;
}
.summary-sub {
margin-top: 4px;
font-size: 12px;
color: #8a8f99;
}
.summary-right {
display: flex;
gap: 16px;
}
.summary-item .k {
font-size: 12px;
color: #8a8f99;
}
.summary-item .v {
margin-top: 2px;
font-weight: 700;
color: #2b2f36;
}
.cost-text {
font-weight: 700;
color: #e6a23c;
font-size: 16px;
}
.cost-text-large {
font-weight: 800;
color: #e6a23c;
font-size: 20px;
}
.block-title {
margin: 16px 0 8px;
padding-left: 10px;
font-weight: 700;
color: #2f3440;
border-left: 3px solid #9aa3b2;
}
.hint-text {
margin: 6px 0 10px;
font-size: 12px;
color: #8a8f99;
}
.inner-card {
border: 1px solid #e6e8ed;
margin-bottom: 12px;
}
.reason-section {
margin-bottom: 16px;
}
.reason-section:last-child {
margin-bottom: 0;
}
.reason-label {
font-size: 13px;
font-weight: 600;
color: #606266;
margin-bottom: 8px;
}
.reason-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #2b2f36;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.approver-info {
display: flex;
gap: 24px;
padding: 12px 0;
}
.approver-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.approver-label {
font-size: 12px;
color: #8a8f99;
}
.approver-value {
font-weight: 600;
color: #2b2f36;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.attachment-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid #e6e8ed;
border-radius: 8px;
background: #fafbfc;
}
.file-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.file-icon {
font-size: 24px;
color: #9aa3b2;
}
.file-details {
flex: 1;
}
.file-name {
font-weight: 600;
color: #2b2f36;
margin-bottom: 4px;
}
.file-meta {
font-size: 12px;
color: #8a8f99;
display: flex;
gap: 12px;
}
.file-time {
margin-left: 8px;
}
.file-actions {
display: flex;
gap: 8px;
}
.empty {
color: #a0a3ad;
font-size: 13px;
padding: 10px 4px;
text-align: center;
}
@media (max-width: 1200px) {
.summary-right {
display: none;
}
}
</style>