# 间接支付 - 关联合同选择带回逻辑整理 ## 概述 在间接支付页面(`/payment/indirect-payment`)中,"关联合同"功能出现在两个位置: 1. **第三步(事前流程分类)**:当所选分类的 `need_contract` 为 `true` 时 2. **第六步(支付分类)**:当所选支付分类的 `need_contract` 为 `true` 时 ## 一、合同选择位置 ### 1.1 第三步 - 支出合同选择 **位置**:步骤3(选择事前流程分类并填写信息) **显示条件**:`selectedCategory?.need_contract === true` **相关变量**: - `selectedExpenditureContract`:存储选中的支出合同 - `showExpenditureContractSelector`:控制合同选择器对话框显示 **UI 结构**: ```vue
选择合同
``` ### 1.2 第六步 - 支付合同选择 **位置**:步骤5(填写支付信息) **显示条件**:`selectedPaymentCategory?.need_contract === true` **相关变量**: - `selectedPaymentContract`:存储选中的支付合同 - `showPaymentContractSelector`:控制合同选择器对话框显示 - `isPaymentContractReused`:判断是否复用第三步的合同 - `reusedPaymentContract`:复用的合同(第三步的合同) **UI 结构**: ```vue
选择合同
``` ## 二、合同选择器组件 ### 2.1 组件位置 `czemc-budget-execution-frontend/src/components/ContractSelector.vue` ### 2.2 组件功能 1. **搜索功能**: - 支持按合同编号(`contract_no`)搜索 - 支持按合同名称(`title`)搜索 - 支持回车键触发查询 2. **列表展示**: - 显示合同编号、合同名称、合同类型、合同金额、金额类型、签订日期、合同履行期 - 支持分页(10/20/50 条每页) - 支持行点击选择 3. **选择确认**: - 点击"确定"按钮时,会调用 `contractAPI.detail(contractId)` 获取合同详情 - 合同详情包含支付统计信息(`payment_stats`) - 通过 `@confirm` 事件将完整的合同详情返回给父组件 ### 2.3 组件使用 ```vue ``` ## 三、合同选择带回逻辑 ### 3.1 支出合同选择处理(第三步) **函数**:`handleExpenditureContractSelected(contract)` **位置**:`IndirectPayment.vue` 第 5885-5911 行 **逻辑流程**: 1. **验证闭口合同**: ```javascript 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 } } ``` 2. **保存合同**: ```javascript 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 行 **逻辑流程**: 与支出合同选择处理逻辑**完全相同**: 1. 验证闭口合同是否已关联过非直接支付 2. 如果验证通过,保存合同并提示成功 **验证规则**:与支出合同选择相同 ### 3.3 合同复用逻辑 **触发时机**:当进入第六步(选择支付分类后)时 **位置**:`IndirectPayment.vue` 第 4992-4994 行 **逻辑**: ```javascript // 如果第三步已选择合同,且第六步也需要合同,则自动复用第三步的合同 if (selectedCategory.value?.need_contract && selectedExpenditureContract.value && selectedPaymentCategory.value?.need_contract) { selectedPaymentContract.value = selectedExpenditureContract.value } ``` **判断条件**: - `isPaymentContractReused` 计算属性: ```javascript 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`(合同对象自带的) - 代码逻辑: ```javascript // 非直接支付场景,合同总金额使用事前流程的合同总金额 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 行 - **验证规则**: ```javascript 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 剩余金额计算 **支出合同剩余金额**: ```javascript 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) }) ``` **支付合同剩余金额**: ```javascript 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 行 **逻辑**: ```javascript 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 } ``` **优先级**: 1. 第六步复用的合同(第三步的合同) 2. 第六步独立选择的合同 3. 第三步的合同(如果第六步不需要合同) ### 4.4 提交时的合同ID **位置**:`IndirectPayment.vue` 第 5620-5628 行 **逻辑**: ```javascript // 支出合同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()` ```javascript const clearExpenditureContract = () => { selectedExpenditureContract.value = null } ``` ### 5.2 清除支付合同 **函数**:`clearPaymentContract()` ```javascript 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 验证失败:显示警告,阻止选择 ↓ 进入第六步时,如果满足条件,自动复用第三步的合同 ↓ 预览和提交时使用合同数据 ``` ## 八、注意事项 1. **闭口合同限制**: - 闭口合同一旦关联过非直接支付(`paid_amount > 0` 或 `payment_count > 0`),就不能再次关联 - 这个限制是为了保证数据一致性 2. **开口合同(框架协议)**: - 开口合同不受"已关联过非直接支付"的限制 - 可以多次关联非直接支付 3. **合同复用**: - 只有在第三步和第六步都需要合同时,才会自动复用 - 复用的合同在第六步不可更改 - 如果用户想使用不同的合同,需要在第三步清除合同,然后在第六步重新选择 4. **合同详情获取**: - 选择器组件在确认选择时会调用 `contractAPI.detail()` 获取完整的合同详情 - 这确保了返回的合同数据包含最新的支付统计信息 5. **数据同步**: - 合同选择后,相关的金额计算、预览显示等会自动更新 - 通过 Vue 的响应式系统实现数据同步 ## 九、相关API ### 9.1 合同列表API **接口**:`contractAPI.list(params)` **参数**: - `page`: 页码 - `page_size`: 每页数量 - `contract_no`: 合同编号(可选) - `title`: 合同名称(可选) **返回**: ```javascript { code: 0, data: { data: [/* 合同列表 */], total: 100 } } ``` ### 9.2 合同详情API **接口**:`contractAPI.detail(contractId)` **返回**: ```javascript { 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 }, // ... 其他合同字段 } } ``` ## 十、总结 "关联合同"选择带回的逻辑核心要点: 1. **两个选择位置**:第三步(支出合同)和第六步(支付合同) 2. **统一的选择器组件**:`ContractSelector` 提供搜索、列表、选择功能 3. **严格的验证规则**:闭口合同不能重复关联非直接支付 4. **智能的复用机制**:满足条件时自动复用第三步的合同 5. **完整的数据流**:从选择到验证到保存到使用的完整链路