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.

2794 lines
105 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>
<div class="container" :class="{ 'is-single-page': isSinglePage }">
<el-card
:shadow="device === 'desktop' ? 'always' : 'never'"
class="card"
:style="{
border: device === 'desktop' ? '' : 'none',
background: device === 'desktop' ? '' : '#f7f8fa',
'min-height': device === 'desktop' ? '' : '100vh',
}"
>
<template #header>
<p>{{ config.customModel ? config.customModel.name : "办理" }}</p>
</template>
<template>
<Steps :logs="config.logs" :current-node="node"></Steps>
<!-- 关联流程组件 -->
<RelatedFlows
v-model="form.related_flow_ids"
:readonly="/\/detail/.test($route.path) || (!!$route.query.flow_id && !isFirstNode)"
:flow-id="$route.query.flow_id"
:collapsible="(!/\/detail/.test($route.path)) && (!$route.query.flow_id || isFirstNode)"
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>
<!-- 支付模板字段fields_data / element_values 双写):只读展示 -->
<div
v-if="getVisiblePaymentTemplateElements(payment).length > 0"
style="margin-top: 15px;"
v-loading="loadingPaymentTemplateElements"
>
<el-descriptions :column="1" border size="small" class="payment-extra-descriptions">
<el-descriptions-item
v-for="el in getVisiblePaymentTemplateElements(payment)"
:key="`pay_el_${el.id}`"
:label="el.name || '字段'"
>
<!-- checklist纯展示不渲染表单控件 -->
<div v-if="el.type === 'checklist'" class="oa-readonly-checklist">
<div
v-for="(opt, idx) in (getPaymentChecklistOptions(el) || [])"
:key="opt.value || idx"
v-show="isChecklistChecked(getPaymentTemplateElementValue(el, payment), opt.value) || getPaymentChecklistRemark(el, opt.value, payment)"
class="oa-checklist-option-item"
>
<div class="oa-checklist-row">
<span
class="oa-checklist-box"
:class="{ checked: isChecklistChecked(getPaymentTemplateElementValue(el, payment), opt.value) }"
aria-hidden="true"
></span>
<span class="oa-checklist-text">
<span class="oa-checklist-label">{{ `${idx + 1}、${opt.label}` }}</span>
<span v-if="getPaymentChecklistRemark(el, opt.value, payment)" class="oa-checklist-remark">
{{ getPaymentChecklistRemark(el, opt.value, payment) }}
</span>
</span>
</div>
</div>
</div>
<!-- 会议纪要:点击抽屉查看 -->
<div v-else-if="el.type === 'meeting_minutes'">
<el-link
v-if="extractMeetingMinuteId(getPaymentTemplateElementValue(el, payment))"
type="primary"
:underline="false"
@click="openMeetingMinutesDrawer(extractMeetingMinuteId(getPaymentTemplateElementValue(el, payment)))"
>
{{ (getPaymentTemplateElementValue(el, payment) && getPaymentTemplateElementValue(el, payment).title) ? getPaymentTemplateElementValue(el, payment).title : '查看会议纪要' }}
</el-link>
<span v-else>-</span>
</div>
<!-- 明细表格 -->
<div v-else-if="el.type === 'detail_table'">
<div class="oa-table-x-scroll">
<el-table
:data="Array.isArray(getPaymentTemplateElementValue(el, payment)) ? getPaymentTemplateElementValue(el, payment) : []"
border
size="small"
:fit="false"
style="width: 100%;"
>
<el-table-column type="index" label="#" width="60" align="center" />
<el-table-column
v-for="col in (getPaymentDetailTableFields(el.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>
</div>
<!-- 审批流程:显示 no/title若有 model_id 则可点开流程抽屉 -->
<div v-else-if="el.type === 'approval_flow'">
<el-link
v-if="getPaymentTemplateElementValue(el, payment) && el.model_id"
type="primary"
:underline="false"
@click="openFlowDrawer({
flowId: Number(getPaymentTemplateElementValue(el, payment)),
customModelId: Number(el.model_id),
title: getPaymentFlowDisplayInfo(Number(getPaymentTemplateElementValue(el, payment)))
})"
>
{{ getPaymentFlowDisplayInfo(Number(getPaymentTemplateElementValue(el, payment))) }}
</el-link>
<span v-else>{{ getPaymentFlowDisplayInfo(Number(getPaymentTemplateElementValue(el, payment))) || '-' }}</span>
</div>
<!-- 附件 -->
<div v-else-if="el.type === 'form_element' && el.field_type === 'attachment'">
<div v-for="(f, idx) in getPaymentFileItems(el, payment)" :key="`${el.id}_${idx}`" style="margin-bottom: 6px;">
<el-link v-if="f.url" type="primary" :underline="false" :href="f.url" target="_blank">
{{ f.name || '附件' }}
</el-link>
<span v-else>{{ f.name || '附件' }}</span>
</div>
<span v-if="getPaymentFileItems(el, payment).length === 0">-</span>
</div>
<!-- 默认展示 -->
<span v-else>
{{ formatReadonlyValue(getPaymentTemplateElementValue(el, payment)) }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 非直接支付:还原“非直接支出(PlannedExpenditure)”的元素模板与内容 -->
<el-card
v-if="getPlannedExpenditureIdsFromPayment(payment).length > 0"
shadow="never"
style="margin-top: 15px;"
v-loading="loadingIndirectExpenditures"
>
<div slot="header" class="clearfix">
<span style="font-weight: bold;">相关事前流程</span>
</div>
<div v-for="(expId, di) in getPlannedExpenditureIdsFromPayment(payment)" :key="`exp-${expId}`" style="margin-bottom: 20px;">
<el-divider v-if="di > 0"></el-divider>
<div v-if="indirectExpenditureMap[expId] && indirectExpenditureMap[expId].title" style="margin-bottom: 10px; font-weight: 600; color:#303133;">
{{ indirectExpenditureMap[expId].title }}
</div>
<el-descriptions :column="1" border class="indirect-payment-descriptions">
<template v-for="f in getIndirectTemplateFields((indirectExpenditureMap[expId] && indirectExpenditureMap[expId].category_id))">
<el-descriptions-item
v-if="shouldRenderPlannedField(expId, f)"
:key="f.key || (f.element_id || f.label)"
:label="f.label || f.name || '字段'"
>
<!-- oa_custom_model / out_custom_model多条流程顿号分隔点击抽屉查看 -->
<template v-if="f.element_type === 'oa_custom_model' || f.element_type === 'out_custom_model'">
<span v-if="getOaCustomModelBindings(expId, f).length">
<span v-for="(b, bi) in getOaCustomModelBindings(expId, f)" :key="`${b.flow_instance_id}_${bi}`">
<span v-if="bi > 0">、</span>
<el-link
type="primary"
:underline="false"
@click="openFlowDrawer({
flowId: b.flow_instance_id,
customModelId: b.flow_custom_model_id,
title: getFlowDisplayName(b.flow_instance_id, b.flow_display_name)
})"
>
{{ getFlowDisplayName(b.flow_instance_id, b.flow_display_name) }}
</el-link>
</span>
</span>
<span v-else>-</span>
</template>
<!-- checklist全量展示选项 + 勾选 + 备注 -->
<template v-else-if="f.element_type === 'checklist'">
<div class="oa-readonly-checklist">
<div
v-for="(opt, idx) in (getPlannedChecklistOptions(f) || [])"
:key="opt.value || idx"
class="oa-checklist-option-item"
>
<div class="oa-checklist-row">
<span
class="oa-checklist-box"
:class="{ checked: isChecklistChecked(getIndirectFieldValue(expId, getTplFieldKey(f)), opt.value) }"
aria-hidden="true"
></span>
<span class="oa-checklist-text">
<span class="oa-checklist-label">{{ `${idx + 1}、${opt.label}` }}</span>
<span v-if="getPlannedChecklistRemark(expId, f, opt.value)" class="oa-checklist-remark">
{{ getPlannedChecklistRemark(expId, f, opt.value) }}
</span>
</span>
</div>
</div>
</div>
</template>
<!-- 附件/文件:还原原始名并可点击 -->
<template v-else-if="f.element_type === 'attachment' || f.element_type === 'file'">
<div v-for="(fi, fidx) in getPlannedFileItems(expId, f)" :key="`${expId}_${f.element_id || f.key}_${fidx}`" style="margin-bottom: 6px;">
<el-link
v-if="fi.url"
type="primary"
:underline="false"
:href="fi.url"
target="_blank"
class="file-link"
@click="handlePlannedFileClick(fi)"
>
{{ fi.name || '附件' }}
</el-link>
<el-link
v-else
type="primary"
:underline="false"
class="file-link file-link--no-url"
@click="handlePlannedFileClick(fi)"
>
{{ fi.name || '附件' }}
</el-link>
</div>
<span v-if="getPlannedFileItems(expId, f).length === 0">-</span>
</template>
<!-- meeting_minutes点击抽屉查看 -->
<template v-else-if="f.element_type === 'meeting_minutes'">
<el-link
v-if="extractMeetingMinuteId(getIndirectFieldValue(expId, getTplFieldKey(f)))"
type="primary"
:underline="false"
@click="openMeetingMinutesDrawer(extractMeetingMinuteId(getIndirectFieldValue(expId, getTplFieldKey(f))))"
>
查看会议纪要
</el-link>
<span v-else>-</span>
</template>
<!-- detail_table渲染为表格 -->
<template v-else-if="f.element_type === 'detail_table'">
<div class="oa-table-x-scroll">
<el-table
:data="Array.isArray(getIndirectFieldValue(expId, getTplFieldKey(f))) ? getIndirectFieldValue(expId, getTplFieldKey(f)) : []"
border
size="small"
:fit="false"
style="width: 100%;"
>
<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
>
<template slot-scope="scope">
{{ formatDetailTableCell(scope.row ? scope.row[col.field_key] : null, col) }}
</template>
</el-table-column>
</el-table>
</div>
</template>
<!-- 其他类型:通用展示 -->
<template v-else>
{{ formatReadonlyValue(getIndirectFieldValue(expId, getTplFieldKey(f))) }}
</template>
</el-descriptions-item>
</template>
</el-descriptions>
</div>
</el-card>
<!-- 关联合同:合同信息链 + 合同相关支付列表 -->
<el-card
v-if="getContractIdFromPayment(payment)"
shadow="never"
style="margin-top: 15px;"
v-loading="loadingContractPayments"
>
<div slot="header" class="clearfix">
<span style="font-weight: bold;">相关合同信息</span>
</div>
<!-- 合同支付时序信息 -->
<div class="contract-payments-summary" style="margin-bottom: 12px;">
<el-row :gutter="20">
<el-col :span="12">
<div class="info-item">
<span class="label">合同相关支付次数:</span>
<span class="value">{{ getContractPaymentStats(payment.id).total_count }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="info-item">
<span class="label">当前为第几次支付:</span>
<span class="value">
{{
getContractPaymentStats(payment.id).current_index
? `${getContractPaymentStats(payment.id).current_index} / ${getContractPaymentStats(payment.id).total_count}`
: '-'
}}
</span>
</div>
</el-col>
</el-row>
</div>
<!-- 合同基本信息 -->
<el-descriptions v-if="contractDetail" :column="2" border size="small" style="margin-bottom: 12px;">
<el-descriptions-item label="合同编号">
{{ contractDetail.contract_no || '-' }}
</el-descriptions-item>
<el-descriptions-item label="合同名称">
{{ contractDetail.title || '-' }}
</el-descriptions-item>
<el-descriptions-item label="甲方">
{{ contractDetail.party_a || '-' }}
</el-descriptions-item>
<el-descriptions-item label="乙方">
{{ contractDetail.party_b || '-' }}
</el-descriptions-item>
<el-descriptions-item label="合同总额">
<span style="color: #F56C6C; font-weight: bold;">¥{{ formatAmount(contractDetail.amount_total) }}</span>
</el-descriptions-item>
<el-descriptions-item label="签订日期">
{{ contractDetail.sign_date ? formatDate(contractDetail.sign_date) : '-' }}
</el-descriptions-item>
</el-descriptions>
<div v-else style="color:#909399; padding: 6px 0; margin-bottom: 12px;">
未找到合同信息
</div>
<div v-if="contractPayments && contractPayments.length">
<div style="font-weight: bold; margin-bottom: 10px; color: #606266;">合同相关支付列表</div>
<el-table :data="contractPayments" border size="small" style="width: 100%">
<el-table-column prop="id" label="流水号" width="90" />
<el-table-column label="创建日期" width="120">
<template slot-scope="scope">
{{ scope.row.created_at ? formatDate(scope.row.created_at) : '-' }}
</template>
</el-table-column>
<el-table-column prop="total_amount" label="金额" width="140" align="right">
<template slot-scope="scope">
<span style="color: #F56C6C; font-weight: bold;">¥{{ formatAmount(scope.row.total_amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="status_text" label="状态" width="120" />
<el-table-column prop="description" label="说明" min-width="160" show-overflow-tooltip />
</el-table>
</div>
<div v-else style="color:#909399; padding: 6px 0;">
暂无合同相关支付
</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;">
<!-- 单条数据 -->
<div v-if="config.flow.out_contracts.length === 1">
<span @click="showContract(config.flow.out_contracts[0].id)" style="cursor: pointer;">
本流程已关联资金预算管理:<span style="color:#409eff;">{{ config.flow.out_contracts[0].name }}</span>,点击查看完整信息链。
</span>
</div>
<!-- 多条数据 -->
<div v-else>
<span>本流程已关联{{ config.flow.out_contracts.length }}条资金预算管理,分别是</span>
<span v-for="(contract, index) in config.flow.out_contracts" :key="contract.id">
<span v-if="index > 0">、</span>
<span @click="showContract(contract.id)" style="cursor: pointer;color:#409eff;">{{ contract.name }}</span>
</span>
<span>,可分别点击查看完整信息链。</span>
</div>
</div>
<!-- out_pay 处理 -->
<div v-if="config.flow.hasOwnProperty('out_pay') && config.flow.out_pay && config.flow.out_pay.length > 0" style="margin-bottom: 10px;color:#F56C6C;">
<!-- 单条数据 -->
<div v-if="config.flow.out_pay.length === 1">
<span @click="showContract(config.flow.out_pay[0].contract_id)" style="cursor: pointer;">
本流程已关联资金预算管理:<span style="color:#409eff;">{{ config.flow.out_pay[0].contract?config.flow.out_pay[0].contract.name:'未知支出'+config.flow.out_pay[0].contract_id }}</span>,点击查看完整信息链。
</span>
</div>
<!-- 多条数据 -->
<div v-else>
<span>本流程已关联{{ config.flow.out_pay.length }}条资金预算管理,分别是</span>
<span v-for="(pay, index) in config.flow.out_pay" :key="pay.id">
<span v-if="index > 0">、</span>
<span @click="showContract(pay.contract_id)" style="cursor: pointer;color:#409eff;">{{ pay.contract?pay.contract.name:'未知支出'+pay.contract_id }}</span>
</span>
<span>,可分别点击查看完整信息链。</span>
</div>
</div>
</div>
<div class="form-container" id="print-content">
<DesktopForm
:device="device"
ref="desktopForm"
:config="config"
:is-first-node="isFirstNode"
:sub-form="subConfig"
:fields="fields"
:original-form="form"
:readable.sync="readableFields"
:script-content="scriptContent"
:writeable.sync="writeableFields"
:rules="rules"
:sub-rules="subRules"
:logs="config.logs"
></DesktopForm>
</div>
</template>
<!-- 审批日志-->
<div v-if="/\/detail/.test($route.path)" style="margin-top: 10px">
<div>流转记录</div>
<vxe-table
style="margin-top: 10px;"
show-footer
ref="table"
stripe
class="log-table-scroll"
keep-source
show-overflow
:column-config="{ resizable: true }"
:print-config="{}"
:export-config="{}"
:custom-config="{ mode: 'popup' }"
:footer-data="footerData"
:data="config.logs || []"
@cell-dblclick="cellDblclickEvent"
>
<vxe-column
type="seq"
width="62"
align="center"
field="seq"
title="编号"
/>
<vxe-column
width="140"
title="节点名称"
align="center"
field="node.name"
:formatter="({ cellValue }) => cellValue || '节点已调整'"
></vxe-column>
<vxe-column
width="80"
align="center"
title="办理状态"
field="status"
:formatter="({ cellValue }) => myStatus.get(cellValue)"
>
<template #default="{ row }">
<el-tag
size="mini"
:type="statusColor.get(row.status)"
effect="dark"
>{{ myStatus.get(row.status) }}</el-tag
>
</template>
</vxe-column>
<vxe-column
align="center"
width="80"
title="承办人员"
field="user.name"
></vxe-column>
<vxe-column
align="center"
width="200"
title="流转时间"
field="created_at"
:formatter="
({ cellValue }) =>
$moment(cellValue).format('YYYY年MM月DD日 HH:mm:ss')
"
></vxe-column>
<vxe-column
min-width="200"
header-align="center"
title="退回原因"
field="reason"
></vxe-column>
<vxe-column
align="center"
width="200"
title="办理时间"
field="updated_at"
>
<template #default="{ row }">
<span
:style="{
color:
row.deadline &&
row.updated_at &&
$moment(row.updated_at).isAfter(
$moment(row.deadline).endOf('day')
)
? 'red'
: '',
}"
>{{
$moment(row.updated_at).format("YYYY年MM月DD日 HH:mm:ss")
}}</span
>
</template>
</vxe-column>
<vxe-column align="center" title="耗时" field="use_time" width="120">
<template #default="{ row }">
<span>{{ diffTime(row.updated_at, row.created_at) }}</span>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="btns" ref="btns">
<template v-if="!/\/detail/.test($route.path)">
<el-button
v-if="$route.query.flow_id"
icon="el-icon-arrow-left"
type="danger"
size="small"
@click="isShowRollback = true"
>退回</el-button
>
<el-button
v-if="$route.query.flow_id"
icon="el-icon-caret-right"
type="primary"
plain
size="small"
@click="isShowForward = true"
>部门内转办</el-button
>
<el-button
v-if="$route.query.flow_id"
icon="el-icon-document-add"
type="info"
size="small"
@click="submit('only-submit')"
>暂存不流转</el-button
>
<el-button type="primary" size="small" @click="submit('assign')"
>保存并流转 <i class="el-icon-right"></i
></el-button>
<el-button
v-if="!$route.query.flow_id"
type="info"
size="small"
@click="$router.go(-1)"
>返回</el-button
>
</template>
<template v-else>
<el-button
v-if="$store.state.user.adminId === 1 && $route.query.flow_id"
icon="el-icon-arrow-left"
type="danger"
size="small"
@click="isShowRollback = true"
>退回</el-button
>
<el-button plain size="small" @click="$router.go(-1)">返回</el-button>
<el-button plain size="small" @click="print(false)">打印</el-button>
<!-- <el-button plain size="small" @click="print(true)">打印(带审批记录)</el-button> -->
<!-- <el-button plain size="small">下载附件</el-button> -->
</template>
</div>
</el-card>
<assign
ref="assign"
:visible.sync="isShowAssign"
:config="config"
:result="result"
></assign>
<forward
ref="forward"
:is-show.sync="isShowForward"
:flow="config.flow"
></forward>
<rollback
ref="rollback"
:is-show.sync="isShowRollback"
:flow="config.flow"
></rollback>
<el-backtop></el-backtop>
<!-- 更改时间 -->
<el-dialog
title="请选择时间"
:visible.sync="isShowTime"
:close-on-click-modal="false"
width="30%"
>
<!-- 日期时间选择器 -->
<el-date-picker
v-model="selectedDateTime"
type="datetime"
placeholder="选择日期时间"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
:clearable="false"
style="width: 100%"
></el-date-picker>
<div slot="footer" class="dialog-footer">
<el-button @click="isShowTime = false,timeId='',selectedDateTime='',selectedDateType=''">取消</el-button>
<el-button type="primary" @click="updateTime">确定</el-button>
</div>
</el-dialog>
<!-- 更改承办人员 -->
<el-dialog
title="请选择承办人员"
:visible.sync="isShowUserDialog"
:close-on-click-modal="false"
width="30%"
>
<!-- 用户选择下拉框 -->
<el-select
v-model="selectedUserId"
placeholder="请选择承办人员"
filterable
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.name"
:value="user.id"
></el-option>
</el-select>
<div slot="footer" class="dialog-footer">
<el-button @click="isShowUserDialog = false,currentRowId='',selectedUserId=''">取消</el-button>
<el-button type="primary" @click="updateUser">确定</el-button>
</div>
</el-dialog>
<!-- 打开支出 详情 -->
<vxe-modal
:id="contractModalId"
v-model="isShowModal"
:z-index="999999"
transfer
show-zoom
resize
:fullscreen="$store.getters.device === 'mobile'"
title="查看"
width="100%"
height="100%"
esc-closable
:padding="false"
>
<iframe
:src="contractUrl"
style="display: block;width: 100%;height: 100%;border: 0;"
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>
<script>
import Steps from "./components/Steps.vue";
import DesktopForm from "./DesktopForm.vue";
import MobileForm from "./MobileForm.vue";
import assign from "./components/assign.vue";
import forward from "./components/forward.vue";
import rollback from "./components/rollback.vue";
import RelatedFlows from "./components/RelatedFlows.vue";
import { generateRandomString } from '@/utils'
import { userListNoAuth, departmentListNoAuth } from "@/api/common";
import {
create,
deal,
fieldConfig, flowList,
preConfig,
preDeal,
updateNodeTime,
view,
} from "@/api/flow";
import { getPaymentsByFlowId, getPaymentsByContractId, getBudgetContractDetail, getPlannedExpenditure, getPlannedExpenditureTemplatesByCategory, getPaymentCategoryTemplateElements, getDetailTableFields, getOaFlowDetails, getTemplateElementDetail } 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";
import JSONBigint from 'json-bigint'
import { getToken, setToken } from "@/utils/auth";
export default {
components: {
Steps,
DesktopForm,
MobileForm,
assign,
forward,
rollback,
RelatedFlows,
},
data() {
return {
// vxe-ui v3.5+ 要求 modal.id
contractModalId: `flow-contract-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
isShowModal:false,
contractUrl:'',
printKey: 0,
isShowRollback: false,
isShowForward: false,
isShowAssign: false,
timeId: '',
isShowTime: false,
selectedDateTime: '',
selectedDateType: '',
isShowUserDialog: false,
selectedUserId: '',
userList: [],
currentRowId: '',
info: [],
config: {},
writeableFields: [],
readableFields: [],
subConfig: new Map(),
myStatus: new Map([
[-2, "会签回退"],
[-1, "退回"],
[0, "办理中"],
[1, "已完成"],
]),
statusColor: new Map([
[-2, "warning"],
[-1, "warning"],
[0, ""],
[1, "success"],
]),
form: {},
result: {},
fileList: {},
subFileList: {},
rules: {},
subRules: {},
flows: [],
csrf_token: '',
// 关联的支付信息
relatedPayments: [],
loadingPayments: false,
// 支付模板字段动态字段checklist/附件/detail_table/审批流程等)
paymentTemplateElements: [], // {id,name,type,field_type,options,model_id,sort_order,...}
loadingPaymentTemplateElements: false,
paymentDetailTableFieldsMap: {}, // { [elementId]: columns[] }
paymentFlowDetailsMap: {}, // { [flowId]: {display_name,no,title,custom_model_id,...} }
paymentChecklistOptionsMap: {}, // { [elementId]: options[] } 从 template-elements/{id} 拉取
// 合同相关支付列表
contractDetail: null,
contractPayments: [],
loadingContractPayments: false,
// 非直接支付:还原“非直接支出(PlannedExpenditure)”的模板字段与内容
indirectExpenditureMap: {}, // { [expenditureId]: PlannedExpenditureDetail }
indirectTemplateConfigMap: {}, // { [categoryId]: templateConfig }
indirectDetailTableFieldsMap: {}, // { [elementId]: columns[] }
oaFlowDetailsMap: {}, // { [flowId]: {display_name,no,title,custom_model_id,...} }
loadingIndirectExpenditures: false,
// 事前流程深度还原checklist options按预算模板元素定义拉取
plannedChecklistOptionsMap: {}, // { [elementId]: options[] }
// detail_table部门/人员映射
simpleDepartmentMap: {}, // { [id]: name }
simpleUserMap: {}, // { [id]: name }
loadedSimpleDepartments: false,
loadedSimpleUsers: false,
// 右侧抽屉(流程详情/会议纪要)
rightDrawerVisible: false,
rightDrawerType: '', // 'flow' | 'meetingMinutes'
rightDrawerTitle: '',
rightDrawerUrl: '',
meetingMinutesDetail: null,
loadingMeetingMinutes: false,
// fill_flow_title 自动填充:专用 watcher避免 deep watcher oldVal/newVal 引用问题)
_unwatchFillFlowTitle: null,
};
},
watch:{
isShowModal(val){
if(!val){
this.contractUrl = ''
}
},
},
methods: {
// 模板字段 key 兼容config 里可能是 key 或 field_key
getTplFieldKey(field) {
return (field && (field.key || field.field_key)) ? (field.key || field.field_key) : '';
},
// 相关事前流程字段是否渲染:
// 1. 最顶层拦截:如果画布配置中 visible === false则不显示
// 2. 对 oa_custom_model/out_custom_model 做"空值隐藏"
// 3. 对 meeting_minutes 和 attachment/file 做"空值隐藏"
shouldRenderPlannedField(expenditureId, field) {
if (!field) return false;
// 最顶层拦截如果画布配置中关闭了元素的显示visible === false则不显示
if (field.visible === false) {
return false;
}
// oa_custom_model/out_custom_model 类型:仅当有绑定值时才显示
if (field.element_type === 'oa_custom_model' || field.element_type === 'out_custom_model') {
return this.getOaCustomModelBindings(expenditureId, field).length > 0;
}
// meeting_minutes 类型仅当有会议纪要ID时才显示
if (field.element_type === 'meeting_minutes') {
const meetingMinuteId = this.extractMeetingMinuteId(
this.getIndirectFieldValue(expenditureId, this.getTplFieldKey(field))
);
return meetingMinuteId !== null && meetingMinuteId !== undefined;
}
// attachment/file 类型:仅当有文件项时才显示
if (field.element_type === 'attachment' || field.element_type === 'file') {
const fileItems = this.getPlannedFileItems(expenditureId, field);
return fileItems && fileItems.length > 0;
}
// 其他类型:默认显示(已通过 visible 检查)
return true;
},
normalizeFlowId(val) {
if (val === null || val === undefined || val === '') return null;
const s = String(val).trim();
if (!s) return null;
if (!/^\d+$/.test(s)) return null;
const n = Number(s);
return n && !Number.isNaN(n) ? n : null;
},
extractFlowIds(val) {
if (val === null || val === undefined || val === '') return [];
if (Array.isArray(val)) {
return val.map(v => this.normalizeFlowId(v)).filter(Boolean);
}
if (typeof val === 'number') {
return [this.normalizeFlowId(val)].filter(Boolean);
}
if (typeof val === 'string') {
return val
.split(/[,,、]/g)
.map(v => this.normalizeFlowId(v))
.filter(Boolean);
}
if (typeof val === 'object') {
const id = this.normalizeFlowId(val.instance_id ?? val.flow_instance_id ?? val.id);
return id ? [id] : [];
}
return [];
},
// ---------- 支付模板字段:辅助 ----------
isEmptyValue(val) {
if (val === null || val === undefined) return true;
if (typeof val === 'string') return val.trim() === '';
if (Array.isArray(val)) return val.length === 0;
if (typeof val === 'object') return Object.keys(val).length === 0;
return false;
},
isChecklistChecked(selected, value) {
const arr = Array.isArray(selected) ? selected : (selected ? [selected] : []);
// 与后端可能的 string/number 混用兼容
const target = value === null || value === undefined ? '' : String(value);
return arr.map(v => (v === null || v === undefined ? '' : String(v))).includes(target);
},
getPaymentChecklistRemark(el, optionValue, payment) {
const p = payment || {};
const key = `${el.id}_remark_${optionValue}`;
const v = p?.fields?.[key];
return v ? String(v) : '';
},
getPaymentTemplateElementValue(el, payment) {
const p = payment || {};
return p?.fields?.[el.id];
},
getVisiblePaymentTemplateElements(payment) {
const p = payment || {};
const list = Array.isArray(this.paymentTemplateElements) ? this.paymentTemplateElements : [];
return list.filter(el => {
const val = this.getPaymentTemplateElementValue(el, p);
if (el.type === 'checklist') {
const options = this.getPaymentChecklistOptions(el);
const anyChecked = (options || []).some(opt => this.isChecklistChecked(val, opt.value));
const anyRemark = (options || []).some(opt => !!this.getPaymentChecklistRemark(el, opt.value, p));
return anyChecked || anyRemark;
}
if (el.type === 'detail_table') {
return Array.isArray(val) && val.length > 0;
}
if (el.type === 'form_element' && el.field_type === 'attachment') {
return Array.isArray(val) && val.length > 0;
}
return !this.isEmptyValue(val);
});
},
getPaymentChecklistOptions(el) {
const id = el?.id ? Number(el.id) : null;
if (id && this.paymentChecklistOptionsMap?.[id] && this.paymentChecklistOptionsMap[id].length) {
return this.paymentChecklistOptionsMap[id];
}
return Array.isArray(el?.options) ? el.options : [];
},
async loadPaymentChecklistOptionsIfNeeded(elementId) {
const id = Number(elementId);
if (!id || Number.isNaN(id)) return;
if (this.paymentChecklistOptionsMap?.[id]) return;
try {
const detail = await getTemplateElementDetail(id);
const opts = Array.isArray(detail?.options) ? detail.options : [];
this.$set(this.paymentChecklistOptionsMap, id, opts);
} catch (e) {
this.$set(this.paymentChecklistOptionsMap, id, []);
}
},
getPaymentDetailTableFields(elementId) {
return this.paymentDetailTableFieldsMap?.[elementId] || [];
},
getPaymentFlowDisplayInfo(flowId) {
if (!flowId) return null;
const d = this.paymentFlowDetailsMap?.[flowId];
if (!d) return `流程ID: ${flowId}`;
return d.display_name || [d.no, d.title].filter(Boolean).join(' - ') || `流程ID: ${flowId}`;
},
// 附件:兼容字符串/对象/数组
getPaymentFileItems(el, payment) {
const v = this.getPaymentTemplateElementValue(el, payment);
if (!v) return [];
const toItem = (x, idx) => {
if (!x) return null;
if (typeof x === 'string') {
const s = x.trim();
if (!s) return null;
const name = s.split('/').pop();
return { name: name || `附件${idx + 1}`, url: s };
}
if (typeof x === 'object') {
const url = x.url || x.path || x.file_url || x.download_url || x.href || x.preview_url;
const name =
x.original_name ||
x.file_name ||
x.name ||
x.filename ||
x.originalName ||
(url ? String(url).split('/').pop() : null);
if (url || name) return { name: name || `附件${idx + 1}`, url };
return null;
}
return null;
};
const arr = Array.isArray(v) ? v : [v];
return arr.map(toItem).filter(Boolean);
},
// ---------- 合同相关 ----------
getContractIdFromPayment(payment) {
const p = payment || {};
if (p.related_type === 'contract' && p.related_id) return p.related_id;
if (p.contract_id) return p.contract_id;
return null;
},
async loadContractPayments(contractId, currentPaymentId) {
const cid = Number(contractId);
if (!cid || Number.isNaN(cid)) {
this.contractPayments = [];
return;
}
this.loadingContractPayments = true;
try {
// 与打印页一致:按 payment.id 升序、包含当前支付在内,用于正确计算“第几次支付”
const list = await getPaymentsByContractId(cid, { all: true, order_by: 'id', order: 'asc' });
const arr = Array.isArray(list) ? list : [];
this.contractPayments = arr.slice();
} catch (e) {
this.contractPayments = [];
} finally {
this.loadingContractPayments = false;
}
},
async loadContractDetail(contractId) {
const cid = Number(contractId);
if (!cid || Number.isNaN(cid)) {
this.contractDetail = null;
return;
}
try {
const detail = await getBudgetContractDetail(cid);
this.contractDetail = detail || null;
} catch (e) {
this.contractDetail = null;
}
},
getContractPaymentStats(currentPaymentId) {
const list = Array.isArray(this.contractPayments) ? this.contractPayments : [];
const total = list.length;
const curId = Number(currentPaymentId);
const idx = list.findIndex(p => Number(p?.id) === curId);
return {
total_count: total,
current_index: idx >= 0 ? idx + 1 : null
};
},
/**
* 为 fill_flow_title=1 的字段建立精确 watcher
* - 只在新建流程时生效
* - 保留 isFirstNode 判断
* - 输入/默认值变更都能触发(不依赖 deep watcher
*/
setupFillFlowTitleWatcher(fields) {
try {
if (typeof this._unwatchFillFlowTitle === 'function') {
this._unwatchFillFlowTitle();
}
} catch (e) {
// ignore
}
this._unwatchFillFlowTitle = null;
if (this.$route.query.flow_id) return; // 只处理新建
const list = Array.isArray(fields) ? fields : [];
const fillField = list.find((f) => Number(f?.fill_flow_title) === 1 && f?.type === 'text' && f?.name);
if (!fillField) return;
const fieldName = fillField.name;
this._unwatchFillFlowTitle = this.$watch(
() => (this.form ? this.form[fieldName] : undefined),
(newVal) => {
if (!this.isFirstNode || this.$route.query.flow_id) return;
const v = newVal === null || newVal === undefined ? "" : String(newVal).trim();
if (!v) return;
this.$set(this.form, "flow_title", v);
},
{ immediate: true }
);
},
// 打开 支出的链接
showContract(id){
this.contractUrl = `${process.env.VUE_APP_BASE_API}/ht/#/contract-flow?auth_token=${window.encodeURIComponent(
getToken()
)}&out_contract_id=`+id
this.isShowModal = true
},
// 处理url中default_json
handleDefaultJSON() {
console.log("123")
this.form['form_canal'] = 'oa'
try {
if(!this.$route.query?.default_json) {
// 即使没有default_json也要处理form_canal参数
if(this.$route.query?.form_canal) {
this.form['form_canal'] = this.$route.query.form_canal
}
return
}
const res = JSON.parse(this.$route.query?.default_json)
for (let key in this.$route.query) {
if(/^out_(.*)_id/.test(key)) {
this.form[key] = this.$route.query[key]
}
if(/^borrow_id/.test(key)) {
this.form[key] = this.$route.query[key]
}
if(/^form_canal/.test(key)){
this.form[key] = this.$route.query[key]
}
}
for (let key in res) {
try {
let jsonObj = JSON.parse(res[key]);
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
if (this.form.hasOwnProperty(key)) {
this.form[key] = res[key];
}
}
}
} catch (err) {
console.error(err)
}
},
async print(isLog=false) {
const _this = this
let customModelId = this.config.customModel.id || this.$route.query.module_id
const modelRes = await fieldConfig(customModelId,true)
let pickTemplate = 0
const printTemplates = [{
id: 0,
name: '基础模版',
print_format: modelRes.customModel.print_format
},...modelRes.customModel.print_formats]
const h = this.$createElement;
await this.$msgbox({
title: '打印模版选择',
message: h('div',{
class: 'print-template-radios',
key: this.printKey++
},[
h('div',{
},printTemplates.map(i => h('div',{
style: {
display: 'flex',
'align-items': 'center',
'margin-top': '4px',
}
},[
h('span',{
class: 'el-radio__input'
},[
h('span', {
class: 'el-radio__inner custom-cursor-on-hover'
}),
h('input', {
style: {
cursor: 'pointer',
opacity: 0,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 0
},
attrs: {
// 添加属性
type: "radio",
id: `print-radio-${i.id}`,
name: "Radio",
value: i.id,
checked: pickTemplate === i.id,
},
on: {
change: () => {
pickTemplate = i.id
}
}
})
]),
h('label', {
style: {
flex: 1
},
attrs: {
for: `print-radio-${i.id}`
},
class: 'el-radio__label',
}, i.name)
])))
]),
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
})
const printText = printTemplates.find(i => i.id === pickTemplate)?.print_format
if(isLog) {
const res = await this.$refs['table'].exportData({
type: 'html',
download: false
})
await print.bind(this)(printText, isLog, _this.config.flow, res.content)
} else {
await print.bind(this)(printText, isLog, _this.config.flow)
}
},
generateForm(object, fields, relation = false, pname) {
fields.forEach((field) => {
if (field.rules && field.rules.length > 0 && this.writeableFields.find(i => i === field.id) && !relation) {
this.rules[field.name] = field.rules.map((rule) => {
switch (rule) {
case "required":
if (field.type === 'relation') {
return {
validator: (myRule, value, callback) => {
if (value instanceof Array && value.length > 0) {
callback()
} else {
callback(`请填写${field.label}`)
}
},
message: `请填写${field.label}`,
trigger: "blur",
};
} else {
return {
required: true,
message: `请填写${field.label}`,
trigger: "blur",
};
}
default:
return {
validator: (myRule, value, callback) => {
if (validation.get(rule).test(value) || value === '') {
callback();
} else {
callback(
new Error(
`${field.label}必须为${validationName.get(rule)}`
)
);
}
},
trigger: "blur",
message: `${field.label}必须为${validationName.get(rule)}`,
};
}
});
}
if (relation) {
this.subRules[`${pname}_rules`][field.name] = field.rules.map((rule) => {
switch (rule) {
case "required":
return {
required: true,
message: `请填写${field.label}`,
};
default:
return {
validator: this.device === 'desktop' ? ({ cellValue }) => {
return new Promise((resolve, reject) => {
if (validation.get(rule).test(cellValue) || cellValue === '') {
resolve()
} else {
reject(
new Error(
`${field.label}必须为${validationName.get(rule)}`
)
);
}
})
} : (myRule, value, callback) => {
if (validation.get(rule).test(value) || value === '') {
callback();
} else {
callback(
new Error(
`${field.label}必须为${validationName.get(rule)}`
)
);
}
},
trigger: "blur",
pattern: validation.get(rule),
message: `${field.label}必须为${validationName.get(rule)}`,
};
}
});
}
if (field.type === "relation") {
this.subRules[`${field.name}_rules`] = {}
let temp = {}
this.subConfig.get(field.sub_custom_model_id)?.customModel?.fields?.forEach(field => temp[field.name] = '')
object[field.name] = [temp];
this.generateForm(
object[field.name][0],
this.subConfig.get(field.sub_custom_model_id)?.customModel?.fields,
true,
field.name
);
} else {
if (/\/detail/.test(this.$route.path) && this.$route.query.flow_id) {
object[field.name] = "";
} else {
if (this.writeableFields.indexOf(field.id) !== -1 || this.readableFields.indexOf(field.id) !== -1) {
object[field.name] = (this.writeableFields.indexOf(field.id) !== -1 && field.default_value) ? field.default_value : (field.type === 'file' ? [] : "");
}
}
}
});
// 初始化关联流程字段
if (!object.hasOwnProperty('related_flow_ids')) {
object['related_flow_ids'] = '';
}
// 查找 fill_flow_title=1 的字段
const fillFlowTitleField = (fields || []).find(f =>
Number(f?.fill_flow_title) === 1 && ['text', 'textarea', 'select'].includes(f?.type)
);
// 设置 flow_title 初始值
if (this.config?.flow?.title) {
// 已有流程,使用已有标题
this.form['flow_title'] = this.config.flow.title;
} else if (fillFlowTitleField && !this.$route.query.flow_id) {
// 新建流程且存在 fill_flow_title=1 的字段,不设置默认值,等待字段值填充
// 注意:这里不判断 isFirstNode因为 generateForm 可能在 isFirstNode 计算之前调用
// 实际的自动填充逻辑在 watch 中会再次检查 isFirstNode
this.form['flow_title'] = '';
} else {
// 新建流程且不存在 fill_flow_title=1 的字段,使用默认格式
this.form['flow_title'] = `${this.config.customModel.name}${this.$store.getters.name} ${this.$moment().format('YYYY-MM-DD HH:mm')}`;
}
},
formatTime(time) {
const days = parseInt(time / (1000 * 60 * 60 * 24));
const hours = parseInt((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = parseInt((time % (1000 * 60 * 60)) / (1000 * 60));
const seconds = (time % (1000 * 60)) / 1000;
return `${days > 0 ? days + "天" : ""}${
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 ids = this.getPlannedExpenditureIdsFromPayment(payment);
if (ids.length === 0) return;
// 并发拉取 PlannedExpenditure 详情
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();
// checklist从元素定义取“原始 options”
const checklistElementIds = 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);
}
if (f && f.element_type === 'checklist' && f.element_id) {
checklistElementIds.add(f.element_id);
}
});
});
const idsToLoad = Array.from(detailTableElementIds);
const colResults = await Promise.all(idsToLoad.map(id => getDetailTableFields(id).catch(() => [])));
let needDept = false;
let needUser = false;
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);
if (arr.some(f => f && f.field_type === 'department')) needDept = true;
if (arr.some(f => f && f.field_type === 'user')) needUser = true;
});
if (needDept) {
await this.loadSimpleDepartmentMap();
}
if (needUser) {
await this.loadSimpleUserMap();
}
// checklist预加载 options按元素定义
const checklistIds = Array.from(checklistElementIds);
for (const id of checklistIds) {
// eslint-disable-next-line no-await-in-loop
await this.loadPlannedChecklistOptionsIfNeeded(id);
}
// 批量获取 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;
}
},
// 从支付对象中提取“关联事前流程(PlannedExpenditure)”ID 列表(兼容新旧结构)
getPlannedExpenditureIdsFromPayment(payment) {
const ids = [];
const p = payment || {};
// 新结构payment.related_type / related_id
if (p.related_type === 'planned_expenditure' && p.related_id) {
ids.push(p.related_id);
}
// 新结构payment.details[*].related_type / related_id
const details = Array.isArray(p.details) ? p.details : [];
details.forEach(d => {
if (!d) return;
if (d.related_type === 'planned_expenditure' && d.related_id) {
ids.push(d.related_id);
}
// 旧结构兼容expenditure_type/expenditure_id
if (d.expenditure_type === 'indirect' && d.expenditure_id) {
ids.push(d.expenditure_id);
}
});
return Array.from(new Set(ids.filter(Boolean).map(v => Number(v)).filter(v => v && !Number.isNaN(v))));
},
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 多条绑定(用于“顿号分隔”的展示)
getOaCustomModelBindings(expenditureId, fieldOrKey) {
const exp = this.indirectExpenditureMap?.[expenditureId];
const bindings = Array.isArray(exp?.flow_bindings) ? exp.flow_bindings : [];
const fieldKey = typeof fieldOrKey === 'object' ? this.getTplFieldKey(fieldOrKey) : String(fieldOrKey || '');
const elementIdRaw = typeof fieldOrKey === 'object' ? fieldOrKey?.element_id : null;
const elementId = elementIdRaw ? Number(elementIdRaw) : null;
// 1) 优先按 field_key / element_id 精确匹配flow_bindings 的 field_key/element_id 可能只存在其一)
const matched = bindings.filter(b => {
if (!b) return false;
if (fieldKey && b.field_key === fieldKey) return true;
if (elementId && b.element_id && Number(b.element_id) === elementId) return true;
return false;
});
if (matched.length) return matched;
// 2) 没有绑定时,尝试从字段值解析多个 flow_id
if (!fieldKey) return [];
const rawVal = this.getIndirectFieldValue(expenditureId, fieldKey);
const ids = this.extractFlowIds(rawVal);
if (!ids.length) return [];
return ids.map(id => {
const hit = bindings.find(b => Number(b?.flow_instance_id) === Number(id));
return {
flow_instance_id: id,
flow_custom_model_id: hit?.flow_custom_model_id || null,
flow_display_name: hit?.flow_display_name || `流程流水号: ${id}`,
};
});
},
// 判断 oa_custom_model 字段是否有值
hasOaCustomModelValue(expenditureId, field) {
return this.getOaCustomModelBindings(expenditureId, field).length > 0;
},
// ---------- 事前流程 checklistoptions/remark ----------
async loadPlannedChecklistOptionsIfNeeded(elementId) {
const id = Number(elementId);
if (!id || Number.isNaN(id)) return;
if (this.plannedChecklistOptionsMap?.[id]) return;
try {
const detail = await getTemplateElementDetail(id);
const opts = Array.isArray(detail?.options) ? detail.options : [];
this.$set(this.plannedChecklistOptionsMap, id, opts);
} catch (e) {
this.$set(this.plannedChecklistOptionsMap, id, []);
}
},
getPlannedChecklistOptions(field) {
const id = field?.element_id ? Number(field.element_id) : null;
if (id && this.plannedChecklistOptionsMap?.[id] && this.plannedChecklistOptionsMap[id].length) {
return this.plannedChecklistOptionsMap[id];
}
return Array.isArray(field?.options) ? field.options : [];
},
getPlannedChecklistRemark(expenditureId, field, optionValue) {
const elementId = field?.element_id;
if (!elementId || optionValue === null || optionValue === undefined || optionValue === '') return '';
const remarkKey = `element_${elementId}_remark_${optionValue}`;
const v = this.getIndirectFieldValue(expenditureId, remarkKey);
return v ? String(v) : '';
},
// ---------- 事前流程附件/文件 ----------
// 构建完整的文件URL如果是相对路径自动拼接当前域名
buildFileUrl(path) {
if (!path) return null;
const pathStr = String(path).trim();
if (!pathStr) return null;
// 如果已经是完整URLhttp:// 或 https://),直接返回
if (pathStr.startsWith('http://') || pathStr.startsWith('https://')) {
return pathStr;
}
// 如果是相对路径(以 / 开头),拼接当前域名
if (pathStr.startsWith('/')) {
const origin = window.location.origin;
return `${origin}${pathStr}`;
}
// 其他情况,尝试拼接(可能是 storage/files/xxx 这种格式)
if (pathStr.includes('/')) {
const origin = window.location.origin;
return `${origin}/${pathStr.replace(/^\//, '')}`;
}
// 无法处理的格式,返回原始值
return pathStr;
},
// 处理附件点击
handlePlannedFileClick(file) {
if (file.url) {
try {
window.open(file.url, '_blank');
} catch (error) {
this.$message.error('打开文件失败:' + error.message);
}
} else {
this.$message.warning('文件链接不可用');
}
},
getPlannedFileItems(expenditureId, field) {
const v = this.getIndirectFieldValue(expenditureId, field?.key);
if (!v) return [];
const toItem = (x, idx) => {
if (!x) return null;
if (typeof x === 'string') {
const s = x.trim();
if (!s) return null;
const name = s.split('/').pop();
const url = this.buildFileUrl(s);
return { name: name || `附件${idx + 1}`, url };
}
if (typeof x === 'object') {
// 尝试从多个可能的字段中获取路径
let rawPath = x.url || x.path || x.file_url || x.download_url || x.href || x.preview_url || x.value || x.file_path || x.folder;
// 如果还是没有,检查对象的所有值,看是否有字符串类型的路径
if (!rawPath) {
for (const [key, value] of Object.entries(x)) {
if (typeof value === 'string' && (value.includes('/') || value.includes('storage') || value.includes('files'))) {
rawPath = value;
break;
}
}
}
const url = rawPath ? this.buildFileUrl(rawPath) : null;
const name =
x.original_name ||
x.file_name ||
x.name ||
x.filename ||
x.originalName ||
(url ? String(url).split('/').pop() : null) ||
(rawPath ? String(rawPath).split('/').pop() : null);
return (url || name) ? { name: name || `附件${idx + 1}`, url } : null;
}
return null;
};
const arr = Array.isArray(v) ? v : [v];
return arr.map(toItem).filter(Boolean);
},
// ---------- detail_table部门/人员映射 ----------
async loadSimpleDepartmentMap() {
if (this.loadedSimpleDepartments) return;
try {
const list = await departmentListNoAuth({ rows: 10000 });
const arr = Array.isArray(list) ? list : (list?.data || []);
const map = {};
(arr || []).forEach(d => {
if (d && d.id) map[d.id] = d.name || d.title || d.label || String(d.id);
});
this.simpleDepartmentMap = map;
} catch (e) {
this.simpleDepartmentMap = {};
} finally {
this.loadedSimpleDepartments = true;
}
},
async loadSimpleUserMap() {
if (this.loadedSimpleUsers) return;
try {
const list = await userListNoAuth({ rows: 10000 });
const arr = Array.isArray(list) ? list : (list?.data || []);
const map = {};
(arr || []).forEach(u => {
if (u && u.id) map[u.id] = u.name || u.realname || u.title || u.label || String(u.id);
});
this.simpleUserMap = map;
} catch (e) {
this.simpleUserMap = {};
} finally {
this.loadedSimpleUsers = true;
}
},
formatDetailTableCell(value, col) {
if (value === null || value === undefined || value === '') return '-';
const type = col?.field_type;
if (type === 'department') {
return this.simpleDepartmentMap?.[value] || `部门ID: ${value}`;
}
if (type === 'user') {
return this.simpleUserMap?.[value] || `用户ID: ${value}`;
}
return value;
},
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;
},
async openFlowDrawer({ flowId, customModelId, title }) {
const fid = Number(flowId);
if (!fid || Number.isNaN(fid)) return;
let cmid = customModelId ? Number(customModelId) : null;
if (!cmid || Number.isNaN(cmid)) {
// 优先从已批量缓存的 flow details 取
const d = this.oaFlowDetailsMap?.[fid];
cmid = d?.custom_model_id ? Number(d.custom_model_id) : null;
}
if (!cmid || Number.isNaN(cmid)) {
// 兜底:调用 OA flow/view 获取 customModel.id
try {
const res = await view(fid);
cmid = Number(res?.customModel?.id || res?.flow?.custom_model_id);
} catch (e) {
cmid = null;
}
}
if (!cmid || Number.isNaN(cmid)) {
this.$message && this.$message.warning ? this.$message.warning('无法打开流程:缺少 custom_model_id') : null;
return;
}
this.rightDrawerType = 'flow';
this.rightDrawerTitle = title || `流程 ${fid}`;
this.rightDrawerUrl = `/oa/#/flow/detail?module_id=${cmid}&flow_id=${fid}&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];
// 支付模板元素(用于展示 fields
await this.loadPaymentTemplateElements(payment);
// 合同相关支付列表(若有合同)
const contractId = this.getContractIdFromPayment(payment);
if (contractId) {
await this.loadContractDetail(contractId);
await this.loadContractPayments(contractId, payment.id);
} else {
this.contractDetail = null;
this.contractPayments = [];
}
// 关联事前流程:加载 PlannedExpenditure 模板与内容(兼容 payment.related_type / 明细 related_type
if (this.getPlannedExpenditureIdsFromPayment(payment).length > 0) {
await this.loadIndirectExpenditures(payment);
} else {
this.indirectExpenditureMap = {};
}
} else {
this.relatedPayments = [];
this.indirectExpenditureMap = {};
this.paymentTemplateElements = [];
this.contractDetail = null;
this.contractPayments = [];
}
} catch (err) {
console.error('加载支付信息失败:', err);
this.relatedPayments = [];
this.indirectExpenditureMap = {};
this.paymentTemplateElements = [];
this.contractDetail = null;
this.contractPayments = [];
} finally {
this.loadingPayments = false;
}
},
// 加载“支付分类绑定的模板元素”并预加载 checklist/options 与 detail_table 列,以及 approval_flow 的流程展示信息
async loadPaymentTemplateElements(payment) {
const categoryId = Number(payment?.payment_category_id);
if (!categoryId || Number.isNaN(categoryId)) {
this.paymentTemplateElements = [];
return;
}
this.loadingPaymentTemplateElements = true;
try {
const list = await getPaymentCategoryTemplateElements(categoryId);
const arr = Array.isArray(list) ? list.slice() : [];
arr.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
this.paymentTemplateElements = arr;
// checklist按元素定义拉 options最原始
const checklistIds = arr
.filter(el => el && el.type === 'checklist' && el.id)
.map(el => Number(el.id))
.filter(Boolean);
for (const id of checklistIds) {
// eslint-disable-next-line no-await-in-loop
await this.loadPaymentChecklistOptionsIfNeeded(id);
}
// detail_table预加载列定义
const dtIds = arr
.filter(el => el && el.type === 'detail_table' && el.id)
.map(el => Number(el.id))
.filter(Boolean);
const colResults = await Promise.all(dtIds.map(id => getDetailTableFields(id).catch(() => [])));
colResults.forEach((cols, idx) => {
const id = dtIds[idx];
const c = Array.isArray(cols) ? cols.slice() : [];
c.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
this.$set(this.paymentDetailTableFieldsMap, id, c);
});
// approval_flow批量拿 no/title 展示
const flowIds = [];
arr.forEach(el => {
if (el && el.type === 'approval_flow' && el.id) {
const v = this.getPaymentTemplateElementValue(el, payment);
if (v) {
const fid = Number(v);
if (fid && !Number.isNaN(fid) && !this.paymentFlowDetailsMap?.[fid]) flowIds.push(fid);
}
}
});
const uniq = Array.from(new Set(flowIds));
if (uniq.length) {
try {
const map = await getOaFlowDetails(uniq);
if (map && typeof map === 'object') {
Object.keys(map).forEach(k => {
const fid = Number(k);
if (fid && map[k]) {
this.$set(this.paymentFlowDetailsMap, fid, map[k]);
}
});
}
} catch (e) {
// ignore
}
}
} catch (e) {
this.paymentTemplateElements = [];
} finally {
this.loadingPaymentTemplateElements = false;
}
},
async getConfig() {
const loading = this.$loading({
lock: true,
text: "拼命加载中",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.8)",
});
// 路由为detail为详情
if (/\/detail/.test(this.$route.path) && this.$route.query.flow_id) {
try {
const res = await view(this.$route.query.flow_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.readableFields = /\/detail/.test(this.$route.path)
? this.fields?.map((i) => i.id)
: this.config?.currentNode?.readable || [];
this.writeableFields = this.config?.currentNode?.writeable || [];
// 生成空的form表单对象
this.generateForm(this.form, fields);
// 加载关联流程ID如果有
if (res?.flow?.related_flows && res.flow.related_flows.length > 0) {
const relatedIds = res.flow.related_flows.map(f => f.id).join(',');
this.form.related_flow_ids = relatedIds;
}
// form赋值
const { data } = res?.flow;
for (let key in data) {
try {
let jsonObj = JSON.parse(data[key]);
jsonObj.forEach(item => {
// 遍历对象中的每个键值对
for (const key in item) {
if (typeof item[key] === 'string') {
try {
// 尝试解析字符串为 JSON 对象
const parsedValue = JSONBigint({ storeAsString: true }).parse(item[key]);
// 如果解析成功,替换原始字符串
item[key] = parsedValue;
} catch (e) {
// 如果解析失败,继续保持原始字符串
}
}
}
})
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
// 允许所有字段被赋值,包括 _display 字段
if (this.form.hasOwnProperty(key) || key.endsWith('_display')) {
if (data[key] instanceof Array) {
if (data[key].length > 0 && data[key][0].hasOwnProperty('url')) {
this.form[key] = data[key].map(i => ({
name: i.original_name,
url: i.url,
response: i,
}))
} else {
this.form[key] = ''
}
} else {
this.form[key] = data[key];
}
}
}
}
this.form = Object.assign({}, this.form);
loading.close();
// 加载关联的支付信息(详情模式下)
if (this.$route.query.flow_id) {
this.loadRelatedPayments();
}
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
} else if (!this.$route.query.flow_id) {
// 新建
try {
this.csrf_token = generateRandomString()
const res = await preConfig(this.$route.query.module_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.readableFields = /\/detail/.test(this.$route.path)
? this.fields?.map((i) => i.id)
: this.config?.currentNode?.readable || [];
this.writeableFields = this.config?.currentNode?.writeable || [];
this.generateForm(this.form, fields);
this.handleDefaultJSON();
this.form = Object.assign({}, this.form);
// 新建完成后建立 fill_flow_title watcher确保 default_json 等已写入后再监听)
this.setupFillFlowTitleWatcher(fields);
loading.close();
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
} else {
// 待办
try {
const res = await preDeal(this.$route.query.flow_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.readableFields = /\/detail/.test(this.$route.path)
? this.fields?.map((i) => i.id)
: this.config?.currentNode?.readable || [];
this.writeableFields = this.config?.currentNode?.writeable || [];
this.generateForm(this.form, fields);
// 加载关联流程ID如果有
if (res?.flow?.related_flows && res.flow.related_flows.length > 0) {
const relatedIds = res.flow.related_flows.map(f => f.id).join(',');
this.form.related_flow_ids = relatedIds;
}
this.handleDefaultJSON();
const { data } = res?.flow;
for (let key in data) {
try {
let jsonObj = JSON.parse(data[key]);
jsonObj.forEach(item => {
// 遍历对象中的每个键值对
for (const key in item) {
if (typeof item[key] === 'string') {
try {
// 尝试解析字符串为 JSON 对象
const parsedValue = JSONBigint({ storeAsString: true }).parse(item[key]);
// 如果解析成功,替换原始字符串
item[key] = parsedValue;
} catch (e) {
// 如果解析失败,继续保持原始字符串
}
}
}
})
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
// 允许所有字段被赋值,包括 _display 字段
if (this.form.hasOwnProperty(key) || key.endsWith('_display')) {
if (data[key] instanceof Array) {
if (data[key].length > 0) {
this.form[key] = data[key];
} else {
this.form[key] = ''
}
}
if (data[key] && data[key] !== 'null' && data[key] !== 'undefined') {
this.form[key] = data[key];
}
}
}
}
this.form = Object.assign({}, this.form);
loading.close();
// 加载关联的支付信息(办理模式下)
if (this.$route.query.flow_id) {
this.loadRelatedPayments();
}
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
}
},
async submit(type) {
if (window.$_uploading) {
this.$message.warning("文件正在上传中")
return
}
let copyForm;
try {
await this.$refs['desktopForm'].validate()
} catch (err) {
console.warn(err)
this.$message.warning('数据校验失败')
return
}
copyForm = deepCopy(this.$refs["desktopForm"].form);
const uploadHandler = (form, fields) => {
let keys = Object.keys(form)
if(fields){
keys.forEach(key => {
if (form[key] instanceof Array) {
if (form[key].length > 0 && typeof form[key][0] === 'object') {
console.log("key",key,fields)
const myField = fields.find(field => field.name === key)
if (myField) {
if (myField.type === 'file') {
form[key] = form[key].map(i => i.hasOwnProperty('response') ? i.response : i)
} else {
form[key].forEach(i => {
uploadHandler(i, this.subConfig.get(myField.sub_custom_model_id)?.customModel?.fields)
})
}
}
} else if (form[key].length > 0) {
} else {
form[key] = ''
}
} else {
if (form[key] === 'null' || form[key] === 'undefined') {
form[key] = ''
}
}
})
}
}
console.log("copyForm",copyForm,this.fields)
console.log("this.writeableFields",this.writeableFields)
uploadHandler(copyForm, this.fields)
for (let key in copyForm) {
let myField = this.fields.find(i => i.name === key)
// 保留关联流程ID字段即使它不在writeableFields中
if (key === 'related_flow_ids') {
continue;
}
if (myField && this.writeableFields.indexOf(myField.id) === -1) {
delete copyForm[key]
}
}
copyForm["current_node_id"] = this.config.currentNode.id;
try {
let callback = () => {};
switch (type) {
case "only-submit":
if (this.$route.query.flow_id) {
copyForm["temporary_save"] = 1;
}
callback = () => this.$router.push("/flow/list/todo")
break;
case "assign":
if (this.$route.query.flow_id) {
copyForm["temporary_save"] = 0;
}
callback = () => (this.isShowAssign = true);
break;
}
// 确保 related_flow_ids 被包含在提交数据中
// 无论是否有值,都要传递这个字段,以便后端能正确处理
copyForm.related_flow_ids = this.form.related_flow_ids || ''
console.log("copyForm",copyForm,this.$refs["desktopForm"].form)
console.log("related_flow_ids:", copyForm.related_flow_ids, "form.related_flow_ids:", this.form.related_flow_ids)
// return
if (this.$route.query.flow_id) {
copyForm.id = this.$route.query.flow_id;
const { flow, is_last_handled_log } = await deal(
this.$route.query.flow_id,
copyForm
);
this.result = flow;
if (!is_last_handled_log) {
await this.$alert(
"办理成功,其他会签办理完成后,由最后办理的成员流转到下一节点。",
"提示",
{
showClose: false,
}
);
callback = () => this.$router.push("/flow/list/todo");
}
} else {
copyForm['csrf_token'] = this.csrf_token
this.result = await create(this.$route.query.module_id, copyForm);
// 如果流程创建成功且在iframe中发送postMessage通知父窗口
if (this.result && this.result.id && window.self !== window.top) {
try {
window.parent.postMessage({
type: 'OA_FLOW_CREATED',
flowId: this.result.id,
success: true,
message: '流程创建成功'
}, '*')
} catch (error) {
console.error('发送postMessage失败:', error)
}
}
}
callback();
} catch (err) {
console.error(err);
// 如果流程创建失败且在iframe中发送postMessage通知父窗口
if (window.self !== window.top) {
try {
window.parent.postMessage({
type: 'OA_FLOW_CREATED',
flowId: null,
success: false,
message: err.message || '流程创建失败'
}, '*')
} catch (postError) {
console.error('发送postMessage失败:', postError)
}
}
}
},
async cellDblclickEvent({ row, column }) {
// if(this.$store.state.user.username !== 'admin') return
if(!this.$store.state.user.roles.includes("全局流程监管")) return
if(column.field === 'created_at' || column.field === 'updated_at') {
this.timeId = row.id
this.selectedDateType = column.field
this.selectedDateTime = row[column.field]
this.isShowTime = true
}
if(column.field === "user.name"){
this.currentRowId = row.id
this.selectedUserId = row.user ? row.user.id : ''
this.isShowUserDialog = true
this.getUserList()
}
},
updateTime() {
updateNodeTime({
id: this.timeId,
date: this.selectedDateTime,
date_type: this.selectedDateType
}).then(_ => {
this.$message.success('更新成功')
this.timeId = ''
this.selectedDateTime = ''
this.selectedDateType = ''
this.isShowTime = false
this.getConfig()
})
},
async getUserList() {
try {
const userRes = await userListNoAuth({
page: 1,
rows: 9999,
})
this.userList = userRes.data || []
} catch (err) {
console.error(err)
this.$message.error('获取用户列表失败')
}
},
updateUser() {
if (!this.selectedUserId) {
this.$message.warning('请选择承办人员')
return
}
updateNodeTime({
id: this.currentRowId,
user_id: this.selectedUserId
}).then(_ => {
this.$message.success('更新成功')
this.currentRowId = ''
this.selectedUserId = ''
this.isShowUserDialog = false
this.getConfig()
})
},
},
computed: {
device() {
return this.$store.state.app.device;
},
fields() {
return this.config?.customModel?.fields || [];
},
// 放到data中为了修改
// readableFields() {
// return /\/detail/.test(this.$route.path)
// ? this.fields?.map((i) => i.id)
// : this.config?.currentNode?.readable || [];
// },
// writeableFields() {
// return this.config?.currentNode?.writeable || [];
// },
node() {
return this.config?.currentNode || {};
},
scriptContent() {
if (this.config?.customModel?.view_js && this.$route.query.flow_id && /\/detail/.test(this.$route.path)) {
return this.config?.customModel?.view_js;
} else if (this.config?.customModel?.js && !/\/detail/.test(this.$route.path)) {
return this.config?.customModel?.js;
}
},
diffTime() {
return function (end, start) {
const diff = this.$moment(end).diff(this.$moment(start));
return this.formatTime(diff);
};
},
footerData() {
const diff = this.$moment(this.config?.logs?.at(-1)?.updated_at).diff(
this.$moment(this.config?.logs?.at(0)?.created_at)
);
return [
{
seq: "总耗时",
use_time: this.formatTime(diff),
},
];
},
isFirstNode() {
return this.config?.logs?.length === 0 || this.config?.currentNode?.category === 'start'
},
isSinglePage() {
// 仅当显式传参 isSinglePage=1 时才启用“单页模式”
// 不能用 window.self !== window.top 作为判定:被主框架 iframe 引入时也会命中,导致 padding/阴影/背景与独立打开不一致
return this.$route.query.isSinglePage === '1' || this.$route.query.isSinglePage === 1
}
},
created() {
this.getConfig();
// 如果是单页模式且在iframe中调整样式
if (this.isSinglePage && window.self !== window.top) {
document.body.style.margin = '0'
document.body.style.padding = '0'
}
},
mounted() {
// 如果是单页模式且在iframe中调整样式
if (this.isSinglePage && window.self !== window.top) {
const container = this.$el
if (container) {
container.style.margin = '0'
container.style.padding = '0'
}
}
},
};
</script>
<style scoped lang="scss">
::v-deep .el-step__title {
font-size: 14px;
line-height: 1.5;
}
::v-deep .el-step__icon.is-icon {
border-radius: 100%;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
width: 36px;
height: 36px;
border: solid 2px;
}
::v-deep .el-step.is-center .el-step__line {
top: 50%;
}
.container {
padding: 20px;
.card {
position: relative;
}
.btns {
display: flex;
justify-content: center;
margin-top: 10px;
flex-wrap: wrap;
}
// 单页模式(弹窗模式)
&.is-single-page {
padding: 0;
margin: 0;
height: 100vh;
overflow-y: auto;
.card {
border: none;
box-shadow: none;
min-height: 100vh;
// iframe 引入时也保持白底(与独立打开一致)
background: #fff;
}
}
}
@media (max-width: 768px) {
.container {
padding: 0;
}
.btns {
justify-content: space-evenly;
& > * {
margin: 4px 6px;
}
}
::v-deep .el-steps--horizontal {
display: flex;
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;
}
}
/* 支付模板字段checklist 纯展示样式(最简线条) */
.oa-readonly-checklist {
width: 100%;
}
.oa-checklist-option-item {
margin: 6px 0;
}
.oa-checklist-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.oa-checklist-box {
width: 14px;
height: 14px;
border: 1px solid #111827;
border-radius: 3px;
margin-top: 2px;
flex-shrink: 0;
position: relative;
background: transparent;
}
.oa-checklist-box.checked::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 10px;
border-right: 2px solid #111827;
border-bottom: 2px solid #111827;
transform: translate(-50%, -58%) rotate(45deg);
}
.oa-checklist-text {
flex: 1;
min-width: 0;
line-height: 18px;
word-break: break-word;
}
.oa-checklist-label {
color: #111827;
}
.oa-checklist-remark {
margin-left: 10px;
color: #6b7280;
}
/* detail_table横向滚动避免被父级/祖先容器裁剪) */
.oa-table-x-scroll {
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
/* 兜底:强制表格内容可“撑开”,由外层容器产生横向滚动条 */
.oa-table-x-scroll ::v-deep .el-table {
width: auto;
min-width: 600px; /* 有列时保证一定宽度 */
}
.oa-table-x-scroll ::v-deep .el-table__header-wrapper,
.oa-table-x-scroll ::v-deep .el-table__body-wrapper {
overflow-x: visible; /* 让宽度由最外层滚动容器接管 */
}
.oa-table-x-scroll ::v-deep .el-table__header,
.oa-table-x-scroll ::v-deep .el-table__body {
width: max-content; /* 列多时撑开 */
}
/* 关键el-descriptions 是 table layout(auto) 时会被内容撑宽,导致“越界但无滚动条”;
仅对本页两块 descriptions 约束表格宽度(并显式设置 label 宽度避免重叠) */
.payment-extra-descriptions,
.indirect-payment-descriptions {
::v-deep .el-descriptions__table {
width: 100%;
table-layout: fixed;
}
::v-deep .el-descriptions-item__label {
width: 250px;
white-space: nowrap;
}
::v-deep .el-descriptions-item__content {
width: auto;
min-width: 0;
word-break: break-word;
}
}
</style>
<style lang="scss">
.log-table-scroll {
::-webkit-scrollbar {
height: 0;
}
}
.print-template-radios .el-radio__input:has(input[type=radio]:checked) .el-radio__inner {
border-color: var(--theme-color);
background: var(--theme-color);
&::after {
transform: translate(-50%,-50%) scale(1);
}
}
/** **/
@media screen and (max-width: 500px) {
.el-picker-panel__sidebar {
width: 100%;
}
.el-picker-panel {
width: 400px!important;
}
.el-picker-panel__content {
width: 100%;
}
.el-picker-panel__body{
margin-left: 0!important;
display: flex;
flex-direction: column;
min-width: auto!important;
}
.el-picker-panel__sidebar {
position: relative;
}
.el-picker-panel__body-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
}
</style>