Files
loan-pricing/doc/plans/2025-02-02-customer-type-based-detail-view.md
2026-02-02 15:25:38 +08:00

25 KiB
Raw Blame History

根据客户类型动态展示流程详情实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

目标: 创建两个独立的详情组件(个人客户和企业客户),根据客户类型动态渲染不同的字段展示。

架构: 将现有的单一详情页面拆分为两个独立组件PersonalWorkflowDetail.vue 和 CorporateWorkflowDetail.vue父组件 detail.vue 作为容器负责数据获取和根据 custType 动态分发渲染。

技术栈: Vue 2.6.12, Element UI 2.15.14, Vue Router 3.4.9


Task 1: 创建个人客户详情组件 PersonalWorkflowDetail.vue

文件:

  • 创建: ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue

Step 1: 创建组件基础结构

<template>
  <div class="personal-workflow-detail" v-loading="loading">
    <!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
    <div v-if="!loading && detailData" class="detail-layout">
      <!-- 左侧关键信息卡片 -->
      <div class="left-panel">
        <el-card class="summary-card">
          <div slot="header" class="card-header">
            <span class="card-title">关键信息</span>
          </div>
          <el-descriptions :column="1" direction="vertical" border>
            <el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
            <el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
            <el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
            <el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
            <el-descriptions-item label="基准利率">
              <span class="rate-value">{{ getBaseLoanRate() }}</span> %
            </el-descriptions-item>
            <el-descriptions-item label="浮动BP">
              <span class="total-bp-value">{{ getTotalBp() }}</span>
            </el-descriptions-item>
            <el-descriptions-item label="测算利率">
              <span class="calculate-rate">{{ getCalculateRate() }}</span> %
            </el-descriptions-item>
            <el-descriptions-item label="执行利率">
              <div class="execute-rate-input-wrapper">
                <el-input
                  v-model="executeRateInput"
                  class="execute-rate-input"
                  placeholder="请输入执行利率"
                >
                  <template slot="append">%</template>
                </el-input>
                <el-button
                  type="primary"
                  size="small"
                  @click="handleSetExecuteRate"
                >
                  确定
                </el-button>
              </div>
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </div>

      <!-- 右侧面板 -->
      <div class="right-panel">
        <!-- 流程详情卡片 -->
        <el-card class="detail-card">
          <div slot="header" class="card-header">
            <span class="card-title">流程详情</span>
          </div>

          <!-- 基本信息组 -->
          <div class="info-section">
            <h4 class="section-title">基本信息</h4>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
              <el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
              <el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
              <el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
              <el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
              <el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
              <el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
            </el-descriptions>
          </div>

          <!-- 业务信息组 -->
          <div class="info-section">
            <h4 class="section-title">业务信息</h4>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
              <el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
              <el-descriptions-item label="是否有经营佐证">{{ formatBoolean(detailData.bizProof) }}</el-descriptions-item>
              <el-descriptions-item label="循环功能">{{ formatBoolean(detailData.loanLoop) }}</el-descriptions-item>
              <el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
              <el-descriptions-item label="抵质押物是否三方所有">{{ formatBoolean(detailData.collThirdParty) }}</el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>

        <!-- 模型输出卡片 -->
        <ModelOutputDisplay
          :cust-type="detailData.custType"
          :retail-output="retailOutput"
          :corp-output="null"
        />

        <!-- 议价池卡片 -->
        <BargainingPoolDisplay
          v-if="bargainingPool"
          :branch-pool="bargainingPool.branchPool"
          :sub-branch-pool="bargainingPool.subBranchPool"
          :private-domain-pool="bargainingPool.privateDomainPool"
          :excess-profit-share="bargainingPool.excessProfitShare"
        />
      </div>
    </div>
  </div>
</template>

<script>
import { setExecuteRate } from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"

