You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cz-hjjc-budget/docs/间接支付-关联合同选择带回逻辑.md

17 KiB

间接支付 - 关联合同选择带回逻辑整理

概述

在间接支付页面(/payment/indirect-payment)中,"关联合同"功能出现在两个位置:

  1. 第三步(事前流程分类):当所选分类的 need_contracttrue
  2. 第六步(支付分类):当所选支付分类的 need_contracttrue

一、合同选择位置

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 组件功能

  1. 搜索功能

    • 支持按合同编号(contract_no)搜索
    • 支持按合同名称(title)搜索
    • 支持回车键触发查询
  2. 列表展示

    • 显示合同编号、合同名称、合同类型、合同金额、金额类型、签订日期、合同履行期
    • 支持分页10/20/50 条每页)
    • 支持行点击选择
  3. 选择确认

    • 点击"确定"按钮时,会调用 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 行

逻辑流程

  1. 验证闭口合同

    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. 保存合同

    selectedExpenditureContract.value = contract
    ElMessage.success('合同选择成功')
    

验证规则

  • 闭口合同amount_type === 'fixed'
    • 如果 payment_stats.paid_amount > 0payment_stats.payment_count > 0,说明已关联过非直接支付,不允许选择
    • 否则允许选择
  • 开口合同amount_type === 'open'
    • 允许选择,不检查是否已关联过非直接支付

3.2 支付合同选择处理(第六步)

函数handlePaymentContractSelected(contract)

位置IndirectPayment.vue 第 5930-5956 行

逻辑流程

与支出合同选择处理逻辑完全相同

  1. 验证闭口合同是否已关联过非直接支付
  2. 如果验证通过,保存合同并提示成功

验证规则:与支出合同选择相同

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
}

优先级

  1. 第六步复用的合同(第三步的合同)
  2. 第六步独立选择的合同
  3. 第三步的合同(如果第六步不需要合同)

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
验证失败:显示警告,阻止选择
    ↓
进入第六步时,如果满足条件,自动复用第三步的合同
    ↓
预览和提交时使用合同数据

八、注意事项

  1. 闭口合同限制

    • 闭口合同一旦关联过非直接支付(paid_amount > 0payment_count > 0),就不能再次关联
    • 这个限制是为了保证数据一致性
  2. 开口合同(框架协议)

    • 开口合同不受"已关联过非直接支付"的限制
    • 可以多次关联非直接支付
  3. 合同复用

    • 只有在第三步和第六步都需要合同时,才会自动复用
    • 复用的合同在第六步不可更改
    • 如果用户想使用不同的合同,需要在第三步清除合同,然后在第六步重新选择
  4. 合同详情获取

    • 选择器组件在确认选择时会调用 contractAPI.detail() 获取完整的合同详情
    • 这确保了返回的合同数据包含最新的支付统计信息
  5. 数据同步

    • 合同选择后,相关的金额计算、预览显示等会自动更新
    • 通过 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
    },
    // ... 其他合同字段
  }
}

十、总结

"关联合同"选择带回的逻辑核心要点:

  1. 两个选择位置:第三步(支出合同)和第六步(支付合同)
  2. 统一的选择器组件ContractSelector 提供搜索、列表、选择功能
  3. 严格的验证规则:闭口合同不能重复关联非直接支付
  4. 智能的复用机制:满足条件时自动复用第三步的合同
  5. 完整的数据流:从选择到验证到保存到使用的完整链路