|
|
|
|
@ -25,6 +25,237 @@
|
|
|
|
|
style="margin-bottom: 20px;"
|
|
|
|
|
></RelatedFlows>
|
|
|
|
|
|
|
|
|
|
<!-- 关联的支付信息(查看/办理:只要有 flow_id 就展示,没数据时展示空态) -->
|
|
|
|
|
<el-card
|
|
|
|
|
v-if="$route.query.flow_id"
|
|
|
|
|
shadow="never"
|
|
|
|
|
style="margin-bottom: 20px;"
|
|
|
|
|
v-loading="loadingPayments"
|
|
|
|
|
>
|
|
|
|
|
<div slot="header" class="clearfix">
|
|
|
|
|
<span style="font-weight: bold;">关联的支付信息</span>
|
|
|
|
|
<span v-if="relatedPayments.length > 0" style="float: right; color: #909399; font-size: 12px;">
|
|
|
|
|
共 {{ relatedPayments.length }} 条
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="relatedPayments.length === 0" style="color:#909399; padding: 8px 0;">
|
|
|
|
|
暂无关联的支付信息
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-for="(payment, index) in relatedPayments" v-else :key="payment.id" style="margin-bottom: 20px;">
|
|
|
|
|
<el-divider v-if="index > 0"></el-divider>
|
|
|
|
|
<div class="payment-info">
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付编号:</span>
|
|
|
|
|
<span class="value">{{ payment.serial_number || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付状态:</span>
|
|
|
|
|
<el-tag :type="getPaymentStatusType(payment.status)" size="small">
|
|
|
|
|
{{ payment.status_text || '-' }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 10px;">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付金额:</span>
|
|
|
|
|
<span class="value" style="color: #F56C6C; font-weight: bold;">
|
|
|
|
|
¥{{ formatAmount(payment.total_amount) }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付日期:</span>
|
|
|
|
|
<span class="value">{{ formatDate(payment.payment_date) || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 10px;" v-if="payment.payment_type_info">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付类型:</span>
|
|
|
|
|
<span class="value">{{ payment.payment_type_info.payment_type_text || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12" v-if="payment.payment_type_info.breadcrumb">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付分类:</span>
|
|
|
|
|
<span class="value">{{ formatBreadcrumb(payment.payment_type_info) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 10px;" v-if="payment.description">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">支付说明:</span>
|
|
|
|
|
<span class="value">{{ payment.description }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<!-- 支付明细 -->
|
|
|
|
|
<div v-if="payment.details && payment.details.length > 0" style="margin-top: 15px;">
|
|
|
|
|
<div style="font-weight: bold; margin-bottom: 10px; color: #606266;">支付明细:</div>
|
|
|
|
|
<el-table
|
|
|
|
|
:data="payment.details"
|
|
|
|
|
border
|
|
|
|
|
size="small"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column prop="amount" label="金额" width="120" align="right">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<span style="color: #F56C6C; font-weight: bold;">
|
|
|
|
|
¥{{ formatAmount(scope.row.amount) }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="description" label="说明" min-width="150" show-overflow-tooltip>
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.description || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="关联信息" min-width="200">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
<div v-if="scope.row.contract">
|
|
|
|
|
<span style="color: #409EFF;">合同:</span>
|
|
|
|
|
{{ scope.row.contract.title || scope.row.contract.contract_no || '-' }}
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else-if="scope.row.expenditure">
|
|
|
|
|
<span style="color: #409EFF;">账单:</span>
|
|
|
|
|
{{ scope.row.expenditure.title || scope.row.expenditure.serial_number || '-' }}
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 非直接支付:还原“非直接支出(PlannedExpenditure)”的元素模板与内容 -->
|
|
|
|
|
<el-card
|
|
|
|
|
v-if="payment.payment_type_info && payment.payment_type_info.payment_type === 'indirect' && payment.details && payment.details.some(d => d.expenditure_type === 'indirect')"
|
|
|
|
|
shadow="never"
|
|
|
|
|
style="margin-top: 15px;"
|
|
|
|
|
v-loading="loadingIndirectExpenditures"
|
|
|
|
|
>
|
|
|
|
|
<div slot="header" class="clearfix">
|
|
|
|
|
<span style="font-weight: bold;">非直接支付相关信息</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-for="(d, di) in payment.details.filter(i => i && i.expenditure_type === 'indirect' && i.expenditure_id)" :key="d.id" style="margin-bottom: 20px;">
|
|
|
|
|
<el-divider v-if="di > 0"></el-divider>
|
|
|
|
|
<div v-if="(indirectExpenditureMap[d.expenditure_id] && indirectExpenditureMap[d.expenditure_id].title) || (d.expenditure && d.expenditure.title)" style="margin-bottom: 10px; font-weight: 600; color:#303133;">
|
|
|
|
|
{{ (indirectExpenditureMap[d.expenditure_id] && indirectExpenditureMap[d.expenditure_id].title) || (d.expenditure && d.expenditure.title) }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-descriptions :column="1" border class="indirect-payment-descriptions">
|
|
|
|
|
<template v-for="f in getIndirectTemplateFields((indirectExpenditureMap[d.expenditure_id] && indirectExpenditureMap[d.expenditure_id].category_id) || (d.expenditure && d.expenditure.category_id))">
|
|
|
|
|
<el-descriptions-item
|
|
|
|
|
v-if="f.element_type !== 'oa_custom_model' || hasOaCustomModelValue(d.expenditure_id, f)"
|
|
|
|
|
:key="f.key || (f.element_id || f.label)"
|
|
|
|
|
:label="f.label || f.name || '字段'"
|
|
|
|
|
>
|
|
|
|
|
<!-- oa_custom_model:显示标题 + 点击抽屉查看流程 -->
|
|
|
|
|
<template v-if="f.element_type === 'oa_custom_model'">
|
|
|
|
|
<span v-if="getOaCustomModelBinding(d.expenditure_id, f.key)">
|
|
|
|
|
<el-link
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
@click="openFlowDrawer({
|
|
|
|
|
flowId: getOaCustomModelBinding(d.expenditure_id, f.key).flow_instance_id,
|
|
|
|
|
customModelId: getOaCustomModelBinding(d.expenditure_id, f.key).flow_custom_model_id,
|
|
|
|
|
title: getFlowDisplayName(getOaCustomModelBinding(d.expenditure_id, f.key).flow_instance_id, getOaCustomModelBinding(d.expenditure_id, f.key).flow_display_name)
|
|
|
|
|
})"
|
|
|
|
|
>
|
|
|
|
|
{{ getFlowDisplayName(getOaCustomModelBinding(d.expenditure_id, f.key).flow_instance_id, getOaCustomModelBinding(d.expenditure_id, f.key).flow_display_name) }}
|
|
|
|
|
</el-link>
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>
|
|
|
|
|
<el-link
|
|
|
|
|
v-if="extractFlowId(getIndirectFieldValue(d.expenditure_id, f.key)) && (f.model_id || f.flow_custom_model_id)"
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
@click="openFlowDrawer({
|
|
|
|
|
flowId: extractFlowId(getIndirectFieldValue(d.expenditure_id, f.key)),
|
|
|
|
|
customModelId: f.model_id || f.flow_custom_model_id,
|
|
|
|
|
title: extractDisplayName(getIndirectFieldValue(d.expenditure_id, f.key))
|
|
|
|
|
})"
|
|
|
|
|
>
|
|
|
|
|
{{ getFlowDisplayName(extractFlowId(getIndirectFieldValue(d.expenditure_id, f.key)), extractDisplayName(getIndirectFieldValue(d.expenditure_id, f.key))) }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<span v-else>{{ formatReadonlyValue(getIndirectFieldValue(d.expenditure_id, f.key)) }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- meeting_minutes:点击抽屉查看 -->
|
|
|
|
|
<template v-else-if="f.element_type === 'meeting_minutes'">
|
|
|
|
|
<el-link
|
|
|
|
|
v-if="extractMeetingMinuteId(getIndirectFieldValue(d.expenditure_id, f.key))"
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
@click="openMeetingMinutesDrawer(extractMeetingMinuteId(getIndirectFieldValue(d.expenditure_id, f.key)))"
|
|
|
|
|
>
|
|
|
|
|
查看会议纪要
|
|
|
|
|
</el-link>
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- detail_table:渲染为表格 -->
|
|
|
|
|
<template v-else-if="f.element_type === 'detail_table'">
|
|
|
|
|
<div style="overflow-x: auto;">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="Array.isArray(getIndirectFieldValue(d.expenditure_id, f.key)) ? getIndirectFieldValue(d.expenditure_id, f.key) : []"
|
|
|
|
|
border
|
|
|
|
|
size="small"
|
|
|
|
|
style="width: 100%; min-width: 600px;"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="col in (indirectDetailTableFieldsMap[f.element_id] || [])"
|
|
|
|
|
:key="col.id || col.field_key"
|
|
|
|
|
:prop="col.field_key"
|
|
|
|
|
:label="col.field_name || col.field_key"
|
|
|
|
|
min-width="120"
|
|
|
|
|
show-overflow-tooltip
|
|
|
|
|
/>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 其他类型:通用展示 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
{{ formatReadonlyValue(getIndirectFieldValue(d.expenditure_id, f.key)) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</template>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 10px;">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">创建时间:</span>
|
|
|
|
|
<span class="value">{{ formatDateTime(payment.created_at) || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12" v-if="payment.processed_at">
|
|
|
|
|
<div class="info-item">
|
|
|
|
|
<span class="label">处理时间:</span>
|
|
|
|
|
<span class="value">{{ formatDateTime(payment.processed_at) || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="loadingPayments" style="text-align: center; padding: 20px;">
|
|
|
|
|
<i class="el-icon-loading"></i> 加载中...
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<div v-if="/\/detail/.test($route.path) && config && config.flow">
|
|
|
|
|
<div v-if="config.flow.hasOwnProperty('out_contracts') && config.flow.out_contracts && config.flow.out_contracts.length > 0" style="margin-bottom: 10px;color:#F56C6C;">
|
|
|
|
|
<!-- 单条数据 -->
|
|
|
|
|
@ -332,6 +563,52 @@
|
|
|
|
|
frameborder="0"
|
|
|
|
|
/>
|
|
|
|
|
</vxe-modal>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧抽屉:流程详情 / 会议纪要 -->
|
|
|
|
|
<el-drawer
|
|
|
|
|
:visible.sync="rightDrawerVisible"
|
|
|
|
|
:title="rightDrawerTitle"
|
|
|
|
|
direction="rtl"
|
|
|
|
|
size="60%"
|
|
|
|
|
:with-header="true"
|
|
|
|
|
>
|
|
|
|
|
<!-- 流程详情:iframe -->
|
|
|
|
|
<div v-if="rightDrawerType === 'flow'" style="width: 100%; height: 100%;">
|
|
|
|
|
<iframe
|
|
|
|
|
:src="rightDrawerUrl"
|
|
|
|
|
style="display: block; width: 100%; height: calc(100vh - 120px); border: 0;"
|
|
|
|
|
frameborder="0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 会议纪要:请求数据展示 -->
|
|
|
|
|
<div v-else-if="rightDrawerType === 'meetingMinutes'" v-loading="loadingMeetingMinutes">
|
|
|
|
|
<div v-if="meetingMinutesDetail">
|
|
|
|
|
<el-descriptions :column="1" border>
|
|
|
|
|
<el-descriptions-item label="标题">
|
|
|
|
|
{{ meetingMinutesDetail.title || '-' }}
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="内容清单">
|
|
|
|
|
<div v-if="meetingMinutesDetail.items && meetingMinutesDetail.items.length">
|
|
|
|
|
<div v-for="(it, idx) in meetingMinutesDetail.items" :key="idx" style="margin-bottom: 8px;">
|
|
|
|
|
<span style="color:#409EFF;">{{ it.type || '类型' }}:</span>
|
|
|
|
|
<span>{{ it.content || '-' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else>无</span>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="附件" v-if="meetingMinutesDetail.files_details && meetingMinutesDetail.files_details.length">
|
|
|
|
|
<div v-for="(f, idx) in meetingMinutesDetail.files_details" :key="idx" style="margin-bottom: 6px;">
|
|
|
|
|
<el-link type="primary" :underline="false" :href="f.url" target="_blank">
|
|
|
|
|
{{ f.original_name || f.name || '附件' }}
|
|
|
|
|
</el-link>
|
|
|
|
|
</div>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else style="color:#909399;">未获取到会议纪要数据</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-drawer>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
@ -354,6 +631,8 @@ import {
|
|
|
|
|
updateNodeTime,
|
|
|
|
|
view,
|
|
|
|
|
} from "@/api/flow";
|
|
|
|
|
import { getPaymentsByFlowId, getPlannedExpenditure, getPlannedExpenditureTemplatesByCategory, getDetailTableFields, getOaFlowDetails } from "@/api/payment";
|
|
|
|
|
import { show as meetingMinutesShow } from "@/api/meetingMinutes";
|
|
|
|
|
import { deepCopy } from "@/utils";
|
|
|
|
|
import { validation, validationName } from "@/utils/validate";
|
|
|
|
|
import { print } from "@/utils/print";
|
|
|
|
|
@ -413,6 +692,23 @@ export default {
|
|
|
|
|
subRules: {},
|
|
|
|
|
flows: [],
|
|
|
|
|
csrf_token: '',
|
|
|
|
|
// 关联的支付信息
|
|
|
|
|
relatedPayments: [],
|
|
|
|
|
loadingPayments: false,
|
|
|
|
|
// 非直接支付:还原“非直接支出(PlannedExpenditure)”的模板字段与内容
|
|
|
|
|
indirectExpenditureMap: {}, // { [expenditureId]: PlannedExpenditureDetail }
|
|
|
|
|
indirectTemplateConfigMap: {}, // { [categoryId]: templateConfig }
|
|
|
|
|
indirectDetailTableFieldsMap: {}, // { [elementId]: columns[] }
|
|
|
|
|
oaFlowDetailsMap: {}, // { [flowId]: {display_name,no,title,custom_model_id,...} }
|
|
|
|
|
loadingIndirectExpenditures: false,
|
|
|
|
|
|
|
|
|
|
// 右侧抽屉(流程详情/会议纪要)
|
|
|
|
|
rightDrawerVisible: false,
|
|
|
|
|
rightDrawerType: '', // 'flow' | 'meetingMinutes'
|
|
|
|
|
rightDrawerTitle: '',
|
|
|
|
|
rightDrawerUrl: '',
|
|
|
|
|
meetingMinutesDetail: null,
|
|
|
|
|
loadingMeetingMinutes: false,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
watch:{
|
|
|
|
|
@ -677,6 +973,286 @@ export default {
|
|
|
|
|
hours > 0 ? hours + "时" : ""
|
|
|
|
|
}${minutes}分${seconds}秒`;
|
|
|
|
|
},
|
|
|
|
|
// 获取支付状态标签类型
|
|
|
|
|
getPaymentStatusType(status) {
|
|
|
|
|
const statusMap = {
|
|
|
|
|
'pending': 'warning',
|
|
|
|
|
'processing': 'primary',
|
|
|
|
|
'completed': 'success',
|
|
|
|
|
'cancelled': 'danger',
|
|
|
|
|
};
|
|
|
|
|
return statusMap[status] || 'info';
|
|
|
|
|
},
|
|
|
|
|
// 格式化金额
|
|
|
|
|
formatAmount(amount) {
|
|
|
|
|
if (!amount && amount !== 0) return '0.00';
|
|
|
|
|
return parseFloat(amount).toLocaleString('zh-CN', {
|
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
|
maximumFractionDigits: 2
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
// 格式化日期
|
|
|
|
|
formatDate(date) {
|
|
|
|
|
if (!date) return '';
|
|
|
|
|
return this.$moment(date).format('YYYY-MM-DD');
|
|
|
|
|
},
|
|
|
|
|
// 格式化日期时间
|
|
|
|
|
formatDateTime(dateTime) {
|
|
|
|
|
if (!dateTime) return '';
|
|
|
|
|
return this.$moment(dateTime).format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
},
|
|
|
|
|
// 显示支付分类面包屑(兼容字符串/数组)
|
|
|
|
|
formatBreadcrumb(paymentTypeInfo) {
|
|
|
|
|
if (!paymentTypeInfo || !paymentTypeInfo.breadcrumb) return '';
|
|
|
|
|
const bc = paymentTypeInfo.breadcrumb;
|
|
|
|
|
if (Array.isArray(bc)) {
|
|
|
|
|
return bc.join(' / ');
|
|
|
|
|
}
|
|
|
|
|
return bc;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 非直接支付:加载每条明细对应的 PlannedExpenditure 详情 & 模板配置
|
|
|
|
|
async loadIndirectExpenditures(payment) {
|
|
|
|
|
this.indirectExpenditureMap = {};
|
|
|
|
|
this.indirectDetailTableFieldsMap = {};
|
|
|
|
|
this.oaFlowDetailsMap = {};
|
|
|
|
|
this.loadingIndirectExpenditures = true;
|
|
|
|
|
try {
|
|
|
|
|
const details = Array.isArray(payment?.details) ? payment.details : [];
|
|
|
|
|
const indirectDetails = details.filter(d => d && d.expenditure_type === 'indirect' && d.expenditure_id);
|
|
|
|
|
if (indirectDetails.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// 先并发拉取 PlannedExpenditure 详情
|
|
|
|
|
const ids = Array.from(new Set(indirectDetails.map(d => d.expenditure_id).filter(Boolean)));
|
|
|
|
|
const expenditureResults = await Promise.all(ids.map(id => getPlannedExpenditure(id).catch(() => null)));
|
|
|
|
|
expenditureResults.forEach(exp => {
|
|
|
|
|
if (exp && exp.id) {
|
|
|
|
|
this.$set(this.indirectExpenditureMap, exp.id, exp);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 再按 category_id 拉模板(用于字段顺序/标题)
|
|
|
|
|
const categoryIds = Array.from(new Set(Object.values(this.indirectExpenditureMap).map(exp => exp.category_id).filter(Boolean)));
|
|
|
|
|
const tplResults = await Promise.all(categoryIds.map(cid => getPlannedExpenditureTemplatesByCategory(cid).catch(() => null)));
|
|
|
|
|
tplResults.forEach((tplList, idx) => {
|
|
|
|
|
const cid = categoryIds[idx];
|
|
|
|
|
// getByCategory 返回数组,通常每个分类 1 个模板
|
|
|
|
|
const firstTpl = Array.isArray(tplList) ? tplList[0] : null;
|
|
|
|
|
const config = firstTpl && firstTpl.config ? firstTpl.config : null;
|
|
|
|
|
this.$set(this.indirectTemplateConfigMap, cid, config || null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 加载 detail_table 的列定义(按模板配置中的 element_id)
|
|
|
|
|
const detailTableElementIds = new Set();
|
|
|
|
|
categoryIds.forEach(cid => {
|
|
|
|
|
const fields = this.getIndirectTemplateFields(cid);
|
|
|
|
|
fields.forEach(f => {
|
|
|
|
|
if (f && f.element_type === 'detail_table' && f.element_id) {
|
|
|
|
|
detailTableElementIds.add(f.element_id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
const idsToLoad = Array.from(detailTableElementIds);
|
|
|
|
|
const colResults = await Promise.all(idsToLoad.map(id => getDetailTableFields(id).catch(() => [])));
|
|
|
|
|
colResults.forEach((cols, i) => {
|
|
|
|
|
const id = idsToLoad[i];
|
|
|
|
|
const arr = Array.isArray(cols) ? cols.slice() : [];
|
|
|
|
|
// 后端字段:field_name, field_key, sort_order
|
|
|
|
|
arr.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
|
|
|
|
|
this.$set(this.indirectDetailTableFieldsMap, id, arr);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 批量获取 oa_custom_model 的流程详情,用于显示标题而不是“流程ID”
|
|
|
|
|
const flowIds = [];
|
|
|
|
|
Object.values(this.indirectExpenditureMap).forEach(exp => {
|
|
|
|
|
const bindings = Array.isArray(exp?.flow_bindings) ? exp.flow_bindings : [];
|
|
|
|
|
bindings.forEach(b => {
|
|
|
|
|
if (b && b.flow_instance_id) {
|
|
|
|
|
flowIds.push(b.flow_instance_id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
const uniqFlowIds = Array.from(new Set(flowIds.filter(Boolean)));
|
|
|
|
|
if (uniqFlowIds.length) {
|
|
|
|
|
try {
|
|
|
|
|
// request.js 成功时直接返回 res.data(这里是 map)
|
|
|
|
|
const map = await getOaFlowDetails(uniqFlowIds);
|
|
|
|
|
if (map && typeof map === 'object') {
|
|
|
|
|
this.oaFlowDetailsMap = map;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.oaFlowDetailsMap = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
this.loadingIndirectExpenditures = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getFlowDisplayName(flowId, fallback = '') {
|
|
|
|
|
const f = this.oaFlowDetailsMap?.[flowId];
|
|
|
|
|
if (!f) return fallback || (flowId ? `流程ID: ${flowId}` : '-');
|
|
|
|
|
return f.display_name || [f.no, f.title].filter(Boolean).join(' - ') || fallback || (flowId ? `流程ID: ${flowId}` : '-');
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 从模板 config 提取字段列表(按配置顺序)
|
|
|
|
|
getIndirectTemplateFields(categoryId) {
|
|
|
|
|
const cfg = this.indirectTemplateConfigMap?.[categoryId];
|
|
|
|
|
if (!cfg || typeof cfg !== 'object') return [];
|
|
|
|
|
const sections = Object.values(cfg);
|
|
|
|
|
const fields = [];
|
|
|
|
|
sections.forEach(sec => {
|
|
|
|
|
if (!sec) return;
|
|
|
|
|
if (Array.isArray(sec.fields)) {
|
|
|
|
|
sec.fields.forEach(f => f && fields.push(f));
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(sec.rounds)) {
|
|
|
|
|
sec.rounds.forEach(r => {
|
|
|
|
|
if (r && Array.isArray(r.fields)) {
|
|
|
|
|
r.fields.forEach(f => f && fields.push(f));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return fields;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 非直接支出字段取值(优先用 element_values 映射)
|
|
|
|
|
getIndirectFieldValue(expenditureId, fieldKey) {
|
|
|
|
|
const exp = this.indirectExpenditureMap?.[expenditureId];
|
|
|
|
|
const map = exp?.element_values || {};
|
|
|
|
|
return map?.[fieldKey];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 取 oa_custom_model 绑定(用于显示标题与打开流程详情)
|
|
|
|
|
getOaCustomModelBinding(expenditureId, fieldKey) {
|
|
|
|
|
const exp = this.indirectExpenditureMap?.[expenditureId];
|
|
|
|
|
const bindings = Array.isArray(exp?.flow_bindings) ? exp.flow_bindings : [];
|
|
|
|
|
return bindings.find(b => b && b.field_key === fieldKey) || null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 判断 oa_custom_model 字段是否有值
|
|
|
|
|
hasOaCustomModelValue(expenditureId, field) {
|
|
|
|
|
// 如果有绑定数据,说明有值
|
|
|
|
|
if (this.getOaCustomModelBinding(expenditureId, field.key)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// 如果字段值中有流程ID,说明有值
|
|
|
|
|
const fieldValue = this.getIndirectFieldValue(expenditureId, field.key);
|
|
|
|
|
if (this.extractFlowId(fieldValue) && (field.model_id || field.flow_custom_model_id)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
extractFlowId(val) {
|
|
|
|
|
if (val === null || val === undefined) return null;
|
|
|
|
|
if (typeof val === 'number' && val > 0) return val;
|
|
|
|
|
if (typeof val === 'string' && /^\d+$/.test(val)) return parseInt(val, 10);
|
|
|
|
|
if (typeof val === 'object') {
|
|
|
|
|
const id = val.instance_id || val.flow_instance_id || val.id;
|
|
|
|
|
if (id && /^\d+$/.test(String(id))) return parseInt(String(id), 10);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
extractDisplayName(val) {
|
|
|
|
|
if (!val) return '';
|
|
|
|
|
if (typeof val === 'string') return val;
|
|
|
|
|
if (typeof val === 'object') {
|
|
|
|
|
return val.display_name || val.name || val.title || '';
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
extractMeetingMinuteId(val) {
|
|
|
|
|
if (val === null || val === undefined) return null;
|
|
|
|
|
if (typeof val === 'number' && val > 0) return val;
|
|
|
|
|
if (typeof val === 'string' && /^\d+$/.test(val)) return parseInt(val, 10);
|
|
|
|
|
if (typeof val === 'object') {
|
|
|
|
|
const id = val.meeting_minute_id || val.id;
|
|
|
|
|
if (id && /^\d+$/.test(String(id))) return parseInt(String(id), 10);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
openFlowDrawer({ flowId, customModelId, title }) {
|
|
|
|
|
if (!flowId || !customModelId) return;
|
|
|
|
|
this.rightDrawerType = 'flow';
|
|
|
|
|
this.rightDrawerTitle = title || `流程 ${flowId}`;
|
|
|
|
|
this.rightDrawerUrl = `/oa/#/flow/detail?module_id=${customModelId}&flow_id=${flowId}&isSinglePage=1&auth_token=${encodeURIComponent(getToken())}`;
|
|
|
|
|
this.rightDrawerVisible = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async openMeetingMinutesDrawer(meetingMinuteId) {
|
|
|
|
|
if (!meetingMinuteId) return;
|
|
|
|
|
this.rightDrawerType = 'meetingMinutes';
|
|
|
|
|
this.rightDrawerTitle = '查看会议纪要';
|
|
|
|
|
this.rightDrawerUrl = '';
|
|
|
|
|
this.meetingMinutesDetail = null;
|
|
|
|
|
this.rightDrawerVisible = true;
|
|
|
|
|
this.loadingMeetingMinutes = true;
|
|
|
|
|
try {
|
|
|
|
|
// meetingMinutesShow 成功时返回 res.data
|
|
|
|
|
const detail = await meetingMinutesShow({ id: meetingMinuteId });
|
|
|
|
|
this.meetingMinutesDetail = detail || null;
|
|
|
|
|
this.rightDrawerTitle = detail?.title || '查看会议纪要';
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('加载会议纪要失败:', e);
|
|
|
|
|
this.meetingMinutesDetail = null;
|
|
|
|
|
} finally {
|
|
|
|
|
this.loadingMeetingMinutes = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 通用只读展示
|
|
|
|
|
formatReadonlyValue(val) {
|
|
|
|
|
if (val === undefined || val === null || val === '') return '-';
|
|
|
|
|
if (Array.isArray(val)) return val.join(',');
|
|
|
|
|
if (typeof val === 'object') return JSON.stringify(val);
|
|
|
|
|
return String(val);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 加载关联的支付信息
|
|
|
|
|
async loadRelatedPayments() {
|
|
|
|
|
if (!this.$route.query.flow_id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loadingPayments = true;
|
|
|
|
|
try {
|
|
|
|
|
// request.js 成功时会直接返回 res.data(也就是 payment 对象或 null)
|
|
|
|
|
const payment = await getPaymentsByFlowId(this.$route.query.flow_id, { all: true });
|
|
|
|
|
// 接口约定:要么无数据(null),要么唯一的一条记录
|
|
|
|
|
if (payment) {
|
|
|
|
|
// 兼容 breadcrumb 为字符串或数组
|
|
|
|
|
if (payment.payment_type_info && payment.payment_type_info.breadcrumb) {
|
|
|
|
|
const bc = payment.payment_type_info.breadcrumb;
|
|
|
|
|
if (Array.isArray(bc)) {
|
|
|
|
|
payment.payment_type_info.breadcrumb = bc;
|
|
|
|
|
} else if (typeof bc === 'string') {
|
|
|
|
|
payment.payment_type_info.breadcrumb = bc.split(/\s*>\s*|\s*\/\s*/).filter(Boolean);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.relatedPayments = [payment];
|
|
|
|
|
// 非直接支付:加载 PlannedExpenditure 模板与内容
|
|
|
|
|
if (payment.payment_type_info && payment.payment_type_info.payment_type === 'indirect') {
|
|
|
|
|
await this.loadIndirectExpenditures(payment);
|
|
|
|
|
} else {
|
|
|
|
|
this.indirectExpenditureMap = {};
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.relatedPayments = [];
|
|
|
|
|
this.indirectExpenditureMap = {};
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('加载支付信息失败:', err);
|
|
|
|
|
this.relatedPayments = [];
|
|
|
|
|
this.indirectExpenditureMap = {};
|
|
|
|
|
} finally {
|
|
|
|
|
this.loadingPayments = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async getConfig() {
|
|
|
|
|
const loading = this.$loading({
|
|
|
|
|
@ -761,6 +1337,11 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
this.form = Object.assign({}, this.form);
|
|
|
|
|
loading.close();
|
|
|
|
|
|
|
|
|
|
// 加载关联的支付信息(详情模式下)
|
|
|
|
|
if (this.$route.query.flow_id) {
|
|
|
|
|
this.loadRelatedPayments();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
this.$message.error("配置失败");
|
|
|
|
|
@ -872,6 +1453,11 @@ export default {
|
|
|
|
|
}
|
|
|
|
|
this.form = Object.assign({}, this.form);
|
|
|
|
|
loading.close();
|
|
|
|
|
|
|
|
|
|
// 加载关联的支付信息(办理模式下)
|
|
|
|
|
if (this.$route.query.flow_id) {
|
|
|
|
|
this.loadRelatedPayments();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
this.$message.error("配置失败");
|
|
|
|
|
@ -1210,6 +1796,34 @@ export default {
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 支付信息展示样式 */
|
|
|
|
|
.payment-info {
|
|
|
|
|
.info-item {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
.label {
|
|
|
|
|
color: #606266;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
.value {
|
|
|
|
|
color: #303133;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 非直接支付相关信息样式 */
|
|
|
|
|
.indirect-payment-descriptions {
|
|
|
|
|
::v-deep .el-descriptions-item__label {
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: #303133;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
::v-deep .el-descriptions-item__content {
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
.log-table-scroll {
|
|
|
|
|
|