You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1541 lines
49 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<el-drawer
:visible.sync="drawerVisible"
:size="drawerSize"
:with-header="true"
:modal="false"
:show-close="showCloseButton"
:before-close="handleClose"
direction="rtl"
custom-class="contract-detail-drawer"
:title="detail?detail.name:'合同详情'"
>
<div class="main-container-over">
<div class="content-area">
<!-- 顶部完整流程概览 -->
<div class="overview-container">
<div class="overview-title">
<i class="fas fa-sitemap" />
流程概览
</div>
<div class="overview-section">
<div class="overview-flow">
<div id="overview-1" :class="['overview-step', getOverviewStepClassForSh()]">
<div class="overview-icon"><i class="fas fa-handshake" /></div>
<div>资金使用上会</div>
<div class="overview-number">1</div>
</div>
<div class="overview-arrow"><i class="fas fa-arrow-right" /></div>
<div id="overview-2" :class="['overview-step', getOverviewStepClassForProcurement()]">
<div class="overview-icon"><i class="fas fa-shopping-cart" /></div>
<div>采购/事前流程</div>
<div class="overview-number">2</div>
</div>
<div class="overview-arrow"><i class="fas fa-arrow-right" /></div>
<div id="overview-3" :class="['overview-step', getOverviewStepClassForTender()]">
<div class="overview-icon"><i class="fas fa-gavel" /></div>
<div>招标审查</div>
<div class="overview-number">3</div>
</div>
<div class="overview-arrow"><i class="fas fa-arrow-right" /></div>
<div id="overview-4" :class="['overview-step', getOverviewStepClassForContractSign()]">
<div class="overview-icon"><i class="fas fa-file-contract" /></div>
<div>合同审批签订</div>
<div class="overview-number">4</div>
</div>
<div class="overview-arrow"><i class="fas fa-arrow-right" /></div>
<div class="payment-group-container">
<div id="overview-5" class="overview-step payment">
<div class="overview-icon"><i class="fas fa-credit-card" /></div>
<div>支付审批上会</div>
<div class="overview-number">5</div>
</div>
<div id="overview-6" class="overview-step payment">
<div class="overview-icon"><i class="fas fa-paper-plane" /></div>
<div>创建付款记录</div>
<div class="overview-number">6</div>
</div>
<div id="overview-7" class="overview-step payment">
<div class="overview-icon"><i class="fas fa-tasks" /></div>
<div>支付审批流转</div>
<div class="overview-number">7</div>
</div>
<div id="overview-8" class="overview-step payment">
<div class="overview-icon"><i class="fas fa-check-circle" /></div>
<div>财务付款确认</div>
<div class="overview-number">8</div>
</div>
</div>
<div class="overview-rounds-info">
<span class="badge bg-secondary">{{ (fundLogs && fundLogs.length > 0) ? ('已发起' + fundLogs.length + '轮') : '未发起' }}</span>
</div>
</div>
</div>
</div>
<!-- 列支信息 -->
<div class="expense-info-container">
<div class="expense-info-title">
<i class="fas fa-receipt" />
列支信息
</div>
<div class="expense-info-section">
<div class="expense-info-content">
<div class="expense-info-item">
<span class="expense-info-label">项目名称</span>
<span class="expense-info-value">{{ detail && detail.name ? detail.name : '-' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">所属科室</span>
<span class="expense-info-value">{{ detail && detail.department ? detail.department.name : '-' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">经办人</span>
<span class="expense-info-value">{{ detail && detail.admin ? detail.admin.name : '-' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">预算计划</span>
<span class="expense-info-value">
<div v-if="detail && detail.plans && detail.plans.length > 0">
<div
v-for="item in detail.plans"
:key="item.id"
class="plan-item"
>
[{{ item.year || '-' }}] {{ item.pid_info ? item.pid_info.name : '' }} - {{ item.name || '-' }}
</div>
</div>
<div v-else>-</div>
</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">项目金额(元)</span>
<span class="expense-info-value">{{ detail && detail.total_money ? moneyFormat(detail.total_money) : '0.00' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">本年预算金额(元)</span>
<span class="expense-info-value">{{ detail && detail.plan_price ? moneyFormat(detail.plan_price) : '0.00' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">次年预算金额(元)</span>
<span class="expense-info-value">{{ detail && detail.next_year_ ? moneyFormat(detail.plan_price) : '0.00' }}</span>
</div>
<div class="expense-info-item">
<span class="expense-info-label">已确认金额</span>
<span class="expense-info-value">{{confirmedAmount}}</span>
</div>
<!-- <div class="expense-info-item">
<span class="expense-info-label">当前状态</span>
<span class="expense-info-value">
<span class="expense-status">已完成</span>
</span>
</div> -->
</div>
</div>
</div>
<!-- 环节详情展开 -->
<div class="detail-section">
<div class="detail-title">
<i class="fas fa-list-ul" />
信息链详情
</div>
<!-- 环节1资金使用上会 -->
<div id="detail-1" :class="['step-detail', getOverviewStepClassForSh()]">
<div class="step-header">
<div class="step-title">
<div class="step-icon">
<i class="fas fa-handshake" />
</div>
<div>资金使用上会</div>
</div>
<div v-if="shOaView" :class="['step-status', getOverviewStepClassForSh()]">{{ shOaView? shOaView.status_text : '-' }}</div>
</div>
<template v-if="shOaView">
<div class="step-content">
<div class="step-info">
<div class="info-item">
<span class="info-label">工作名称</span>
<span class="info-value">{{ shOaView.title? shOaView.title : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请人</span>
<span class="info-value">{{ shOaView.creator? shOaView.creator.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请部门</span>
<span class="info-value">{{ shOaView.creator_department? shOaView.creator_department.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请时间</span>
<span class="info-value">{{ shOaView.create_at }}</span>
</div>
<div class="info-item">
<span class="info-label">审批状态</span>
<span class="info-value">{{ shOaView.status_text? shOaView.status_text : '-' }}</span>
</div>
</div>
</div>
</template>
<template v-else>
<div class="step-content">
暂无资金上会流程
</div>
</template>
</div>
<!-- 环节2采购/事前流程 -->
<div id="detail-2" :class="['step-detail', getStepClassForProcurement()]">
<div class="step-header">
<div class="step-title">
<div class="step-icon">
<i class="fas fa-shopping-cart" />
</div>
<div>采购/事前流程</div>
</div>
<div v-if="procurementStatus.label" :class="['step-status', procurementStatus.class]">{{ procurementStatus.label }}</div>
</div>
<div class="step-content">
<template v-if="procurementFlows && procurementFlows.length > 0">
<div class="flow-block" v-for="(flow, idx) in procurementFlows" :key="flow.id || idx">
<div class="flow-title">{{ flow.custom_model_name || '-' }}</div>
<div class="step-info">
<div class="info-item">
<span class="info-label">流程名称</span>
<span class="info-value">{{ flow.flow_title || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起日期</span>
<span class="info-value">{{ flow.created_at || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">
<span :class="['step-status', getStatusClass(flow.flow_status)]">{{ getStatusLabel(flow.flow_status) }}</span>
</span>
</div>
</div>
</div>
</template>
<template v-else>
<div class="step-content">暂无采购/事前流程</div>
</template>
</div>
</div>
<!-- 环节3招标审查 -->
<div id="detail-3-tender" :class="['step-detail', getStepClassForTender()]">
<div class="step-header">
<div class="step-title">
<div class="step-icon">
<i class="fas fa-gavel" />
</div>
<div>招标审查</div>
</div>
<div v-if="tenderStatusText" :class="['step-status', getStepClassForTender()]">{{ tenderStatusText }}</div>
</div>
<div class="step-content">
<template v-if="detail && detail.is_tender_audit === 0">
<div class="step-content">
无需招标审查
</div>
</template>
<template v-else-if="detail && detail.tender && detail.tender.length > 0">
<div class="step-info">
<div class="info-item" v-for="(file, index) in detail.tender" :key="index">
<span class="info-label">文件</span>
<span class="info-value">
<a :href="file.url" target="_blank" rel="noopener">{{ file.original_name || '附件' }}</a>
<span class="action-link" @click.stop="open(file.url)">预览</span>
<span class="action-link" @click.stop="download(file.id)">下载</span>
</span>
</div>
</div>
</template>
<template v-else>
<div class="step-content">
暂无招标审查文件
</div>
</template>
</div>
</div>
<!-- 环节4合同签订 -->
<div id="detail-3" :class="['step-detail', getStepClassForContractSign()]">
<div class="step-header">
<div class="step-title">
<div class="step-icon">
<i class="fas fa-file-contract" />
</div>
<div>合同审批签订</div>
</div>
<div v-if="contractSignStatus.label" :class="['step-status', contractSignStatus.class]">{{ contractSignStatus.label }}</div>
</div>
<div class="step-content">
<div class="step-info">
<template v-if="contractApprovalFlows && contractApprovalFlows.length">
<div class="subsection-title">合同审批流程</div>
<div class="flow-block compact" v-for="(flow, idx) in contractApprovalFlows" :key="'c-'+(flow.id || idx)">
<div class="info-item">
<span class="info-label">流程名称</span>
<span class="info-value">{{ flow.flow_title || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起日期</span>
<span class="info-value">{{ flow.created_at || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">
<span :class="['step-status', getStatusClass(flow.flow_status)]">{{ getStatusLabel(flow.flow_status) }}</span>
</span>
</div>
</div>
</template>
<template v-else-if="detail && detail.is_contract">
<div class="subsection-title-with-status">
<div class="subsection-title">合同审批流程</div>
<div class="step-status pending">待申请</div>
</div>
<div class="flow-empty">暂无合同审批流程</div>
</template>
<div class="info-item">
<span class="info-label">合同编号</span>
<span class="info-value">{{ detail && detail.number ? detail.number : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">供应商</span>
<span class="info-value">{{ detail && detail.supply ? detail.supply: '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">合同金额</span>
<span class="info-value">{{ detail && detail.money ? moneyFormat(detail.money) : '0.00' }}</span>
</div>
<div class="info-item">
<span class="info-label">签订时间</span>
<span class="info-value">{{ detail && detail.date ? detail.date : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- 付款轮次详情v-for 渲染) -->
<div v-for="(round, roundIndex) in fundLogs" :key="round.id || roundIndex" :id="`detail-round-${roundIndex+1}`" class="step-detail" :class="getRoundClass(round)">
<div class="step-header">
<div class="step-title">
<div class="step-icon">
<i class="fas fa-credit-card" />
</div>
<div>付款轮次 {{ roundIndex + 1 }} <span v-if="round && round.is_end === 1" class="badge bg-danger ms-2">最后一轮</span></div>
</div>
<div class="step-status" :class="getRoundClass(round)">{{ getRoundStatusText(round) }}</div>
</div>
<!-- 环节5支付审批上会 -->
<div class="round-step-detail-section">
<div class="round-step-header">
<div class="round-step-title">
<i class="fas fa-credit-card me-2" />
支付审批上会
</div>
<div v-if="paymentMeetingFlow(roundIndex)" :class="['round-step-status', getStatusClass(paymentMeetingFlow(roundIndex).status)]">{{ getStatusLabel(paymentMeetingFlow(roundIndex).status) }}</div>
</div>
<div class="step-content">
<template v-if="paymentMeetingFlow(roundIndex)">
<div class="step-info">
<div class="info-item">
<span class="info-label">流程名称</span>
<span class="info-value">{{ paymentMeetingFlow(roundIndex).title || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起日期</span>
<span class="info-value">{{ paymentMeetingFlow(roundIndex).created_at || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起人</span>
<span class="info-value">{{ paymentMeetingFlow(roundIndex).creator && paymentMeetingFlow(roundIndex).creator.name ? paymentMeetingFlow(roundIndex).creator.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起部门</span>
<span class="info-value">{{ paymentMeetingFlow(roundIndex).creator_department && paymentMeetingFlow(roundIndex).creator_department.name ? paymentMeetingFlow(roundIndex).creator_department.name : '-' }}</span>
</div>
</div>
</template>
<template v-else>
<div class="step-info">暂无支付审批上会流程</div>
</template>
</div>
</div>
<!-- 环节6创建付款记录 -->
<div class="round-step-detail-section">
<div class="round-step-header">
<div class="round-step-title">
<i class="fas fa-paper-plane me-2" />
创建付款记录
</div>
<div class="round-step-status completed">已完成</div>
</div>
<div class="step-content">
<div class="step-info">
<div class="info-item">
<span class="info-label">申请付款金额</span>
<span class="info-value">{{ round && round.apply_money ? moneyFormat(round.apply_money) : '0.00' }}</span>
</div>
<div class="info-item">
<span class="info-label">款项类型</span>
<span class="info-value">{{ round && round.type ? round.type : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请时间</span>
<span class="info-value">{{ round && round.created_at ? round.created_at : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请人</span>
<span class="info-value">{{ round && round.admin && round.admin.name ? round.admin.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">申请部门</span>
<span class="info-value">{{ round && round.department && round.department.name ? round.department.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">备注</span>
<span class="info-value">{{ round && round.remark ? round.remark : '-' }}</span>
</div>
</div>
</div>
</div>
<!-- 环节7付款审批流转 -->
<div class="round-step-detail-section">
<div class="round-step-header">
<div class="round-step-title">
<i class="fas fa-tasks me-2" />
支付审批流转
</div>
<div :class="['round-step-status', flowLinksHeaderStatus(roundIndex).class]">{{ flowLinksHeaderStatus(roundIndex).label }}</div>
</div>
<div class="step-content">
<template v-if="flowLinksOfRound(roundIndex).length">
<div class="step-info" v-for="(flow, idx2) in flowLinksOfRound(roundIndex)" :key="flow.id || idx2">
<div class="info-item">
<span class="info-label">流程名称</span>
<span class="info-value">{{ flow.flow_title || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">发起时间</span>
<span class="info-value">{{ flow.created_at || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">
<span :class="['step-status', getStatusClass(flow.flow_status)]">{{ getStatusLabel(flow.flow_status) }}</span>
</span>
</div>
</div>
</template>
<template v-else>
<div class="step-info">暂无付款审批流转流程</div>
</template>
</div>
</div>
<!-- 环节8财务付款确认 -->
<div class="round-step-detail-section">
<div class="round-step-header">
<div class="round-step-title">
<i class="fas fa-check-circle me-2" />
财务付款确认
</div>
<div :class="['round-step-status', (round && round.status === 1) ? 'completed' : 'pending']">{{ (round && round.status === 1) ? '已审核' : '待审核' }}</div>
</div>
<div class="step-content">
<template v-if="round && round.status === 1">
<div class="step-info">
<div class="info-item">
<span class="info-label">审核时间</span>
<span class="info-value">{{ round.updated_at || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">审核人</span>
<span class="info-value">{{ round.audit_admin && round.audit_admin.name ? round.audit_admin.name : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">审核人部门</span>
<span class="info-value">{{ round.audit_admin && round.audit_admin.department && round.audit_admin.department.name ? round.audit_admin.department.name : '-' }}</span>
</div>
</div>
</template>
<template v-else>
<div class="step-info">
{{ flowLinksHeaderStatus(roundIndex).label === '待申请' ? '暂未发起支付审批' : '支付审批流程流转中' }}
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
<Modal :z-index="9999999" v-model="showModal" :width="76" transfer :footer-hide="true" title="预览">
<template>
<iframe style="width: 100%; height: 57vh" :src="codeUri" border="0" />
</template>
</Modal>
</el-drawer>
</template>
<script>
import { detailContract } from '@/api/contract/contract'
import { getContractSign } from '@/api/contractSign/contractSign'
import { moneyFormatter } from '@/utils'
import { getToken } from '@/utils/auth'
import { getOaView } from '@/api/oatoken'
import {
getFundLog
} from '@/api/paymentRegistration/fundLog'
export default {
name: 'ContractDetailDrawer',
props: {
visible: {
type: Boolean,
default: false
},
contractData: {
type: Object,
default: null
}
},
data() {
return {
// 合同详情
id: '',
detail: null,
shOaView: null,
signPlan: [],
loading: false,
showModal: false,
codeUri: '',
fundLogs: [],
// 合同流程分类结果
procurementFlows: [],
contractApprovalFlows: [],
paymentApprovalFlows: [],
paymentSteps: [
{ id: 5, name: '支付审批上会', icon: 'fas fa-credit-card', desc: '5万以上支付审批流程' },
{ id: 6, name: '创建付款记录', icon: 'fas fa-paper-plane', desc: '创建付款申请单' },
{ id: 7, name: '付款审批流转', icon: 'fas fa-tasks', desc: '付款申请的审批和处理流程' },
{ id: 8, name: '财务付款确认', icon: 'fas fa-check-circle', desc: '财务部门最终确认付款' }
],
// 状态列表:-1 退回0 办理中1 已办结
statusList: [
{ value: -1, label: '退回', class: 'rejected' },
{ value: 0, label: '办理中', class: 'active' },
{ value: 1, label: '已办结', class: 'completed' }
]
}
},
computed: {
// 路由模式下(含 out_contract_id强制显示抽屉组件模式下走父级的 visible
drawerVisible: {
get() {
return !!this.$route.query.out_contract_id || this.visible
},
set(val) {
// 路由模式不响应关闭(已隐藏关闭按钮)
if (this.$route.query.out_contract_id) return
this.$emit('update:visible', val)
}
},
// 合同签订头部状态(简化口径):
// - 不需要签订合同:显示“无需合同签订”
// - 需要签订且未签订:显示“合同待签订”
// - 已签订:显示“已签订”
contractSignStatus() {
const d = this.detail || {}
if (!d.is_contract) return { label: '无需合同签订', class: 'pending' }
if (d.status === 2) return { label: '已签订', class: 'completed' }
return { label: '合同待签订', class: 'pending' }
},
// 招标审查文案is_tender_audit === 0 -> 空(不显示徽标);否则根据附件判断
tenderStatusText() {
if (this.detail && this.detail.is_tender_audit === 0) return ''
const files = this.detail && this.detail.tender
if (files && files.length > 0) return '已上传'
return '待上传'
},
// 已确认金额fundLogs中status=1的act_money之和
confirmedAmount() {
if (!this.fundLogs || !this.fundLogs.length) return 0
const total = this.fundLogs
.filter(item => item.status === 1)
.reduce((sum, item) => sum + (parseFloat(item.act_money) || 0), 0)
return total.toFixed(2)
},
// 采购/事前流程头部状态计算(与列表页按钮口径一致):
// - 无任何流程数据且需要走采购流程 => 显示“待申请”
// 需要采购的判定detail.is_purchase 为真,且
// a) 一般采购 is_common_purchase 为真;或
// b) 非一般采购,且非简易 is_simple !== 1且非代建 !is_substitute
// - 有流程数据:全部 flow_status===1 => 已办结;否则 办理中
procurementStatus() {
const d = this.detail || {}
const flows = Array.isArray(this.procurementFlows) ? this.procurementFlows : []
if (!flows.length) {
if (d.is_purchase) {
const isGeneral = !!d.is_common_purchase
const needFormal = !d.is_common_purchase && d.is_simple !== 1 && !d.is_substitute
if (isGeneral || needFormal) {
return { label: '待申请', class: 'pending' }
}
}
return { label: '', class: '' }
}
const allDone = flows.every(f => f && f.flow_status === 1)
return allDone ? { label: '已办结', class: 'completed' } : { label: '办理中', class: 'active' }
},
// 根据URL参数决定drawer大小
drawerSize() {
return this.$route.query.out_contract_id ? '100%' : '60%'
},
// 根据URL参数决定是否显示关闭按钮
showCloseButton() {
return !this.$route.query.out_contract_id
}
},
watch: {
showModal(val) {
if (!val) this.codeUri = ''
},
contractData: {
handler(newVal) {
if (newVal && this.visible) {
this.detail = newVal
}
},
immediate: true
},
// 监听路由变化
'$route.query.out_contract_id': {
handler(newVal, oldVal) {
if (newVal && newVal !== oldVal) {
console.log('路由参数变化检测到out_contract_id:', newVal)
// 重置组件状态并重新加载数据
this.resetAndLoadFromUrl()
}
},
immediate: true
}
},
methods: {
// 付款轮次:获取“支付审批上会”的流程数据(按轮次索引)
paymentMeetingFlow(roundIndex) {
const list = Array.isArray(this.fundLogs) ? this.fundLogs : []
const item = list[roundIndex]
if (!item || !item.meeting_flow) return null
return item.meeting_flow
},
// 付款审批流转:取当前轮次的 flow_links 数组
flowLinksOfRound(roundIndex) {
const list = Array.isArray(this.fundLogs) ? this.fundLogs : []
const item = list[roundIndex]
const links = item && Array.isArray(item.fund_log_flow_links) ? item.fund_log_flow_links : []
return links
},
// 付款审批流转:头部状态
flowLinksHeaderStatus(roundIndex) {
const links = this.flowLinksOfRound(roundIndex)
if (!links.length) {
return { label: '待申请', class: 'pending' }
}
const allDone = links.every(l => l && l.flow_status === 1)
return allDone ? { label: '已办结', class: 'completed' } : { label: '办理中', class: 'active' }
},
handleClose() {
this.$emit('update:visible', false)
// 重置组件状态
this.resetComponent()
},
// 重置组件状态
resetComponent() {
this.id = ''
this.detail = null
this.signPlan = []
this.loading = false
},
// 根据URL参数重置并加载数据
resetAndLoadFromUrl() {
this.resetComponent()
this.checkUrlAndLoadData()
},
// 重新加载数据
retryLoad() {
if (this.id) {
this.getDetail(this.id)
} else if (this.contractData) {
this.detail = this.contractData
}
},
// 检查URL参数并加载数据
checkUrlAndLoadData() {
const outContractId = this.$route.query.out_contract_id
console.log('检查URL参数 - out_contract_id:', outContractId, '当前detail:', this.detail)
if (outContractId && !this.detail) {
console.log('检测到out_contract_id参数开始获取合同详情:', outContractId)
// 如果有out_contract_id且没有数据直接获取合同详情
this.getDetail(outContractId)
} else if (outContractId && this.detail) {
console.log('已有合同数据,无需重复获取')
} else {
console.log('未检测到out_contract_id参数')
}
},
// 获取合同详情
async getDetail(id) {
try {
// 防止重复请求相同ID且正在加载时直接返回
if (this.loading && id === this.id) {
console.log('忽略重复 getDetail 调用: ', id)
return
}
this.loading = true
this.id = id
const res = await detailContract({
id: id
})
this.detail = res
// 注释掉的代码已移除直接使用res.plans
console.log('合同详情:', this.detail)
// 获取上会详情
if (this.detail.meeting_flow_mod_id) {
this.getShOaView(this.detail.sh_id)
}
// 获取合同签订计划
// const plan = await getContractSign({
// contract_id: id
// })
// this.signPlan = plan.data
// 根据合同流程进行分类
this.categorizeContractFlows(this.detail && this.detail.contract_flow_links)
// 获取付款记录
await this.getFundLogs(id)
} catch (error) {
console.error('获取合同详情失败:', error)
this.$message.error('获取合同详情失败')
} finally {
this.loading = false
}
},
// 将合同的流程链接按业务分类
categorizeContractFlows(flowLinks) {
const flows = Array.isArray(flowLinks) ? flowLinks : []
const procurement = []
const contractApprove = []
const paymentApprove = []
flows.forEach((flow) => {
const modelId = flow && flow.custom_model_id
if (modelId === 72) {
contractApprove.push(flow)
} else if (modelId === 75) {
paymentApprove.push(flow)
} else {
procurement.push(flow)
}
})
this.procurementFlows = procurement
this.contractApprovalFlows = contractApprove
this.paymentApprovalFlows = paymentApprove
console.log('流程分类: 采购/事前', this.procurementFlows, '合同审批', this.contractApprovalFlows, '支付审批', this.paymentApprovalFlows)
},
// 获取付款记录
async getFundLogs(contractId) {
try {
const res = await getFundLog({
page: 1,
page_size: 999,
contract_id: contractId,
sort_type: 'asc',
sort_name: 'is_end'
})
// 兼容不同返回结构
console.log("123r",res)
this.fundLogs = res.data
console.log('付款记录:', this.fundLogs)
} catch (e) {
console.error('获取付款记录失败:', e)
}
},
async getShOaView(id) {
const res = await getOaView({
id: id
})
this.shOaView = res.data.flow
},
// 概览/详情-合同签订状态类名(结合是否需要合同签订与审批完成)
getOverviewStepClassForContractSign() {
const s = this.contractSignStatus
return s && s.class ? s.class : 'pending'
},
getStepClassForContractSign() {
const s = this.contractSignStatus
return s && s.class ? s.class : 'pending'
},
// 概览-招标审查状态is_tender_audit === 0 -> pending无需审查不高亮否则根据附件
getOverviewStepClassForTender() {
if (this.detail && this.detail.is_tender_audit === 0) return 'pending'
const files = this.detail && this.detail.tender
if (files && files.length > 0) return 'completed'
return 'pending'
},
// 详情块-招标审查状态is_tender_audit === 0 -> pending否则根据附件
getStepClassForTender() {
if (this.detail && this.detail.is_tender_audit === 0) return 'pending'
const files = this.detail && this.detail.tender
if (files && files.length > 0) return 'completed'
return 'pending'
},
getOverviewStepClassForSh() {
if (!this.shOaView) return 'pending'
const status = this.shOaView.status
if (status === -1) return 'rejected'
if (status === 0) return 'active'
if (status === 1) return 'completed'
return 'pending'
},
// 概览-采购/事前流程状态类名
getOverviewStepClassForProcurement() {
const s = this.procurementStatus
return s && s.class ? s.class : 'pending'
},
// 详情-采购/事前流程状态类名
getStepClassForProcurement() {
const s = this.procurementStatus
return s && s.class ? s.class : 'pending'
},
// 状态文本
getStatusLabel(status) {
const item = this.statusList.find(s => s.value === status)
return item ? item.label : '办理中'
},
// 状态类名
getStatusClass(status) {
const item = this.statusList.find(s => s.value === status)
return item ? item.class : 'active'
},
// 概览-招标审查状态:有文件 completed否则 pending
getOverviewStepClassForTender() {
const files = this.detail && this.detail.tender
if (files && files.length > 0) return 'completed'
return 'pending'
},
// 详情块-招标审查状态:有文件 completed否则 pending
getStepClassForTender() {
const files = this.detail && this.detail.tender
if (files && files.length > 0) return 'completed'
return 'pending'
},
showPaymentStepInfo(roundId, stepId) {
const step = this.paymentSteps.find(s => s.id === stepId)
console.log(`轮次${roundId} - ${step.name}${step.desc}`)
},
// 类型格式化
typeFormatter(type) {
switch (type) {
case 1:
return '服务'
case 2:
return '货物'
case 3:
return '工程'
case 4:
return '其他'
default:
return '未知'
}
},
// 资金渠道格式化
moneyWayFormatter(arr) {
if (!arr || !Array.isArray(arr)) return ''
const res = arr.map((item) => {
return item && item.value ? item.value : ''
}).filter(Boolean)
return res.join('、') || '暂无'
},
// 金额格式化
moneyFormat(money) {
return moneyFormatter(money)
},
// 轮次总体状态:依据后端 status0 待处理1 已办结)
getRoundClass(round) {
if (!round) return 'pending'
return round.status === 1 ? 'completed' : 'pending'
},
getRoundStatusText(round) {
if (!round) return '待处理'
return round.status === 1 ? '已办结' : '待处理'
},
// 预览附件(参考 biddingUpload.vue 的实现)
open(url) {
try {
const toBase64 = (u) => {
if (typeof btoa === 'function') {
return btoa(unescape(encodeURIComponent(u)))
}
if (window.Buffer) {
return window.Buffer.from(u).toString('base64')
}
return u
}
const encoded = toBase64(url)
this.codeUri = `${process.env.VUE_APP_BASE_API}/${process.env.VUE_APP_MODULE_NAME}/#/preview?url=${encodeURIComponent(encoded)}`
this.showModal = true
} catch (e) {
console.error('预览失败:', e)
this.$message.error('预览失败')
}
},
// 下载附件(参考 biddingUpload.vue 的实现)
download(id) {
try {
const url = `${window.location.origin}/api/download-file?id=${id}&token=${window.encodeURIComponent(getToken())}`
window.open(url, '_blank')
} catch (e) {
console.error('下载失败:', e)
this.$message.error('下载失败')
}
}
},
mounted() {
console.log('合同详情抽屉组件加载完成')
// 进入页面时不再主动拉取改由路由侦听器immediate或父组件触发
},
beforeDestroy() {
// 组件销毁前重置状态
this.resetComponent()
}
}
</script>
<style scoped lang="scss">
/* Font Awesome */
@import url('https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css');
::v-deep .el-drawer__body{
overflow: scroll !important;
}
.main-container-over {
background: white;
height: 100%;
overflow: scroll; /* */
}
.content-area {
padding: 20px;
height: 100%;
overflow-y: auto; /* */
}
/* */
.overview-container {
margin-bottom: 30px;
}
.overview-title {
font-size: 1.2rem;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.overview-section {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
}
.overview-flow {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
align-items: center;
}
.overview-step {
width: 100px;
height: 60px;
background: white;
border: 2px solid #dee2e6;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
font-size: 0.75rem;
color: #6c757d;
}
/* 1-4 */
.overview-step.pending {
border-color: #6c757d;
color: #6c757d;
background: #f8f9fa;
}
.overview-step.active {
border-color: #007bff;
color: #007bff;
background: #e3f2fd;
}
.overview-step.completed {
border-color: #28a745;
color: #28a745;
background: #e8f5e8;
}
/* 5-8 */
.overview-step.payment {
width: 80px;
height: 50px;
border: 2px dashed #adb5bd;
color: #6c757d;
background: #f8f9fa;
font-size: 0.7rem;
}
.overview-icon {
font-size: 1rem;
margin-bottom: 2px;
}
.overview-number {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
background: #6c757d;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: bold;
}
.overview-step.active .overview-number {
background: #007bff;
}
.overview-step.completed .overview-number {
background: #28a745;
}
.overview-step.pending .overview-number {
background: #6c757d;
}
.overview-step.payment .overview-number {
background: #adb5bd;
}
.overview-arrow {
color: #6c757d;
font-size: 1.2rem;
margin: 0 5px;
}
.overview-rounds-info {
margin-top: 10px;
text-align: center;
}
/* */
.payment-group-container {
display: flex;
align-items: center;
gap: 8px;
border: 2px dashed #adb5bd;
border-radius: 8px;
padding: 8px;
background: #f8f9fa;
margin: 0 5px;
}
/* */
.detail-section {
margin-bottom: 30px;
}
.detail-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.step-detail {
background: white;
border: 2px solid #dee2e6;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
transition: all 0.3s ease;
}
.step-detail.active {
border-color: #007bff;
box-shadow: 0 4px 8px rgba(0,123,255,0.1);
}
.step-detail.completed {
border-color: #28a745;
background: #f8fff9;
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.step-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
font-weight: 600;
color: #333;
}
.step-icon {
width: 40px;
height: 40px;
background: #f8f9fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: #6c757d;
}
.step-detail.active .step-icon {
background: #007bff;
color: white;
}
.step-detail.completed .step-icon {
background: #28a745;
color: white;
}
.step-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.step-status.pending {
background: #f8f9fa;
color: #6c757d;
}
.step-status.active {
background: #e3f2fd;
color: #1976d2;
}
.step-status.completed {
background: #e8f5e8;
color: #2e7d32;
}
/* 退 */
.step-status.rejected {
background: #fef0f0;
color: #f56c6c;
}
.plan-item {
margin-bottom: 5px;
font-size: 0.9rem;
line-height: 1.4;
}
.plan-item:last-child {
margin-bottom: 0;
}
.step-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.step-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.flow-block {
border: 1px dashed #dcdfe6;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
background: #fafafa;
}
.flow-block.compact {
background: #ffffff;
}
.flow-title {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subsection-title {
font-size: 1rem;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.subsection-title-with-status {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.flow-empty {
background: #fafafa;
border: 1px dashed #eaeaea;
padding: 12px;
border-radius: 6px;
color: #666;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007bff;
}
.step-detail.completed .info-item {
border-left-color: #28a745;
}
.step-detail.active .info-item {
border-left-color: #ffc107;
}
.step-detail.pending .info-item {
border-left-color: #6c757d;
}
.info-label {
font-weight: 500;
color: #666;
}
.info-value {
color: #333;
}
.action-link {
cursor: pointer;
margin: 0 6px;
color: #409eff;
}
/* */
.expense-info-container {
margin-bottom: 20px;
}
.expense-info-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.expense-info-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
}
.expense-info-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.expense-info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007bff;
font-size: 0.9rem;
}
.expense-info-label {
font-weight: 500;
color: #666;
min-width: 100px;
}
.expense-info-value {
color: #333;
text-align: right;
flex: 1;
font-weight: 500;
}
.expense-status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
background: #e8f5e8;
color: #2e7d32;
}
/* */
.round-step-detail-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.round-step-detail-section .info-item {
border-left-color: #007bff;
}
.round-step-detail-section .round-step-status.completed ~ .step-content .info-item {
border-left-color: #28a745;
}
.round-step-detail-section .round-step-status.active ~ .step-content .info-item {
border-left-color: #ffc107;
}
.round-step-detail-section .round-step-status.pending ~ .step-content .info-item {
border-left-color: #6c757d;
}
/* */
.round-step-detail-section .step-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.round-step-detail-section .step-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.round-step-detail-section:hover {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.round-step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.round-step-title {
display: flex;
align-items: center;
font-size: 1rem;
font-weight: 600;
color: #333;
}
.round-step-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.round-step-status.pending {
background: #f8f9fa;
color: #6c757d;
}
.round-step-status.active {
background: #e3f2fd;
color: #1976d2;
}
.round-step-status.completed {
background: #e8f5e8;
color: #2e7d32;
}
/* */
.detail-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.detail-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
/* */
@media (max-width: 768px) {
.step-content {
grid-template-columns: 1fr;
}
.expense-info-content {
grid-template-columns: 1fr;
}
.expense-info-item {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.expense-info-value {
text-align: left;
}
.overview-step {
width: 80px;
height: 50px;
font-size: 0.7rem;
}
.overview-step.payment {
width: 60px;
height: 40px;
font-size: 0.6rem;
}
.payment-group-container {
padding: 5px;
gap: 5px;
}
.round-step-detail {
width: 120px;
height: 70px;
}
.round-step-title-detail {
font-size: 0.7rem;
}
}
/* drawer */
.contract-detail-drawer .el-drawer__body {
padding: 0;
overflow: hidden;
}
/* Bootstrap */
.badge {
display: inline-block;
padding: 0.25em 0.4em;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
.bg-secondary {
background-color: #6c757d !important;
color: white;
}
.bg-danger {
background-color: #dc3545 !important;
color: white;
}
.ms-2 {
margin-left: 0.5rem !important;
}
.me-2 {
margin-right: 0.5rem !important;
}
</style>