|
|
<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;
|
|
|
},
|
|
|
|
|
|
// ---------- 事前流程 checklist:options/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;
|
|
|
|
|
|
// 如果已经是完整URL(http:// 或 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>
|