export default {
  name: "PersonalWorkflowDetail",
  components: {
    ModelOutputDisplay,
    BargainingPoolDisplay
  },
  props: {
    detailData: {
      type: Object,
      required: true
    },
    retailOutput: {
      type: Object,
      default: null
    },
    bargainingPool: {
      type: Object,
      default: null
    }
  },
  data() {
    return {
      loading: false,
      executeRateInput: ''
    }
  },
  watch: {
    'detailData.executeRate': {
      handler(newVal) {
        this.executeRateInput = newVal || ''
      },
      immediate: true
    }
  },
  methods: {
    /** 格式化布尔值为中文 */
    formatBoolean(value) {
      if (value === 'true' || value === true) return '是'
      if (value === 'false' || value === false) return '否'
      return value || '-'
    },
    /** 获取基准利率 */
    getBaseLoanRate() {
      return this.retailOutput?.baseLoanRate || '-'
    },
    /** 获取浮动BP */
    getTotalBp() {
      return this.retailOutput?.totalBp || '-'
    },
    /** 获取测算利率 */
    getCalculateRate() {
      return this.retailOutput?.calculateRate || '-'
    },
    /** 设定执行利率 */
    handleSetExecuteRate() {
      const value = this.executeRateInput
      if (value === null || value === undefined || value === '') {
        this.$modal.msgError("请输入执行利率")
        return
      }

      const numValue = parseFloat(value)
      if (isNaN(numValue)) {
        this.$modal.msgError("请输入有效的数字")
        return
      }
      if (numValue < 0 || numValue > 100) {
        this.$modal.msgError("执行利率必须在 0 到 100 之间")
        return
      }

      this.loading = true
      setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
        this.$modal.msgSuccess("执行利率设定成功")
        this.$emit('refresh')
        this.loading = false
      }).catch(error => {
        this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
        this.loading = false
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.personal-workflow-detail {
  .detail-layout {
    display: flex;
    gap: 20px;
    align-items: flex-start;

    .left-panel {
      flex: 0 0 280px;
      max-width: 280px;

      .summary-card {
        ::v-deep .el-card__header {
          padding: 16px 20px;
          background-color: #fafafa;
          border-bottom: 1px solid #ebeef5;
        }

        .card-header {
          display: flex;
          align-items: center;

          .card-title {
            font-size: 16px;
            font-weight: 500;
            color: #303133;
          }
        }

        ::v-deep .el-card__body {
          padding: 16px;
        }

        .execute-rate-input-wrapper {
          display: flex;
          flex-direction: column;
          gap: 8px;
          width: 100%;

          .execute-rate-input {
            width: 100%;
          }
        }

        .rate-value {
          color: #67c23a;
          font-weight: 500;
        }

        .total-bp-value {
          color: #e6a23c;
          font-weight: 600;
        }

        .calculate-rate {
          color: #f56c6c;
          font-weight: 600;
          font-size: 16px;
        }
      }
    }

    .right-panel {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 20px;

      .detail-card {
        ::v-deep .el-card__header {
          padding: 16px 20px;
          background-color: #fafafa;
          border-bottom: 1px solid #ebeef5;
        }

        .card-header {
          display: flex;
          align-items: center;

          .card-title {
            font-size: 16px;
            font-weight: 500;
            color: #303133;
          }
        }

        ::v-deep .el-card__body {
          padding: 20px;
        }

        .info-section {
          &:not(:last-child) {
            margin-bottom: 24px;
          }

          .section-title {
            margin: 0 0 16px 0;
            font-size: 14px;
            font-weight: 500;
            color: #606266;
            padding-bottom: 8px;
            border-bottom: 1px solid #ebeef5;
          }
        }
      }
    }
  }
}

@media screen and (max-width: 992px) {
  .personal-workflow-detail {
    .detail-layout {
      flex-direction: column;

      .left-panel,
      .right-panel {
        flex: 1 1 100%;
        max-width: 100%;
      }
    }
  }
}
</style>

Step 2: 验证组件语法正确

检查: 在浏览器开发工具中确认无语法错误


Task 2: 创建企业客户详情组件 CorporateWorkflowDetail.vue

文件:

  • 创建: ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue

Step 1: 创建组件基础结构

<template>
  <div class="corporate-workflow-detail" v-loading="loading">
    <!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
    <div v-if="!loading && detailData" class="detail-layout">
      <!-- 左侧关键信息卡片 -->
      <div class="left-panel">
        <el-card class="summary-card">
          <div slot="header" class="card-header">
            <span class="card-title">关键信息</span>
          </div>
          <el-descriptions :column="1" direction="vertical" border>
            <el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
            <el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
            <el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
            <el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
            <el-descriptions-item label="基准利率">
              <span class="rate-value">{{ getBaseLoanRate() }}</span> %
            </el-descriptions-item>
            <el-descriptions-item label="浮动BP">
              <span class="total-bp-value">{{ getTotalBp() }}</span>
            </el-descriptions-item>
            <el-descriptions-item label="测算利率">
              <span class="calculate-rate">{{ getCalculateRate() }}</span> %
            </el-descriptions-item>
            <el-descriptions-item label="执行利率">
              <div class="execute-rate-input-wrapper">
                <el-input
                  v-model="executeRateInput"
                  class="execute-rate-input"
                  placeholder="请输入执行利率"
                >
                  <template slot="append">%</template>
                </el-input>
                <el-button
                  type="primary"
                  size="small"
                  @click="handleSetExecuteRate"
                >
                  确定
                </el-button>
              </div>
            </el-descriptions-item>
          </el-descriptions>
        </el-card>
      </div>

      <!-- 右侧面板 -->
      <div class="right-panel">
        <!-- 流程详情卡片 -->
        <el-card class="detail-card">
          <div slot="header" class="card-header">
            <span class="card-title">流程详情</span>
          </div>

          <!-- 基本信息组 -->
          <div class="info-section">
            <h4 class="section-title">基本信息</h4>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
              <el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
              <el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
              <el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
              <el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
              <el-descriptions-item label="贷款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
              <el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
              <el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
            </el-descriptions>
          </div>

          <!-- 业务信息组 -->
          <div class="info-section">
            <h4 class="section-title">业务信息</h4>
            <el-descriptions :column="2" border>
              <el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
              <el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
              <el-descriptions-item label="省农担担保贷款">{{ formatBoolean(detailData.isAgriGuar) }}</el-descriptions-item>
              <el-descriptions-item label="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
              <el-descriptions-item label="科技型企业">{{ formatBoolean(detailData.isTechEnt) }}</el-descriptions-item>
              <el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
              <el-descriptions-item label="抵质押物是否三方所有">{{ formatBoolean(detailData.collThirdParty) }}</el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>

        <!-- 模型输出卡片 -->
        <ModelOutputDisplay
          :cust-type="detailData.custType"
          :retail-output="null"
          :corp-output="corpOutput"
        />

        <!-- 议价池卡片 -->
        <BargainingPoolDisplay
          v-if="bargainingPool"
          :branch-pool="bargainingPool.branchPool"
          :sub-branch-pool="bargainingPool.subBranchPool"
          :private-domain-pool="bargainingPool.privateDomainPool"
          :excess-profit-share="bargainingPool.excessProfitShare"
        />
      </div>
    </div>
  </div>
</template>

<script>
import { setExecuteRate } from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"

export default {
  name: "CorporateWorkflowDetail",
  components: {
    ModelOutputDisplay,
    BargainingPoolDisplay
  },
  props: {
    detailData: {
      type: Object,
      required: true
    },
    corpOutput: {
      type: Object,
      default: null
    },
    bargainingPool: {
      type: Object,
      default: null
    }
  },
  data() {
    return {
      loading: false,
      executeRateInput: ''
    }
  },
  watch: {
    'detailData.executeRate': {
      handler(newVal) {
        this.executeRateInput = newVal || ''
      },
      immediate: true
    }
  },
  methods: {
    /** 格式化布尔值为中文 */
    formatBoolean(value) {
      if (value === 'true' || value === true) return '是'
      if (value === 'false' || value === false) return '否'
      return value || '-'
    },
    /** 获取基准利率 */
    getBaseLoanRate() {
      return this.corpOutput?.baseLoanRate || '-'
    },
    /** 获取浮动BP */
    getTotalBp() {
      return this.corpOutput?.totalBp || '-'
    },
    /** 获取测算利率 */
    getCalculateRate() {
      return this.corpOutput?.calculateRate || '-'
    },
    /** 设定执行利率 */
    handleSetExecuteRate() {
      const value = this.executeRateInput
      if (value === null || value === undefined || value === '') {
        this.$modal.msgError("请输入执行利率")
        return
      }

      const numValue = parseFloat(value)
      if (isNaN(numValue)) {
        this.$modal.msgError("请输入有效的数字")
        return
      }
      if (numValue < 0 || numValue > 100) {
        this.$modal.msgError("执行利率必须在 0 到 100 之间")
        return
      }

      this.loading = true
      setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
        this.$modal.msgSuccess("执行利率设定成功")
        this.$emit('refresh')
        this.loading = false
      }).catch(error => {
        this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
        this.loading = false
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.corporate-workflow-detail {
  .detail-layout {
    display: flex;
    gap: 20px;
    align-items: flex-start;

    .left-panel {
      flex: 0 0 280px;
      max-width: 280px;

      .summary-card {
        ::v-deep .el-card__header {
          padding: 16px 20px;
          background-color: #fafafa;
          border-bottom: 1px solid #ebeef5;
        }

        .card-header {
          display: flex;
          align-items: center;

          .card-title {
            font-size: 16px;
            font-weight: 500;
            color: #303133;
          }
        }

        ::v-deep .el-card__body {
          padding: 16px;
        }

        .execute-rate-input-wrapper {
          display: flex;
          flex-direction: column;
          gap: 8px;
          width: 100%;

          .execute-rate-input {
            width: 100%;
          }
        }

        .rate-value {
          color: #67c23a;
          font-weight: 500;
        }

        .total-bp-value {
          color: #e6a23c;
          font-weight: 600;
        }

        .calculate-rate {
          color: #f56c6c;
          font-weight: 600;
          font-size: 16px;
        }
      }
    }

    .right-panel {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 20px;

      .detail-card {
        ::v-deep .el-card__header {
          padding: 16px 20px;
          background-color: #fafafa;
          border-bottom: 1px solid #ebeef5;
        }

        .card-header {
          display: flex;
          align-items: center;

          .card-title {
            font-size: 16px;
            font-weight: 500;
            color: #303133;
          }
        }

        ::v-deep .el-card__body {
          padding: 20px;
        }

        .info-section {
          &:not(:last-child) {
            margin-bottom: 24px;
          }

          .section-title {
            margin: 0 0 16px 0;
            font-size: 14px;
            font-weight: 500;
            color: #606266;
            padding-bottom: 8px;
            border-bottom: 1px solid #ebeef5;
          }
        }
      }
    }
  }
}

@media screen and (max-width: 992px) {
  .corporate-workflow-detail {
    .detail-layout {
      flex-direction: column;

      .left-panel,
      .right-panel {
        flex: 1 1 100%;
        max-width: 100%;
      }
    }
  }
}
</style>

Step 2: 验证组件语法正确

检查: 在浏览器开发工具中确认无语法错误


Task 3: 修改 detail.vue 为容器组件

文件:

  • 修改: ruoyi-ui/src/views/loanPricing/workflow/detail.vue

Step 1: 替换为容器组件结构

将整个文件内容替换为:

<template>
  <div class="app-container workflow-detail-container" v-loading="loading">
    <!-- 页面头部标题和返回按钮 -->
    <div class="page-header">
      <h2 class="page-title">流程详情</h2>
      <el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
    </div>

    <!-- 根据客户类型渲染对应的详情组件 -->
    <personal-workflow-detail
      v-if="!loading && workflowDetail && workflowDetail.custType === '个人'"
      :detail-data="workflowDetail"
      :retail-output="retailOutput"
      :bargaining-pool="bargainingPool"
      @refresh="getDetail"
    />

    <corporate-workflow-detail
      v-if="!loading && workflowDetail && workflowDetail.custType === '企业'"
      :detail-data="workflowDetail"
      :corp-output="corpOutput"
      :bargaining-pool="bargainingPool"
      @refresh="getDetail"
    />
  </div>
</template>

<script>
import { getWorkflow } from "@/api/loanPricing/workflow"
import PersonalWorkflowDetail from "./components/PersonalWorkflowDetail.vue"
import CorporateWorkflowDetail from "./components/CorporateWorkflowDetail.vue"

export default {
  name: "LoanPricingWorkflowDetail",
  components: {
    PersonalWorkflowDetail,
    CorporateWorkflowDetail
  },
  data() {
    return {
      loading: true,
      workflowDetail: null,
      retailOutput: null,
      corpOutput: null,
      bargainingPool: null
    }
  },
  created() {
    this.getDetail()
  },
  methods: {
    /** 获取流程详情 */
    getDetail() {
      const serialNum = this.$route.params.serialNum
      if (!serialNum) {
        this.$modal.msgError("缺少业务方流水号参数")
        this.goBack()
        return
      }

      getWorkflow(serialNum).then(response => {
        this.workflowDetail = response.data.loanPricingWorkflow
        this.retailOutput = response.data.modelRetailOutputFields
        this.corpOutput = response.data.modelCorpOutputFields
        this.bargainingPool = response.data.bargainingPool
        this.loading = false
      }).catch(error => {
        this.$modal.msgError("获取流程详情失败:" + (error.message || "未知错误"))
        this.loading = false
      })
    },
    /** 返回上一页 */
    goBack() {
      this.$router.go(-1)
    }
  }
}
</script>

<style lang="scss" scoped>
.workflow-detail-container {
  .page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding: 0 4px;

    .page-title {
      margin: 0;
      font-size: 20px;
      font-weight: 500;
      color: #303133;
    }
  }
}
</style>

Step 2: 验证修改正确

检查: 在浏览器中访问详情页,确认根据客户类型正确渲染对应组件


Task 4: 测试验证

Step 1: 测试个人客户详情页

  1. 在列表页点击个人客户记录的"查看"按钮
  2. 确认页面正确显示 PersonalWorkflowDetail 组件
  3. 确认所有个人客户字段正确显示
  4. 确认模型输出正确展示retailOutput
  5. 测试执行利率设定功能

Step 2: 测试企业客户详情页

  1. 在列表页点击企业客户记录的"查看"按钮
  2. 确认页面正确显示 CorporateWorkflowDetail 组件
  3. 确认所有企业客户字段正确显示
  4. 确认模型输出正确展示corpOutput
  5. 测试执行利率设定功能

Step 3: 响应式布局测试

  1. 调整浏览器窗口宽度到小于992px
  2. 确认布局自动切换为单列垂直布局
  3. 确认所有内容正常显示

Task 5: 提交代码

Step 1: 添加文件到 Git

git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue
git add ruoyi-ui/src/views/loanPricing/workflow/detail.vue

Step 2: 提交变更

git commit -m "feat: 根据客户类型动态展示流程详情

- 创建 PersonalWorkflowDetail.vue 个人客户详情组件
- 创建 CorporateWorkflowDetail.vue 企业客户详情组件
- 修改 detail.vue 为容器组件,根据 custType 动态渲染
- 个人客户展示:基本信息(客户内码、证件信息)+ 业务信息(担保方式、经营佐证、循环功能等)
- 企业客户展示:基本信息(客户内码、证件信息、贷款期限)+ 业务信息(省农担担保、绿色贷款、科技型企业等)
- 两个组件完全独立,各自实现格式化和计算方法
- 支持响应式布局,小屏幕自动切换为单列布局"

相关文档

  • 字段定义参考: doc/person.csvdoc/corp.csv
  • API 文档: doc/api/loan-pricing-workflow-api.md
  • 原详情页面: ruoyi-ui/src/views/loanPricing/workflow/detail.vue (修改前)