25 KiB
25 KiB
根据客户类型动态展示流程详情实施计划
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: 测试个人客户详情页
- 在列表页点击个人客户记录的"查看"按钮
- 确认页面正确显示 PersonalWorkflowDetail 组件
- 确认所有个人客户字段正确显示
- 确认模型输出正确展示(retailOutput)
- 测试执行利率设定功能
Step 2: 测试企业客户详情页
- 在列表页点击企业客户记录的"查看"按钮
- 确认页面正确显示 CorporateWorkflowDetail 组件
- 确认所有企业客户字段正确显示
- 确认模型输出正确展示(corpOutput)
- 测试执行利率设定功能
Step 3: 响应式布局测试
- 调整浏览器窗口宽度到小于992px
- 确认布局自动切换为单列垂直布局
- 确认所有内容正常显示
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.csv和doc/corp.csv - API 文档:
doc/api/loan-pricing-workflow-api.md - 原详情页面:
ruoyi-ui/src/views/loanPricing/workflow/detail.vue(修改前)