17 KiB
间接支付 - 关联合同选择带回逻辑整理
概述
在间接支付页面(/payment/indirect-payment)中,"关联合同"功能出现在两个位置:
- 第三步(事前流程分类):当所选分类的
need_contract为true时 - 第六步(支付分类):当所选支付分类的
need_contract为true时
一、合同选择位置
1.1 第三步 - 支出合同选择
位置:步骤3(选择事前流程分类并填写信息)
显示条件:selectedCategory?.need_contract === true
相关变量:
selectedExpenditureContract:存储选中的支出合同showExpenditureContractSelector:控制合同选择器对话框显示
UI 结构:
<el-form-item label="关联合同">
<!-- 未选择时显示选择按钮 -->
<div v-if="!selectedExpenditureContract" class="contract-selector-wrapper">
<el-button type="primary" @click="showExpenditureContractSelector = true">
选择合同
</el-button>
</div>
<!-- 已选择时显示合同信息卡片 -->
<div v-else class="contract-info-card">
<!-- 显示合同详情:名称、总额、已支付次数、已支付金额、剩余金额等 -->
</div>
</el-form-item>
1.2 第六步 - 支付合同选择
位置:步骤5(填写支付信息)
显示条件:selectedPaymentCategory?.need_contract === true
相关变量:
selectedPaymentContract:存储选中的支付合同showPaymentContractSelector:控制合同选择器对话框显示isPaymentContractReused:判断是否复用第三步的合同reusedPaymentContract:复用的合同(第三步的合同)
UI 结构:
<el-form-item label="关联合同">
<!-- 情况1:复用第三步的合同(不可更改) -->
<div v-if="isPaymentContractReused" class="contract-info-card">
<el-card>
<template #header>
<span>已选择合同(复用第三步)</span>
</template>
<!-- 显示复用合同的详情 -->
</el-card>
</div>
<!-- 情况2:未选择且不复用时显示选择按钮 -->
<div v-else-if="!selectedPaymentContract" class="contract-selector-wrapper">
<el-button type="primary" @click="showPaymentContractSelector = true">
选择合同
</el-button>
</div>
<!-- 情况3:已选择时显示合同信息卡片 -->
<div v-else class="contract-info-card">
<!-- 显示合同详情,支持重新选择和清除 -->
</div>
</el-form-item>
二、合同选择器组件
2.1 组件位置
czemc-budget-execution-frontend/src/components/ContractSelector.vue
2.2 组件功能
-
搜索功能:
- 支持按合同编号(
contract_no)搜索 - 支持按合同名称(
title)搜索 - 支持回车键触发查询
- 支持按合同编号(
-
列表展示:
- 显示合同编号、合同名称、合同类型、合同金额、金额类型、签订日期、合同履行期
- 支持分页(10/20/50 条每页)
- 支持行点击选择
-
选择确认:
- 点击"确定"按钮时,会调用
contractAPI.detail(contractId)获取合同详情 - 合同详情包含支付统计信息(
payment_stats) - 通过
@confirm事件将完整的合同详情返回给父组件
- 点击"确定"按钮时,会调用
2.3 组件使用
<!-- 支出合同选择器(第三步) -->
<ContractSelector
:model-value="showExpenditureContractSelector"
@update:model-value="showExpenditureContractSelector = $event"
@confirm="handleExpenditureContractSelected"
/>
<!-- 支付合同选择器(第六步) -->
<ContractSelector
:model-value="showPaymentContractSelector"
@update:model-value="showPaymentContractSelector = $event"
@confirm="handlePaymentContractSelected"
/>
三、合同选择带回逻辑
3.1 支出合同选择处理(第三步)
函数:handleExpenditureContractSelected(contract)
位置:IndirectPayment.vue 第 5885-5911 行
逻辑流程:
-
验证闭口合同:
if (contract.amount_type === 'fixed') { // 检查是否已关联过非直接支付 const paymentCount = contract.payment_stats?.payment_count || 0 const paidAmount = Number(contract.payment_stats?.paid_amount || 0) // 如果有已支付记录,不允许选择 if (paidAmount > 0 || paymentCount > 0) { // 显示警告提示,阻止选择 return } } -
保存合同:
selectedExpenditureContract.value = contract ElMessage.success('合同选择成功')
验证规则:
- ✅ 闭口合同(
amount_type === 'fixed'):- 如果
payment_stats.paid_amount > 0或payment_stats.payment_count > 0,说明已关联过非直接支付,不允许选择 - 否则允许选择
- 如果
- ✅ 开口合同(
amount_type === 'open'):- 允许选择,不检查是否已关联过非直接支付
3.2 支付合同选择处理(第六步)
函数:handlePaymentContractSelected(contract)
位置:IndirectPayment.vue 第 5930-5956 行
逻辑流程:
与支出合同选择处理逻辑完全相同:
- 验证闭口合同是否已关联过非直接支付
- 如果验证通过,保存合同并提示成功
验证规则:与支出合同选择相同
3.3 合同复用逻辑
触发时机:当进入第六步(选择支付分类后)时
位置:IndirectPayment.vue 第 4992-4994 行
逻辑:
// 如果第三步已选择合同,且第六步也需要合同,则自动复用第三步的合同
if (selectedCategory.value?.need_contract &&
selectedExpenditureContract.value &&
selectedPaymentCategory.value?.need_contract) {
selectedPaymentContract.value = selectedExpenditureContract.value
}
判断条件:
isPaymentContractReused计算属性:const isPaymentContractReused = computed(() => { return selectedCategory.value?.need_contract && selectedExpenditureContract.value && selectedPaymentCategory.value?.need_contract })
复用效果:
- 第六步的合同选择区域显示"已选择合同(复用第三步)"
- 合同信息不可更改(不显示"重新选择"和"清除"按钮)
- 使用第三步选择的合同数据
四、合同数据使用
4.1 合同信息展示
已选择的合同会显示以下信息:
- 合同名称(
title) - 合同总额(
amount_total) - 已支付次数(
payment_stats.payment_count) - 已支付金额(
payment_stats.paid_amount) - 剩余金额(仅闭口合同显示,计算公式:
总额 - 已支付金额)
4.1.1 合同总额的使用场景
重要说明:合同选择后,不会自动填充合同总额到表单字段 contract_total_amount。用户需要手动输入合同总金额。
合同总额(contract.amount_total)的使用场景如下:
场景1:合同信息展示
- 位置:第三步和第六步的合同选择卡片
- 用途:显示选中合同的总额信息
- 代码位置:
- 第三步:
IndirectPayment.vue第 631-632 行 - 第六步:
IndirectPayment.vue第 901-902 行、941-942 行
- 第三步:
- 显示格式:使用
formatAmount()格式化,显示为带千分位的数字
场景2:计算剩余金额(闭口合同)
- 位置:合同信息卡片中的"剩余金额"字段
- 用途:计算闭口合同的剩余可支付金额
- 计算公式:
剩余金额 = 合同总额 - 已支付金额 - 代码位置:
- 支出合同:
IndirectPayment.vue第 5919-5926 行 - 支付合同:
IndirectPayment.vue第 5964-5971 行 - 复用合同:
IndirectPayment.vue第 5986-5993 行
- 支出合同:
- 注意:仅闭口合同(
amount_type === 'fixed')才显示剩余金额
场景3:预览页面显示
- 位置:步骤6(总体预览)的合同信息部分
- 用途:在预览页面显示合同总额
- 代码位置:
IndirectPayment.vue第 773-774 行 - 数据来源:通过
getPreviewContract()函数获取合同对象
场景4:字段显示条件判断(支付分类)
- 位置:第六步(填写支付信息)的动态字段显示
- 用途:根据合同总额判断某些字段是否显示
- 代码位置:
IndirectPayment.vue第 5221-5270 行 - 重要说明:
- 在支付分类的字段显示条件中,使用的是表单字段
formData.contract_total_amount(用户手动输入的) - 不是合同的
amount_total(合同对象自带的) - 代码逻辑:
// 非直接支付场景,合同总金额使用事前流程的合同总金额 const contractTotalAmount = formData.value.contract_total_amount || 0 - 这意味着:用户需要手动输入合同总金额,该金额会用于判断支付分类的字段是否显示
- 在支付分类的字段显示条件中,使用的是表单字段
场景5:金额限制条件验证(事前流程分类)
- 位置:第三步(填写事前流程信息)的金额限制验证
- 用途:验证用户输入的合同总金额是否满足分类的金额限制条件
- 代码位置:
- 金额限制条件获取:
IndirectPayment.vue第 6070 行 - 金额限制验证:
IndirectPayment.vue第 6072-6074 行 - 验证函数:
IndirectPayment.vue第 6024-6068 行
- 金额限制条件获取:
- 验证字段:
formData.contract_total_amount(用户手动输入的) - 验证规则:根据分类配置的
amount_limit_conditions进行验证- 支持的操作符:
lt(小于)、lte(小于等于)、gt(大于)、gte(大于等于)、eq(等于)
- 支持的操作符:
- UI 显示:
- 实时显示验证结果(成功/失败)
- 显示金额限制条件的提示文本
场景6:表单验证
- 位置:第三步进入下一步的验证
- 用途:验证合同总金额是否已填写且大于0
- 代码位置:
IndirectPayment.vue第 4281-4284 行 - 验证规则:
const contractAmount = Number(formData.value.contract_total_amount) if (!Number.isFinite(contractAmount) || contractAmount <= 0) { return false // 不能进入下一步 }
场景7:金额大写显示
- 位置:第三步的合同总金额输入框下方
- 用途:将合同总金额转换为中文大写
- 代码位置:
- 计算属性:
IndirectPayment.vue第 5880 行 - 格式化函数:
IndirectPayment.vue第 5874-5878 行
- 计算属性:
- 显示格式:例如 "壹佰万元整"
4.2 剩余金额计算
支出合同剩余金额:
const expenditureContractRemainingAmount = computed(() => {
if (!selectedExpenditureContract.value ||
selectedExpenditureContract.value.amount_type !== 'fixed') {
return 0
}
const total = Number(selectedExpenditureContract.value.amount_total) || 0
const paid = Number(selectedExpenditureContract.value.payment_stats?.paid_amount) || 0
return Math.max(0, total - paid)
})
支付合同剩余金额:
const paymentContractRemainingAmount = computed(() => {
if (!selectedPaymentContract.value ||
selectedPaymentContract.value.amount_type !== 'fixed') {
return 0
}
const total = Number(selectedPaymentContract.value.amount_total) || 0
const paid = Number(selectedPaymentContract.value.payment_stats?.paid_amount) || 0
return Math.max(0, total - paid)
})
4.3 预览页面合同显示
函数:getPreviewContract()
位置:IndirectPayment.vue 第 5819-5835 行
逻辑:
const getPreviewContract = () => {
// 优先使用第六步的合同(如果支付分类需要合同)
if (selectedPaymentCategory.value?.need_contract) {
// 如果复用第三步的合同,使用第三步的合同
if (isPaymentContractReused.value && reusedPaymentContract.value) {
return reusedPaymentContract.value
}
// 否则使用第六步选择的合同
if (selectedPaymentContract.value) {
return selectedPaymentContract.value
}
}
// 如果第六步没有合同,但第三步有合同,显示第三步的合同
if (selectedCategory.value?.need_contract && selectedExpenditureContract.value) {
return selectedExpenditureContract.value
}
return null
}
优先级:
- 第六步复用的合同(第三步的合同)
- 第六步独立选择的合同
- 第三步的合同(如果第六步不需要合同)
4.4 提交时的合同ID
位置:IndirectPayment.vue 第 5620-5628 行
逻辑:
// 支出合同ID(第三步)
const expenditureContractId = selectedCategory.value?.need_contract && selectedExpenditureContract.value
? selectedExpenditureContract.value.id
: null
// 支付合同ID(第六步)
const paymentContractId = selectedPaymentCategory.value?.need_contract
? (isPaymentContractReused.value
? reusedPaymentContract.value?.id
: selectedPaymentContract.value?.id)
: null
五、清除合同
5.1 清除支出合同
函数:clearExpenditureContract()
const clearExpenditureContract = () => {
selectedExpenditureContract.value = null
}
5.2 清除支付合同
函数:clearPaymentContract()
const clearPaymentContract = () => {
selectedPaymentContract.value = null
}
注意:如果支付合同是复用的第三步合同,清除操作会清除 selectedPaymentContract,但不会影响 selectedExpenditureContract。
六、关键代码位置
| 功能 | 代码位置 | 行号范围 |
|---|---|---|
| 第三步合同选择UI | IndirectPayment.vue |
596-647 |
| 第六步合同选择UI | IndirectPayment.vue |
884-956 |
| 合同选择器组件使用 | IndirectPayment.vue |
1162-1173 |
| 支出合同选择处理 | IndirectPayment.vue |
5885-5911 |
| 支付合同选择处理 | IndirectPayment.vue |
5930-5956 |
| 合同复用逻辑 | IndirectPayment.vue |
4992-4994 |
| 合同复用判断 | IndirectPayment.vue |
5974-5978 |
| 预览合同获取 | IndirectPayment.vue |
5819-5835 |
| 合同选择器组件 | components/ContractSelector.vue |
全部 |
七、数据流图
用户点击"选择合同"
↓
打开 ContractSelector 对话框
↓
用户搜索/浏览合同列表
↓
用户选择合同并点击"确定"
↓
ContractSelector 调用 contractAPI.detail(contractId)
↓
获取合同详情(包含 payment_stats)
↓
触发 @confirm 事件,传递合同详情
↓
父组件处理函数(handleExpenditureContractSelected / handlePaymentContractSelected)
↓
验证合同(闭口合同检查是否已关联过非直接支付)
↓
验证通过:保存到 selectedExpenditureContract / selectedPaymentContract
验证失败:显示警告,阻止选择
↓
进入第六步时,如果满足条件,自动复用第三步的合同
↓
预览和提交时使用合同数据
八、注意事项
-
闭口合同限制:
- 闭口合同一旦关联过非直接支付(
paid_amount > 0或payment_count > 0),就不能再次关联 - 这个限制是为了保证数据一致性
- 闭口合同一旦关联过非直接支付(
-
开口合同(框架协议):
- 开口合同不受"已关联过非直接支付"的限制
- 可以多次关联非直接支付
-
合同复用:
- 只有在第三步和第六步都需要合同时,才会自动复用
- 复用的合同在第六步不可更改
- 如果用户想使用不同的合同,需要在第三步清除合同,然后在第六步重新选择
-
合同详情获取:
- 选择器组件在确认选择时会调用
contractAPI.detail()获取完整的合同详情 - 这确保了返回的合同数据包含最新的支付统计信息
- 选择器组件在确认选择时会调用
-
数据同步:
- 合同选择后,相关的金额计算、预览显示等会自动更新
- 通过 Vue 的响应式系统实现数据同步
九、相关API
9.1 合同列表API
接口:contractAPI.list(params)
参数:
page: 页码page_size: 每页数量contract_no: 合同编号(可选)title: 合同名称(可选)
返回:
{
code: 0,
data: {
data: [/* 合同列表 */],
total: 100
}
}
9.2 合同详情API
接口:contractAPI.detail(contractId)
返回:
{
code: 0,
data: {
id: 1,
contract_no: "HT2024001",
title: "合同名称",
amount_total: 1000000,
amount_type: "fixed", // "fixed" | "open"
payment_stats: {
payment_count: 0,
paid_amount: 0
},
// ... 其他合同字段
}
}
十、总结
"关联合同"选择带回的逻辑核心要点:
- 两个选择位置:第三步(支出合同)和第六步(支付合同)
- 统一的选择器组件:
ContractSelector提供搜索、列表、选择功能 - 严格的验证规则:闭口合同不能重复关联非直接支付
- 智能的复用机制:满足条件时自动复用第三步的合同
- 完整的数据流:从选择到验证到保存到使用的完整链路